diff options
| author | Laurenz <laurmaedje@gmail.com> | 2024-10-27 19:04:55 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-10-27 18:04:55 +0000 |
| commit | be7cfc85d08c545abfac08098b7b33b4bd71f37e (patch) | |
| tree | f4137fa2aaa57babae1f7603a9b2ed7e688f43d8 /crates/typst-library/src | |
| parent | b8034a343831e8609aec2ec81eb7eeda57aa5d81 (diff) | |
Split out four new crates (#5302)
Diffstat (limited to 'crates/typst-library/src')
152 files changed, 51548 insertions, 0 deletions
diff --git a/crates/typst-library/src/diag.rs b/crates/typst-library/src/diag.rs new file mode 100644 index 00000000..bd4c90a1 --- /dev/null +++ b/crates/typst-library/src/diag.rs @@ -0,0 +1,580 @@ +//! 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 ecow::{eco_vec, EcoVec}; +use typst_syntax::package::{PackageSpec, PackageVersion}; +use typst_syntax::{Span, Spanned, SyntaxError}; + +use crate::{World, WorldExt}; + +/// 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`. +/// +/// You can also emit hints with the `; hint: "..."` syntax. +/// +/// ```ignore +/// bail!("bailing with a {}", "string result"); +/// bail!(span, "bailing with a {}", "source result"); +/// bail!( +/// span, "bailing with a {}", "source result"; +/// hint: "hint 1" +/// ); +/// bail!( +/// span, "bailing with a {}", "source result"; +/// hint: "hint 1"; +/// hint: "hint 2"; +/// ); +/// ``` +#[macro_export] +#[doc(hidden)] +macro_rules! __bail { + // For bail!("just a {}", "string") + ( + $fmt:literal $(, $arg:expr)* + $(; hint: $hint:literal $(, $hint_arg:expr)*)* + $(,)? + ) => { + return Err($crate::diag::error!( + $fmt $(, $arg)* + $(; hint: $hint $(, $hint_arg)*)* + )) + }; + + // For bail!(error!(..)) + ($error:expr) => { + return Err(::ecow::eco_vec![$error]) + }; + + // For bail(span, ...) + ($($tts:tt)*) => { + return Err(::ecow::eco_vec![$crate::diag::error!($($tts)*)]) + }; +} + +/// Construct an [`EcoString`], [`HintedString`] or [`SourceDiagnostic`] with +/// severity `Error`. +#[macro_export] +#[doc(hidden)] +macro_rules! __error { + // For bail!("just a {}", "string"). + ($fmt:literal $(, $arg:expr)* $(,)?) => { + $crate::diag::eco_format!($fmt, $($arg),*).into() + }; + + // For bail!("a hinted {}", "string"; hint: "some hint"; hint: "...") + ( + $fmt:literal $(, $arg:expr)* + $(; hint: $hint:literal $(, $hint_arg:expr)*)* + $(,)? + ) => { + $crate::diag::HintedString::new( + $crate::diag::eco_format!($fmt, $($arg),*) + ) $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))* + }; + + // For bail!(span, ...) + ( + $span:expr, $fmt:literal $(, $arg:expr)* + $(; hint: $hint:literal $(, $hint_arg:expr)*)* + $(,)? + ) => { + $crate::diag::SourceDiagnostic::error( + $span, + $crate::diag::eco_format!($fmt, $($arg),*), + ) $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))* + }; +} + +/// Construct a [`SourceDiagnostic`] with severity `Warning`. +/// +/// You can also emit hints with the `; hint: "..."` syntax. +/// +/// ```ignore +/// warning!(span, "warning with a {}", "source result"); +/// warning!( +/// span, "warning with a {}", "source result"; +/// hint: "hint 1" +/// ); +/// warning!( +/// span, "warning with a {}", "source result"; +/// hint: "hint 1"; +/// hint: "hint 2"; +/// ); +/// ``` +#[macro_export] +#[doc(hidden)] +macro_rules! __warning { + ( + $span:expr, + $fmt:literal $(, $arg:expr)* + $(; hint: $hint:literal $(, $hint_arg:expr)*)* + $(,)? + ) => { + $crate::diag::SourceDiagnostic::warning( + $span, + $crate::diag::eco_format!($fmt, $($arg),*), + ) $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))* + }; +} + +#[rustfmt::skip] +#[doc(inline)] +pub use { + crate::__bail as bail, + crate::__error as error, + crate::__warning as warning, + ecow::{eco_format, EcoString}, +}; + +/// A result that can carry multiple source errors. +pub type SourceResult<T> = Result<T, EcoVec<SourceDiagnostic>>; + +/// An output alongside warnings generated while producing it. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Warned<T> { + /// The produced output. + pub output: T, + /// Warnings generated while producing the output. + pub warnings: EcoVec<SourceDiagnostic>, +} + +/// An error or warning 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 SourceDiagnostic { + /// Whether the diagnostic is an error or a warning. + pub severity: Severity, + /// The span of the relevant 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 problem. + pub trace: EcoVec<Spanned<Tracepoint>>, + /// Additional hints to the user, indicating how this problem could be avoided + /// or worked around. + pub hints: EcoVec<EcoString>, +} + +/// The severity of a [`SourceDiagnostic`]. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum Severity { + /// A fatal error. + Error, + /// A non-fatal warning. + Warning, +} + +impl SourceDiagnostic { + /// Create a new, bare error. + pub fn error(span: Span, message: impl Into<EcoString>) -> Self { + Self { + severity: Severity::Error, + span, + trace: eco_vec![], + message: message.into(), + hints: eco_vec![], + } + } + + /// Create a new, bare warning. + pub fn warning(span: Span, message: impl Into<EcoString>) -> Self { + Self { + severity: Severity::Warning, + span, + trace: eco_vec![], + message: message.into(), + hints: eco_vec![], + } + } + + /// Adds a single hint to the diagnostic. + pub fn hint(&mut self, hint: impl Into<EcoString>) { + self.hints.push(hint.into()); + } + + /// Adds a single hint to the diagnostic. + pub fn with_hint(mut self, hint: impl Into<EcoString>) -> Self { + self.hint(hint); + self + } + + /// Adds user-facing hints to the diagnostic. + pub fn with_hints(mut self, hints: impl IntoIterator<Item = EcoString>) -> Self { + self.hints.extend(hints); + self + } +} + +impl From<SyntaxError> for SourceDiagnostic { + fn from(error: SyntaxError) -> Self { + Self { + severity: Severity::Error, + span: error.span, + message: error.message, + trace: eco_vec![], + hints: error.hints, + } + } +} + +/// A part of a diagnostic's [trace](SourceDiagnostic::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| { + let Some(trace_range) = world.range(span) else { return errors }; + for error in errors.make_mut().iter_mut() { + // Skip traces that surround the error. + if let Some(error_range) = world.range(error.span) { + if error.span.id() == span.id() + && 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`] or [`HintedStrResult`] 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| { + let mut diagnostic = SourceDiagnostic::error(span, message); + if diagnostic.message.contains("(access denied)") { + diagnostic.hint("cannot read file outside of project root"); + diagnostic + .hint("you can adjust the project root with the --root argument"); + } + eco_vec![diagnostic] + }) + } +} + +/// A result type with a string error message and hints. +pub type HintedStrResult<T> = Result<T, HintedString>; + +/// A string message with hints. +/// +/// This is internally represented by a vector of strings. +/// The first element of the vector contains the message. +/// The remaining elements are the hints. +/// This is done to reduce the size of a HintedString. +/// The vector is guaranteed to not be empty. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct HintedString(EcoVec<EcoString>); + +impl HintedString { + /// Creates a new hinted string with the given message. + pub fn new(message: EcoString) -> Self { + Self(eco_vec![message]) + } + + /// A diagnostic message describing the problem. + pub fn message(&self) -> &EcoString { + self.0.first().unwrap() + } + + /// Additional hints to the user, indicating how this error could be avoided + /// or worked around. + pub fn hints(&self) -> &[EcoString] { + self.0.get(1..).unwrap_or(&[]) + } + + /// Adds a single hint to the hinted string. + pub fn hint(&mut self, hint: impl Into<EcoString>) { + self.0.push(hint.into()); + } + + /// Adds a single hint to the hinted string. + pub fn with_hint(mut self, hint: impl Into<EcoString>) -> Self { + self.hint(hint); + self + } + + /// Adds user-facing hints to the hinted string. + pub fn with_hints(mut self, hints: impl IntoIterator<Item = EcoString>) -> Self { + self.0.extend(hints); + self + } +} + +impl<S> From<S> for HintedString +where + S: Into<EcoString>, +{ + fn from(value: S) -> Self { + Self::new(value.into()) + } +} + +impl<T> At<T> for HintedStrResult<T> { + fn at(self, span: Span) -> SourceResult<T> { + self.map_err(|err| { + let mut components = err.0.into_iter(); + let message = components.next().unwrap(); + let diag = SourceDiagnostic::error(span, message).with_hints(components); + eco_vec![diag] + }) + } +} + +/// 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, S> Hint<T> for Result<T, S> +where + S: Into<EcoString>, +{ + fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T> { + self.map_err(|message| HintedString::new(message.into()).with_hint(hint)) + } +} + +impl<T> Hint<T> for HintedStrResult<T> { + fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T> { + self.map_err(|mut error| { + error.hint(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. + /// + /// The optional string can give more details, if available. + Other(Option<EcoString>), +} + +impl FileError { + /// Create a file error from an I/O error. + pub fn from_io(err: io::Error, path: &Path) -> Self { + match err.kind() { + io::ErrorKind::NotFound => Self::NotFound(path.into()), + io::ErrorKind::PermissionDenied => Self::AccessDenied, + io::ErrorKind::InvalidData + if err.to_string().contains("stream did not contain valid UTF-8") => + { + Self::InvalidUtf8 + } + _ => Self::Other(Some(eco_format!("{err}"))), + } + } +} + +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(Some(err)) => write!(f, "failed to load file ({err})"), + Self::Other(None) => 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(err: PackageError) -> Self { + Self::Package(err) + } +} + +impl From<FileError> for EcoString { + fn from(err: FileError) -> Self { + eco_format!("{err}") + } +} + +/// A result type with a package-related error. +pub type PackageResult<T> = Result<T, PackageError>; + +/// An error that occurred while trying to load a package. +/// +/// Some variants have an optional string can give more details, if available. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum PackageError { + /// The specified package does not exist. + NotFound(PackageSpec), + /// The specified package found, but the version does not exist. + VersionNotFound(PackageSpec, PackageVersion), + /// Failed to retrieve the package through the network. + NetworkFailed(Option<EcoString>), + /// The package archive was malformed. + MalformedArchive(Option<EcoString>), + /// Another error. + Other(Option<EcoString>), +} + +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::VersionNotFound(spec, latest) => { + write!( + f, + "package found, but version {} does not exist (latest is {})", + spec.version, latest, + ) + } + Self::NetworkFailed(Some(err)) => { + write!(f, "failed to download package ({err})") + } + Self::NetworkFailed(None) => f.pad("failed to download package"), + Self::MalformedArchive(Some(err)) => { + write!(f, "failed to decompress package ({err})") + } + Self::MalformedArchive(None) => { + f.pad("failed to decompress package (archive malformed)") + } + Self::Other(Some(err)) => write!(f, "failed to load package ({err})"), + Self::Other(None) => f.pad("failed to load package"), + } + } +} + +impl From<PackageError> for EcoString { + fn from(err: PackageError) -> Self { + eco_format!("{err}") + } +} + +/// 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)") + } + err => eco_format!("failed to parse {format} ({err})"), + } +} diff --git a/crates/typst-library/src/engine.rs b/crates/typst-library/src/engine.rs new file mode 100644 index 00000000..e532172e --- /dev/null +++ b/crates/typst-library/src/engine.rs @@ -0,0 +1,387 @@ +//! Definition of the central compilation context. + +use std::collections::HashSet; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use comemo::{Track, Tracked, TrackedMut, Validate}; +use ecow::EcoVec; +use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator}; +use typst_syntax::{FileId, Span}; + +use crate::diag::{bail, HintedStrResult, SourceDiagnostic, SourceResult, StrResult}; +use crate::foundations::{Styles, Value}; +use crate::introspection::Introspector; +use crate::routines::Routines; +use crate::World; + +/// Holds all data needed during compilation. +pub struct Engine<'a> { + /// Defines implementation of various Typst compiler routines as a table of + /// function pointers. + pub routines: &'a Routines, + /// The compilation environment. + pub world: Tracked<'a, dyn World + 'a>, + /// Provides access to information about the document. + pub introspector: Tracked<'a, Introspector>, + /// May hold a span that is currently under inspection. + pub traced: Tracked<'a, Traced>, + /// A pure sink for warnings, delayed errors, and spans under inspection. + pub sink: TrackedMut<'a, Sink>, + /// The route the engine took during compilation. This is used to detect + /// cyclic imports and excessive nesting. + pub route: Route<'a>, +} + +impl Engine<'_> { + /// Handles a result without immediately terminating execution. Instead, it + /// produces a delayed error that is only promoted to a fatal one if it + /// remains by the end of the introspection loop. + pub fn delay<T: Default>(&mut self, result: SourceResult<T>) -> T { + match result { + Ok(value) => value, + Err(errors) => { + self.sink.delay(errors); + T::default() + } + } + } + + /// Runs tasks on the engine in parallel. + pub fn parallelize<P, I, T, U, F>(&mut self, iter: P, f: F) -> impl Iterator<Item = U> + where + P: IntoIterator<IntoIter = I>, + I: Iterator<Item = T>, + T: Send, + U: Send, + F: Fn(&mut Engine, T) -> U + Send + Sync, + { + let Engine { + world, introspector, traced, ref route, routines, .. + } = *self; + + // We collect into a vector and then call `into_par_iter` instead of + // using `par_bridge` because it does not retain the ordering. + let work: Vec<T> = iter.into_iter().collect(); + + // Work in parallel. + let mut pairs: Vec<(U, Sink)> = Vec::with_capacity(work.len()); + work.into_par_iter() + .map(|value| { + let mut sink = Sink::new(); + let mut engine = Engine { + world, + introspector, + traced, + sink: sink.track_mut(), + route: route.clone(), + routines, + }; + (f(&mut engine, value), sink) + }) + .collect_into_vec(&mut pairs); + + // Apply the subsinks to the outer sink. + for (_, sink) in &mut pairs { + let sink = std::mem::take(sink); + self.sink.extend(sink.delayed, sink.warnings, sink.values); + } + + pairs.into_iter().map(|(output, _)| output) + } +} + +/// May hold a span that is currently under inspection. +#[derive(Default)] +pub struct Traced(Option<Span>); + +impl Traced { + /// Wraps a to-be-traced `Span`. + /// + /// Call `Traced::default()` to trace nothing. + pub fn new(traced: Span) -> Self { + Self(Some(traced)) + } +} + +#[comemo::track] +impl Traced { + /// Returns the traced span _if_ it is part of the given source file or + /// `None` otherwise. + /// + /// We hide the span if it isn't in the given file so that only results for + /// the file with the traced span are invalidated. + pub fn get(&self, id: FileId) -> Option<Span> { + if self.0.and_then(Span::id) == Some(id) { + self.0 + } else { + None + } + } +} + +/// A push-only sink for delayed errors, warnings, and traced values. +/// +/// All tracked methods of this type are of the form `(&mut self, ..) -> ()`, so +/// in principle they do not need validation (though that optimization is not +/// yet implemented in comemo). +#[derive(Default, Clone)] +pub struct Sink { + /// Delayed errors: Those are errors that we can ignore until the last + /// iteration. For instance, show rules may throw during earlier iterations + /// because the introspector is not yet ready. We first ignore that and + /// proceed with empty content and only if the error remains by the end + /// of the last iteration, we promote it. + delayed: EcoVec<SourceDiagnostic>, + /// Warnings emitted during iteration. + warnings: EcoVec<SourceDiagnostic>, + /// Hashes of all warning's spans and messages for warning deduplication. + warnings_set: HashSet<u128>, + /// A sequence of traced values for a span. + values: EcoVec<(Value, Option<Styles>)>, +} + +impl Sink { + /// The maximum number of traced values. + pub const MAX_VALUES: usize = 10; + + /// Create a new empty sink. + pub fn new() -> Self { + Self::default() + } + + /// Get the stored delayed errors. + pub fn delayed(&mut self) -> EcoVec<SourceDiagnostic> { + std::mem::take(&mut self.delayed) + } + + /// Get the stored warnings. + pub fn warnings(self) -> EcoVec<SourceDiagnostic> { + self.warnings + } + + /// Get the values for the traced span. + pub fn values(self) -> EcoVec<(Value, Option<Styles>)> { + self.values + } + + /// Extend from another sink. + pub fn extend_from_sink(&mut self, other: Sink) { + self.extend(other.delayed, other.warnings, other.values); + } +} + +#[comemo::track] +impl Sink { + /// Push delayed errors. + pub fn delay(&mut self, errors: EcoVec<SourceDiagnostic>) { + self.delayed.extend(errors); + } + + /// Add a warning. + pub fn warn(&mut self, warning: SourceDiagnostic) { + // Check if warning is a duplicate. + let hash = typst_utils::hash128(&(&warning.span, &warning.message)); + if self.warnings_set.insert(hash) { + self.warnings.push(warning); + } + } + + /// Trace a value and optionally styles for the traced span. + pub fn value(&mut self, value: Value, styles: Option<Styles>) { + if self.values.len() < Self::MAX_VALUES { + self.values.push((value, styles)); + } + } + + /// Extend from parts of another sink. + fn extend( + &mut self, + delayed: EcoVec<SourceDiagnostic>, + warnings: EcoVec<SourceDiagnostic>, + values: EcoVec<(Value, Option<Styles>)>, + ) { + self.delayed.extend(delayed); + for warning in warnings { + self.warn(warning); + } + if let Some(remaining) = Self::MAX_VALUES.checked_sub(self.values.len()) { + self.values.extend(values.into_iter().take(remaining)); + } + } +} + +/// The route the engine took during compilation. This is used to detect +/// cyclic imports and excessive nesting. +pub struct Route<'a> { + /// The parent route segment, if present. + /// + /// This is used when an engine is created from another engine. + // 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>>, + /// This is set if this route segment was inserted through the start of a + /// module evaluation. + id: Option<FileId>, + /// This is set whenever we enter a function, nested layout, or are applying + /// a show rule. The length of this segment plus the lengths of all `outer` + /// route segments make up the length of the route. If the length of the + /// route exceeds `MAX_DEPTH`, then we throw a "maximum ... depth exceeded" + /// error. + len: usize, + /// The upper bound we've established for the parent chain length. + /// + /// We don't know the exact length (that would defeat the whole purpose + /// because it would prevent cache reuse of some computation at different, + /// non-exceeding depths). + upper: AtomicUsize, +} + +impl<'a> Route<'a> { + /// Create a new, empty route. + pub fn root() -> Self { + Self { + id: None, + outer: None, + len: 0, + upper: AtomicUsize::new(0), + } + } + + /// Extend the route with another segment with a default length of 1. + pub fn extend(outer: Tracked<'a, Self>) -> Self { + Route { + outer: Some(outer), + id: None, + len: 1, + upper: AtomicUsize::new(usize::MAX), + } + } + + /// Attach a file id to the route segment. + pub fn with_id(self, id: FileId) -> Self { + Self { id: Some(id), ..self } + } + + /// Set the length of the route segment to zero. + pub fn unnested(self) -> Self { + Self { len: 0, ..self } + } + + /// Start tracking this route. + /// + /// 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() && self.len == 0 => outer, + _ => Track::track(self), + } + } + + /// Increase the nesting depth for this route segment. + pub fn increase(&mut self) { + self.len += 1; + } + + /// Decrease the nesting depth for this route segment. + pub fn decrease(&mut self) { + self.len -= 1; + } +} + +/// The maximum nesting depths. They are different so that even if show rule and +/// call checks are interleaved, for show rule problems we always get the show +/// rule error. The lower the max depth for a kind of error, the higher its +/// precedence compared to the others. +impl Route<'_> { + /// The maximum stack nesting depth. + const MAX_SHOW_RULE_DEPTH: usize = 64; + + /// The maximum layout nesting depth. + const MAX_LAYOUT_DEPTH: usize = 72; + + /// The maximum function call nesting depth. + const MAX_CALL_DEPTH: usize = 80; + + /// Ensures that we are within the maximum show rule depth. + pub fn check_show_depth(&self) -> HintedStrResult<()> { + if !self.within(Route::MAX_SHOW_RULE_DEPTH) { + bail!( + "maximum show rule depth exceeded"; + hint: "check whether the show rule matches its own output" + ); + } + Ok(()) + } + + /// Ensures that we are within the maximum layout depth. + pub fn check_layout_depth(&self) -> HintedStrResult<()> { + if !self.within(Route::MAX_LAYOUT_DEPTH) { + bail!( + "maximum layout depth exceeded"; + hint: "try to reduce the amount of nesting in your layout", + ); + } + Ok(()) + } + + /// Ensures that we are within the maximum function call depth. + pub fn check_call_depth(&self) -> StrResult<()> { + if !self.within(Route::MAX_CALL_DEPTH) { + bail!("maximum function call depth exceeded"); + } + Ok(()) + } +} + +#[comemo::track] +impl<'a> Route<'a> { + /// Whether the given id is part of the route. + pub fn contains(&self, id: FileId) -> bool { + self.id == Some(id) || self.outer.is_some_and(|outer| outer.contains(id)) + } + + /// Whether the route's depth is less than or equal to the given depth. + pub fn within(&self, depth: usize) -> bool { + // We only need atomicity and no synchronization of other operations, so + // `Relaxed` is fine. + use Ordering::Relaxed; + + let upper = self.upper.load(Relaxed); + if upper.saturating_add(self.len) <= depth { + return true; + } + + match self.outer { + Some(_) if depth < self.len => false, + Some(outer) => { + let within = outer.within(depth - self.len); + if within && depth < upper { + // We don't want to accidentally increase the upper bound, + // hence the compare-exchange. + self.upper.compare_exchange(upper, depth, Relaxed, Relaxed).ok(); + } + within + } + None => true, + } + } +} + +impl Default for Route<'_> { + fn default() -> Self { + Self::root() + } +} + +impl Clone for Route<'_> { + fn clone(&self) -> Self { + Self { + outer: self.outer, + id: self.id, + len: self.len, + upper: AtomicUsize::new(self.upper.load(Ordering::Relaxed)), + } + } +} diff --git a/crates/typst-library/src/foundations/args.rs b/crates/typst-library/src/foundations/args.rs new file mode 100644 index 00000000..ee282a87 --- /dev/null +++ b/crates/typst-library/src/foundations/args.rs @@ -0,0 +1,453 @@ +use std::fmt::{self, Debug, Formatter}; + +use ecow::{eco_format, eco_vec, EcoString, EcoVec}; +use typst_syntax::{Span, Spanned}; + +use crate::diag::{bail, error, At, SourceDiagnostic, SourceResult, StrResult}; +use crate::foundations::{ + cast, func, repr, scope, ty, Array, Dict, FromValue, IntoValue, Repr, Str, Value, +}; + +/// Captured arguments to a function. +/// +/// # Argument Sinks +/// Like built-in functions, custom functions can also take a variable number of +/// arguments. You can specify an _argument sink_ which collects all excess +/// arguments as `..sink`. The resulting `sink` value is of the `arguments` +/// type. It exposes methods to access the positional and named arguments. +/// +/// ```example +/// #let format(title, ..authors) = { +/// let by = authors +/// .pos() +/// .join(", ", last: " and ") +/// +/// [*#title* \ _Written by #by;_] +/// } +/// +/// #format("ArtosFlow", "Jane", "Joe") +/// ``` +/// +/// # Spreading +/// Inversely to an argument sink, you can _spread_ arguments, arrays and +/// dictionaries into a function call with the `..spread` operator: +/// +/// ```example +/// #let array = (2, 3, 5) +/// #calc.min(..array) +/// #let dict = (fill: blue) +/// #text(..dict)[Hello] +/// ``` +#[ty(scope, cast, name = "arguments")] +#[derive(Clone, Hash)] +#[allow(clippy::derived_hash_with_manual_eq)] +pub struct Args { + /// The callsite span for the function. This is not the span of the argument + /// list itself, but of the whole function call. + pub span: Span, + /// The positional and named arguments. + pub items: EcoVec<Arg>, +} + +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 } + } + + /// Attach a span to these arguments if they don't already have one. + pub fn spanned(mut self, span: Span) -> Self { + if self.span.is_detached() { + self.span = span; + } + self + } + + /// Returns the number of remaining positional arguments. + pub fn remaining(&self) -> usize { + self.items.iter().filter(|slot| slot.name.is_none()).count() + } + + /// Insert a positional argument at a specific index. + pub fn insert(&mut self, index: usize, span: Span, value: Value) { + self.items.insert( + index, + Arg { + span: self.span, + name: None, + value: Spanned::new(value, span), + }, + ) + } + + /// 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.missing_argument(what)), + } + } + + /// The error message for missing arguments. + fn missing_argument(&self, what: &str) -> SourceDiagnostic { + for item in &self.items { + let Some(name) = item.name.as_deref() else { continue }; + if name == what { + return error!( + item.span, + "the argument `{what}` is positional"; + hint: "try removing `{}:`", name, + ); + } + } + + error!(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![]; + let mut errors = eco_vec![]; + self.items.retain(|item| { + if item.name.is_some() { + return true; + }; + let span = item.value.span; + let spanned = Spanned::new(std::mem::take(&mut item.value.v), span); + match T::from_value(spanned).at(span) { + Ok(val) => list.push(val), + Err(diags) => errors.extend(diags), + } + false + }); + if !errors.is_empty() { + return Err(errors); + } + 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(()) + } +} + +/// A key that can be used to get an argument: either the index of a positional +/// argument, or the name of a named argument. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ArgumentKey { + Index(i64), + Name(Str), +} + +cast! { + ArgumentKey, + v: i64 => Self::Index(v), + v: Str => Self::Name(v), +} + +impl Args { + fn get(&self, key: &ArgumentKey) -> Option<&Value> { + let item = match key { + &ArgumentKey::Index(index) => { + let mut iter = self.items.iter().filter(|item| item.name.is_none()); + if index < 0 { + let index = (-(index + 1)).try_into().ok()?; + iter.nth_back(index) + } else { + let index = index.try_into().ok()?; + iter.nth(index) + } + } + // Accept the last argument with the right name. + ArgumentKey::Name(name) => { + self.items.iter().rfind(|item| item.name.as_ref() == Some(name)) + } + }; + item.map(|item| &item.value.v) + } +} + +#[scope] +impl Args { + /// Construct spreadable arguments in place. + /// + /// This function behaves like `{let args(..sink) = sink}`. + /// + /// ```example + /// #let args = arguments(stroke: red, inset: 1em, [Body]) + /// #box(..args) + /// ``` + #[func(constructor)] + pub fn construct( + /// The real arguments (the other argument is just for the docs). + /// The docs argument cannot be called `args`. + args: &mut Args, + /// The arguments to construct. + #[external] + #[variadic] + arguments: Vec<Value>, + ) -> Args { + args.take() + } + + /// Returns the positional argument at the specified index, or the named + /// argument with the specified name. + /// + /// If the key is an [integer]($int), this is equivalent to first calling + /// [`pos`]($arguments.pos) and then [`array.at`]. If it is a [string]($str), + /// this is equivalent to first calling [`named`]($arguments.named) and then + /// [`dictionary.at`]. + #[func] + pub fn at( + &self, + /// The index or name of the argument to get. + key: ArgumentKey, + /// A default value to return if the key is invalid. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { + self.get(&key) + .cloned() + .or(default) + .ok_or_else(|| missing_key_no_default(key)) + } + + /// Returns the captured positional arguments as an array. + #[func(name = "pos", title = "Positional")] + pub fn to_pos(&self) -> Array { + self.items + .iter() + .filter(|item| item.name.is_none()) + .map(|item| item.value.v.clone()) + .collect() + } + + /// Returns the captured named arguments as a dictionary. + #[func(name = "named")] + 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 { + f.debug_list().entries(&self.items).finish() + } +} + +impl Repr for Args { + fn repr(&self) -> EcoString { + let pieces = self.items.iter().map(Arg::repr).collect::<Vec<_>>(); + repr::pretty_array_like(&pieces, false).into() + } +} + +impl PartialEq for Args { + fn eq(&self, other: &Self) -> bool { + self.to_pos() == other.to_pos() && self.to_named() == other.to_named() + } +} + +/// An argument to a function call: `12` or `draw: false`. +#[derive(Clone, Hash)] +#[allow(clippy::derived_hash_with_manual_eq)] +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 Debug for Arg { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if let Some(name) = &self.name { + name.fmt(f)?; + f.write_str(": ")?; + self.value.v.fmt(f) + } else { + self.value.v.fmt(f) + } + } +} + +impl Repr for Arg { + fn repr(&self) -> EcoString { + if let Some(name) = &self.name { + eco_format!("{}: {}", name, self.value.v.repr()) + } else { + self.value.v.repr() + } + } +} + +impl PartialEq for Arg { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.value.v == other.value.v + } +} + +/// Things that can be used as arguments. +pub trait IntoArgs { + /// Convert into arguments, attaching the `fallback` span in case `Self` + /// doesn't have a span. + fn into_args(self, fallback: Span) -> Args; +} + +impl IntoArgs for Args { + fn into_args(self, fallback: Span) -> Args { + self.spanned(fallback) + } +} + +impl<I, T> IntoArgs for I +where + I: IntoIterator<Item = T>, + T: IntoValue, +{ + fn into_args(self, fallback: Span) -> Args { + Args::new(fallback, self) + } +} + +/// The missing key access error message when no default was given. +#[cold] +fn missing_key_no_default(key: ArgumentKey) -> EcoString { + eco_format!( + "arguments do not contain key {} \ + and no default value was specified", + match key { + ArgumentKey::Index(i) => i.repr(), + ArgumentKey::Name(name) => name.repr(), + } + ) +} diff --git a/crates/typst-library/src/foundations/array.rs b/crates/typst-library/src/foundations/array.rs new file mode 100644 index 00000000..9c8aecac --- /dev/null +++ b/crates/typst-library/src/foundations/array.rs @@ -0,0 +1,1133 @@ +use std::cmp::Ordering; +use std::fmt::{Debug, Formatter}; +use std::num::{NonZeroI64, NonZeroUsize}; +use std::ops::{Add, AddAssign}; + +use comemo::Tracked; +use ecow::{eco_format, EcoString, EcoVec}; +use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; +use typst_syntax::{Span, Spanned}; + +use crate::diag::{bail, At, HintedStrResult, SourceDiagnostic, SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, func, ops, repr, scope, ty, Args, Bytes, CastInfo, Context, Dict, FromValue, + Func, IntoValue, Reflect, Repr, Str, Value, Version, +}; + +/// Create a new [`Array`] from values. +#[macro_export] +#[doc(hidden)] +macro_rules! __array { + ($value:expr; $count:expr) => { + $crate::foundations::Array::from($crate::foundations::eco_vec![ + $crate::foundations::IntoValue::into_value($value); + $count + ]) + }; + + ($($value:expr),* $(,)?) => { + $crate::foundations::Array::from($crate::foundations::eco_vec![$( + $crate::foundations::IntoValue::into_value($value) + ),*]) + }; +} + +#[doc(inline)] +pub use crate::__array as array; + +/// A sequence of values. +/// +/// You can construct an array by enclosing a comma-separated sequence of values +/// in parentheses. The values do not have to be of the same type. +/// +/// You can access and update array items with the `.at()` method. Indices are +/// zero-based and negative indices wrap around to the end of the array. You can +/// iterate over an array using a [for loop]($scripting/#loops). Arrays can be +/// added together with the `+` operator, [joined together]($scripting/#blocks) +/// and multiplied with integers. +/// +/// **Note:** An array of length one needs a trailing comma, as in `{(1,)}`. +/// This is to disambiguate from a simple parenthesized expressions like `{(1 + +/// 2) * 3}`. An empty array is written as `{()}`. +/// +/// # Example +/// ```example +/// #let values = (1, 7, 4, -3, 2) +/// +/// #values.at(0) \ +/// #(values.at(0) = 3) +/// #values.at(-1) \ +/// #values.find(calc.even) \ +/// #values.filter(calc.odd) \ +/// #values.map(calc.abs) \ +/// #values.rev() \ +/// #(1, (2, 3)).flatten() \ +/// #(("A", "B", "C") +/// .join(", ", last: " and ")) +/// ``` +#[ty(scope, cast)] +#[derive(Default, Clone, PartialEq, Hash, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Array(EcoVec<Value>); + +impl Array { + /// Create a new, empty array. + pub fn new() -> Self { + Self::default() + } + + /// Creates a new vec, with a known capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self(EcoVec::with_capacity(capacity)) + } + + /// Return `true` if the length is 0. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// 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() + } + + /// 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) + } + + /// 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) + } + + /// 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_opt(index, false) + .and_then(move |i| self.0.make_mut().get_mut(i)) + .ok_or_else(|| out_of_bounds(index, len)) + } + + /// Resolve an index or throw an out of bounds error. + fn locate(&self, index: i64, end_ok: bool) -> StrResult<usize> { + self.locate_opt(index, end_ok) + .ok_or_else(|| out_of_bounds(index, self.len())) + } + + /// Resolve an index, if it is within bounds. + /// + /// `index == len` is considered in bounds if and only if `end_ok` is true. + fn locate_opt(&self, index: i64, end_ok: bool) -> Option<usize> { + let wrapped = + if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) }; + + wrapped + .and_then(|v| usize::try_from(v).ok()) + .filter(|&v| v < self.0.len() + end_ok as usize) + } + + /// Repeat this array `n` times. + pub fn repeat(&self, n: usize) -> StrResult<Self> { + let count = self + .len() + .checked_mul(n) + .ok_or_else(|| format!("cannot repeat this array {n} times"))?; + + Ok(self.iter().cloned().cycle().take(count).collect()) + } +} + +#[scope] +impl Array { + /// Converts a value to an array. + /// + /// Note that this function is only intended for conversion of a collection-like + /// value to an array, not for creation of an array from individual items. Use + /// the array syntax `(1, 2, 3)` (or `(1,)` for a single-element array) instead. + /// + /// ```example + /// #let hi = "Hello 😃" + /// #array(bytes(hi)) + /// ``` + #[func(constructor)] + pub fn construct( + /// The value that should be converted to an array. + value: ToArray, + ) -> Array { + value.0 + } + + /// The number of values in the array. + #[func(title = "Length")] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns the first item in the array. May be used on the left-hand side + /// of an assignment. Fails with an error if the array is empty. + #[func] + pub fn first(&self) -> StrResult<Value> { + self.0.first().cloned().ok_or_else(array_is_empty) + } + + /// Returns the last item in the array. May be used on the left-hand side of + /// an assignment. Fails with an error if the array is empty. + #[func] + pub fn last(&self) -> StrResult<Value> { + self.0.last().cloned().ok_or_else(array_is_empty) + } + + /// Returns the item at the specified index in the array. May be used on the + /// left-hand side of an assignment. Returns the default value if the index + /// is out of bounds or fails with an error if no default value was + /// specified. + #[func] + pub fn at( + &self, + /// The index at which to retrieve the item. If negative, indexes from + /// the back. + index: i64, + /// A default value to return if the index is out of bounds. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { + self.locate_opt(index, false) + .and_then(|i| self.0.get(i).cloned()) + .or(default) + .ok_or_else(|| out_of_bounds_no_default(index, self.len())) + } + + /// Adds a value to the end of the array. + #[func] + pub fn push( + &mut self, + /// The value to insert at the end of the array. + value: Value, + ) { + self.0.push(value); + } + + /// Removes the last item from the array and returns it. Fails with an error + /// if the array is empty. + #[func] + pub fn pop(&mut self) -> StrResult<Value> { + self.0.pop().ok_or_else(array_is_empty) + } + + /// Inserts a value into the array at the specified index, shifting all + /// subsequent elements to the right. Fails with an error if the index is + /// out of bounds. + /// + /// To replace an element of an array, use [`at`]($array.at). + #[func] + pub fn insert( + &mut self, + /// The index at which to insert the item. If negative, indexes from + /// the back. + index: i64, + /// The value to insert into the array. + value: Value, + ) -> StrResult<()> { + let i = self.locate(index, true)?; + self.0.insert(i, value); + Ok(()) + } + + /// Removes the value at the specified index from the array and return it. + #[func] + pub fn remove( + &mut self, + /// The index at which to remove the item. If negative, indexes from + /// the back. + index: i64, + /// A default value to return if the index is out of bounds. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { + self.locate_opt(index, false) + .map(|i| self.0.remove(i)) + .or(default) + .ok_or_else(|| out_of_bounds_no_default(index, self.len())) + } + + /// Extracts a subslice of the array. Fails with an error if the start or end + /// index is out of bounds. + #[func] + pub fn slice( + &self, + /// The start index (inclusive). If negative, indexes from the back. + start: i64, + /// The end index (exclusive). If omitted, the whole slice until the end + /// of the array is extracted. If negative, indexes from the back. + #[default] + end: Option<i64>, + /// The number of items to extract. This is equivalent to passing + /// `start + count` as the `end` position. Mutually exclusive with `end`. + #[named] + count: Option<i64>, + ) -> StrResult<Array> { + let mut end = end; + if end.is_none() { + end = count.map(|c: i64| start + c); + } + let start = self.locate(start, true)?; + let end = self.locate(end.unwrap_or(self.len() as i64), true)?.max(start); + Ok(self.0[start..end].into()) + } + + /// Whether the array contains the specified value. + /// + /// This method also has dedicated syntax: You can write `{2 in (1, 2, 3)}` + /// instead of `{(1, 2, 3).contains(2)}`. + #[func] + pub fn contains( + &self, + /// The value to search for. + value: Value, + ) -> bool { + self.0.contains(&value) + } + + /// Searches for an item for which the given function returns `{true}` and + /// returns the first match or `{none}` if there is no match. + #[func] + pub fn find( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The function to apply to each item. Must return a boolean. + searcher: Func, + ) -> SourceResult<Option<Value>> { + for item in self.iter() { + if searcher + .call(engine, context, [item.clone()])? + .cast::<bool>() + .at(searcher.span())? + { + return Ok(Some(item.clone())); + } + } + Ok(None) + } + + /// Searches for an item for which the given function returns `{true}` and + /// returns the index of the first match or `{none}` if there is no match. + #[func] + pub fn position( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The function to apply to each item. Must return a boolean. + searcher: Func, + ) -> SourceResult<Option<i64>> { + for (i, item) in self.iter().enumerate() { + if searcher + .call(engine, context, [item.clone()])? + .cast::<bool>() + .at(searcher.span())? + { + return Ok(Some(i as i64)); + } + } + + Ok(None) + } + + /// Create an array consisting of a sequence of numbers. + /// + /// If you pass just one positional parameter, it is interpreted as the + /// `end` of the range. If you pass two, they describe the `start` and `end` + /// of the range. + /// + /// This function is available both in the array function's scope and + /// globally. + /// + /// ```example + /// #range(5) \ + /// #range(2, 5) \ + /// #range(20, step: 4) \ + /// #range(21, step: 4) \ + /// #range(5, 2, step: -1) + /// ``` + #[func] + pub fn range( + /// The real arguments (the other arguments are just for the docs, this + /// function is a bit involved, so we parse the arguments manually). + args: &mut Args, + /// The start of the range (inclusive). + #[external] + #[default] + start: i64, + /// The end of the range (exclusive). + #[external] + end: i64, + /// The distance between the generated numbers. + #[named] + #[default(NonZeroI64::new(1).unwrap())] + step: NonZeroI64, + ) -> SourceResult<Array> { + let first = args.expect::<i64>("end")?; + let (start, end) = match args.eat::<i64>()? { + Some(second) => (first, second), + None => (0, first), + }; + + let step = step.get(); + + let mut x = start; + let mut array = Self::new(); + + while x.cmp(&end) == 0.cmp(&step) { + array.push(x.into_value()); + x += step; + } + + Ok(array) + } + + /// Produces a new array with only the items from the original one for which + /// the given function returns true. + #[func] + pub fn filter( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The function to apply to each item. Must return a boolean. + test: Func, + ) -> SourceResult<Array> { + let mut kept = EcoVec::new(); + for item in self.iter() { + if test + .call(engine, context, [item.clone()])? + .cast::<bool>() + .at(test.span())? + { + kept.push(item.clone()) + } + } + Ok(kept.into()) + } + + /// Produces a new array in which all items from the original one were + /// transformed with the given function. + #[func] + pub fn map( + self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The function to apply to each item. + mapper: Func, + ) -> SourceResult<Array> { + self.into_iter() + .map(|item| mapper.call(engine, context, [item])) + .collect() + } + + /// Returns a new array with the values alongside their indices. + /// + /// The returned array consists of `(index, value)` pairs in the form of + /// length-2 arrays. These can be [destructured]($scripting/#bindings) with + /// a let binding or for loop. + #[func] + pub fn enumerate( + self, + /// The index returned for the first pair of the returned list. + #[named] + #[default(0)] + start: i64, + ) -> StrResult<Array> { + self.into_iter() + .enumerate() + .map(|(i, value)| { + Ok(array![ + start + .checked_add_unsigned(i as u64) + .ok_or("array index is too large")?, + value + ] + .into_value()) + }) + .collect() + } + + /// Zips the array with other arrays. + /// + /// Returns an array of arrays, where the `i`th inner array contains all the + /// `i`th elements from each original array. + /// + /// If the arrays to be zipped have different lengths, they are zipped up to + /// the last element of the shortest array and all remaining elements are + /// ignored. + /// + /// This function is variadic, meaning that you can zip multiple arrays + /// together at once: `{(1, 2).zip(("A", "B"), (10, 20))}` yields + /// `{((1, "A", 10), (2, "B", 20))}`. + #[func] + pub fn zip( + self, + /// The real arguments (the `others` arguments are just for the docs, this + /// function is a bit involved, so we parse the positional arguments manually). + args: &mut Args, + /// Whether all arrays have to have the same length. + /// For example, `{(1, 2).zip((1, 2, 3), exact: true)}` produces an + /// error. + #[named] + #[default(false)] + exact: bool, + /// The arrays to zip with. + #[external] + #[variadic] + others: Vec<Array>, + ) -> SourceResult<Array> { + let remaining = args.remaining(); + + // Fast path for one array. + if remaining == 0 { + return Ok(self.into_iter().map(|item| array![item].into_value()).collect()); + } + + // Fast path for just two arrays. + if remaining == 1 { + let Spanned { v: other, span: other_span } = + args.expect::<Spanned<Array>>("others")?; + if exact && self.len() != other.len() { + bail!( + other_span, + "second array has different length ({}) from first array ({})", + other.len(), + self.len() + ); + } + return Ok(self + .into_iter() + .zip(other) + .map(|(first, second)| array![first, second].into_value()) + .collect()); + } + + // If there is more than one array, we use the manual method. + let mut out = Self::with_capacity(self.len()); + let arrays = args.all::<Spanned<Array>>()?; + if exact { + let errs = arrays + .iter() + .filter(|sp| sp.v.len() != self.len()) + .map(|Spanned { v, span }| { + SourceDiagnostic::error( + *span, + eco_format!( + "array has different length ({}) from first array ({})", + v.len(), + self.len() + ), + ) + }) + .collect::<EcoVec<_>>(); + if !errs.is_empty() { + return Err(errs); + } + } + + let mut iterators = + arrays.into_iter().map(|i| i.v.into_iter()).collect::<Vec<_>>(); + + for this in self { + let mut row = Self::with_capacity(1 + iterators.len()); + row.push(this.clone()); + + for iterator in &mut iterators { + let Some(item) = iterator.next() else { + return Ok(out); + }; + + row.push(item); + } + + out.push(row.into_value()); + } + + Ok(out) + } + + /// Folds all items into a single value using an accumulator function. + #[func] + pub fn fold( + self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The initial value to start with. + init: Value, + /// The folding function. Must have two parameters: One for the + /// accumulated value and one for an item. + folder: Func, + ) -> SourceResult<Value> { + let mut acc = init; + for item in self { + acc = folder.call(engine, context, [acc, item])?; + } + Ok(acc) + } + + /// Sums all items (works for all types that can be added). + #[func] + pub fn sum( + self, + /// What to return if the array is empty. Must be set if the array can + /// be empty. + #[named] + default: Option<Value>, + ) -> HintedStrResult<Value> { + let mut iter = self.into_iter(); + let mut acc = iter + .next() + .or(default) + .ok_or("cannot calculate sum of empty array with no default")?; + for item in iter { + acc = ops::add(acc, item)?; + } + Ok(acc) + } + + /// Calculates the product all items (works for all types that can be + /// multiplied). + #[func] + pub fn product( + self, + /// What to return if the array is empty. Must be set if the array can + /// be empty. + #[named] + default: Option<Value>, + ) -> HintedStrResult<Value> { + let mut iter = self.into_iter(); + let mut acc = iter + .next() + .or(default) + .ok_or("cannot calculate product of empty array with no default")?; + for item in iter { + acc = ops::mul(acc, item)?; + } + Ok(acc) + } + + /// Whether the given function returns `{true}` for any item in the array. + #[func] + pub fn any( + self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The function to apply to each item. Must return a boolean. + test: Func, + ) -> SourceResult<bool> { + for item in self { + if test.call(engine, context, [item])?.cast::<bool>().at(test.span())? { + return Ok(true); + } + } + + Ok(false) + } + + /// Whether the given function returns `{true}` for all items in the array. + #[func] + pub fn all( + self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The function to apply to each item. Must return a boolean. + test: Func, + ) -> SourceResult<bool> { + for item in self { + if !test.call(engine, context, [item])?.cast::<bool>().at(test.span())? { + return Ok(false); + } + } + + Ok(true) + } + + /// Combine all nested arrays into a single flat one. + #[func] + pub fn flatten(self) -> Array { + let mut flat = EcoVec::with_capacity(self.0.len()); + for item in self { + if let Value::Array(nested) = item { + flat.extend(nested.flatten()); + } else { + flat.push(item); + } + } + flat.into() + } + + /// Return a new array with the same items, but in reverse order. + #[func(title = "Reverse")] + pub fn rev(self) -> Array { + self.into_iter().rev().collect() + } + + /// Split the array at occurrences of the specified value. + #[func] + pub fn split( + &self, + /// The value to split at. + at: Value, + ) -> Array { + self.as_slice() + .split(|value| *value == at) + .map(|subslice| Value::Array(subslice.iter().cloned().collect())) + .collect() + } + + /// Combine all items in the array into one. + #[func] + pub fn join( + self, + /// A value to insert between each item of the array. + #[default] + separator: Option<Value>, + /// An alternative separator between the last two items. + #[named] + last: Option<Value>, + ) -> StrResult<Value> { + let len = self.0.len(); + let separator = separator.unwrap_or(Value::None); + + let mut last = last; + let mut result = Value::None; + for (i, value) in self.into_iter().enumerate() { + if i > 0 { + if i + 1 == len && last.is_some() { + result = ops::join(result, last.take().unwrap())?; + } else { + result = ops::join(result, separator.clone())?; + } + } + + result = ops::join(result, value)?; + } + + Ok(result) + } + + /// Returns an array with a copy of the separator value placed between + /// adjacent elements. + #[func] + pub fn intersperse( + self, + /// The value that will be placed between each adjacent element. + separator: Value, + ) -> Array { + // TODO: Use once stabilized: + // https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.intersperse + let size = match self.len() { + 0 => return Array::new(), + n => (2 * n) - 1, + }; + let mut vec = EcoVec::with_capacity(size); + let mut iter = self.into_iter(); + + if let Some(first) = iter.next() { + vec.push(first); + } + + for value in iter { + vec.push(separator.clone()); + vec.push(value); + } + + Array(vec) + } + + /// Splits an array into non-overlapping chunks, starting at the beginning, + /// ending with a single remainder chunk. + /// + /// All chunks but the last have `chunk-size` elements. + /// If `exact` is set to `{true}`, the remainder is dropped if it + /// contains less than `chunk-size` elements. + /// + /// ```example + /// #let array = (1, 2, 3, 4, 5, 6, 7, 8) + /// #array.chunks(3) + /// #array.chunks(3, exact: true) + /// ``` + #[func] + pub fn chunks( + self, + /// How many elements each chunk may at most contain. + chunk_size: NonZeroUsize, + /// Whether to keep the remainder if its size is less than `chunk-size`. + #[named] + #[default(false)] + exact: bool, + ) -> Array { + let to_array = |chunk| Array::from(chunk).into_value(); + if exact { + self.0.chunks_exact(chunk_size.get()).map(to_array).collect() + } else { + self.0.chunks(chunk_size.get()).map(to_array).collect() + } + } + + /// Returns sliding windows of `window-size` elements over an array. + /// + /// If the array length is less than `window-size`, this will return an empty array. + /// + /// ```example + /// #let array = (1, 2, 3, 4, 5, 6, 7, 8) + /// #array.windows(5) + /// ``` + #[func] + pub fn windows( + self, + /// How many elements each window will contain. + window_size: NonZeroUsize, + ) -> Array { + self.0 + .windows(window_size.get()) + .map(|window| Array::from(window).into_value()) + .collect() + } + + /// Return a sorted version of this array, optionally by a given key + /// function. The sorting algorithm used is stable. + /// + /// Returns an error if two values could not be compared or if the key + /// function (if given) yields an error. + #[func] + pub fn sorted( + self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The callsite span. + span: Span, + /// If given, applies this function to the elements in the array to + /// determine the keys to sort by. + #[named] + key: Option<Func>, + ) -> SourceResult<Array> { + let mut result = Ok(()); + let mut vec = self.0; + 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(engine, context, [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)) => 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()) + } + + /// Deduplicates all items in the array. + /// + /// Returns a new array with all duplicate items removed. Only the first + /// element of each duplicate is kept. + /// + /// ```example + /// #(1, 1, 2, 3, 1).dedup() + /// ``` + #[func(title = "Deduplicate")] + pub fn dedup( + self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// If given, applies this function to the elements in the array to + /// determine the keys to deduplicate by. + #[named] + key: Option<Func>, + ) -> SourceResult<Array> { + let mut out = EcoVec::with_capacity(self.0.len()); + 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(engine, context, [x]), + None => Ok(x), + }; + + // This algorithm is O(N^2) because we cannot rely on `HashSet` since: + // 1. We would like to preserve the order of the elements. + // 2. We cannot hash arbitrary `Value`. + 'outer: for value in self { + let key = key_of(value.clone())?; + if out.is_empty() { + out.push(value); + continue; + } + + for second in out.iter() { + if ops::equal(&key, &key_of(second.clone())?) { + continue 'outer; + } + } + + out.push(value); + } + + Ok(Self(out)) + } + + /// Converts an array of pairs into a dictionary. + /// The first value of each pair is the key, the second the value. + /// + /// If the same key occurs multiple times, the last value is selected. + /// + /// ```example + /// #( + /// ("apples", 2), + /// ("peaches", 3), + /// ("apples", 5), + /// ).to-dict() + /// ``` + #[func] + pub fn to_dict(self) -> StrResult<Dict> { + self.into_iter() + .map(|value| { + let value_ty = value.ty(); + let pair = value.cast::<Array>().map_err(|_| { + eco_format!("expected (str, any) pairs, found {}", value_ty) + })?; + if let [key, value] = pair.as_slice() { + let key = key.clone().cast::<Str>().map_err(|_| { + eco_format!("expected key of type str, found {}", value.ty()) + })?; + Ok((key, value.clone())) + } else { + bail!("expected pairs of length 2, found length {}", pair.len()); + } + }) + .collect() + } + + /// Reduces the elements to a single one, by repeatedly applying a reducing + /// operation. + /// + /// If the array is empty, returns `{none}`, otherwise, returns the result + /// of the reduction. + /// + /// The reducing function is a closure with two arguments: an "accumulator", + /// and an element. + /// + /// For arrays with at least one element, this is the same as [`array.fold`] + /// with the first element of the array as the initial accumulator value, + /// folding every subsequent element into it. + #[func] + pub fn reduce( + self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The reducing function. Must have two parameters: One for the + /// accumulated value and one for an item. + reducer: Func, + ) -> SourceResult<Value> { + let mut iter = self.into_iter(); + let mut acc = iter.next().unwrap_or_default(); + for item in iter { + acc = reducer.call(engine, context, [acc, item])?; + } + Ok(acc) + } +} + +/// A value that can be cast to bytes. +pub struct ToArray(Array); + +cast! { + ToArray, + v: Array => Self(v), + v: Bytes => Self(v.iter().map(|&b| Value::Int(b.into())).collect()), + v: Version => Self(v.values().iter().map(|&v| Value::Int(v as i64)).collect()) +} + +impl Debug for Array { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + f.debug_list().entries(&self.0).finish() + } +} + +impl Repr for Array { + fn repr(&self) -> EcoString { + let max = 40; + let mut pieces: Vec<_> = self + .iter() + .take(max) + .map(|value| eco_format!("{}", value.repr())) + .collect(); + if self.len() > max { + pieces.push(eco_format!(".. ({} items omitted)", self.len() - max)); + } + repr::pretty_array_like(&pieces, self.len() == 1).into() + } +} + +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: Self) { + 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 input() -> CastInfo { + Array::input() + } + + fn output() -> CastInfo { + Array::output() + } + + fn castable(value: &Value) -> bool { + Array::castable(value) + } +} + +impl<T: Reflect, const N: usize> Reflect for SmallVec<[T; N]> { + fn input() -> CastInfo { + Array::input() + } + + fn output() -> CastInfo { + Array::output() + } + + 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: IntoValue, const N: usize> IntoValue for SmallVec<[T; N]> { + 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) -> HintedStrResult<Self> { + value.cast::<Array>()?.into_iter().map(Value::cast).collect() + } +} + +impl<T: FromValue, const N: usize> FromValue for SmallVec<[T; N]> { + fn from_value(value: Value) -> HintedStrResult<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/crates/typst-library/src/foundations/auto.rs b/crates/typst-library/src/foundations/auto.rs new file mode 100644 index 00000000..8237b450 --- /dev/null +++ b/crates/typst-library/src/foundations/auto.rs @@ -0,0 +1,267 @@ +use std::fmt::{self, Debug, Formatter}; + +use ecow::EcoString; + +use crate::diag::HintedStrResult; +use crate::foundations::{ + ty, CastInfo, Fold, FromValue, IntoValue, Reflect, Repr, Resolve, StyleChain, Type, + Value, +}; + +/// A value that indicates a smart default. +/// +/// The auto type has exactly one value: `{auto}`. +/// +/// Parameters that support the `{auto}` value have some smart default or +/// contextual behaviour. A good example is the [text direction]($text.dir) +/// parameter. Setting it to `{auto}` lets Typst automatically determine the +/// direction from the [text language]($text.lang). +#[ty(cast, name = "auto")] +#[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) -> HintedStrResult<Self> { + match value { + Value::Auto => Ok(Self), + _ => Err(Self::error(&value)), + } + } +} + +impl Reflect for AutoValue { + fn input() -> CastInfo { + CastInfo::Type(Type::of::<Self>()) + } + + fn output() -> CastInfo { + CastInfo::Type(Type::of::<Self>()) + } + + fn castable(value: &Value) -> bool { + matches!(value, Value::Auto) + } +} + +impl Debug for AutoValue { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Auto") + } +} + +impl Repr for AutoValue { + fn repr(&self) -> EcoString { + "auto".into() + } +} + +/// 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(_)) + } + + /// Whether this is a `Smart::Custom(x)` and `f(x)` is true. + pub fn is_custom_and<F>(self, f: F) -> bool + where + F: Fn(T) -> bool, + { + match self { + Self::Auto => false, + Self::Custom(x) => f(x), + } + } + + /// Returns a `Smart<&T>` borrowing the inner `T`. + pub fn as_ref(&self) -> Smart<&T> { + match self { + Smart::Auto => Smart::Auto, + Smart::Custom(v) => Smart::Custom(v), + } + } + + /// Returns the contained custom value. + /// + /// If the value is [`Smart::Auto`], returns `None`. + /// + /// Equivalently, this just converts `Smart` to `Option`. + pub fn 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, + } + } + + /// Keeps `self` if it contains a custom value, otherwise returns the + /// output of the given function. + pub fn or_else<F>(self, f: F) -> Self + where + F: FnOnce() -> Self, + { + match self { + Self::Custom(x) => Self::Custom(x), + Self::Auto => f(), + } + } + + /// Returns `Auto` if `self` is `Auto`, otherwise calls the provided + /// function on the contained value and returns the result. + pub fn and_then<F, U>(self, f: F) -> Smart<U> + where + F: FnOnce(T) -> Smart<U>, + { + match self { + Smart::Auto => Smart::Auto, + Smart::Custom(x) => f(x), + } + } + + /// 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, + { + // we want to do this; the Clippy lint is not type-aware + #[allow(clippy::unwrap_or_default)] + self.unwrap_or_else(T::default) + } +} + +impl<T> Smart<Smart<T>> { + /// Removes a single level of nesting, returns `Auto` if the inner or outer value is `Auto`. + pub fn flatten(self) -> Smart<T> { + match self { + Smart::Custom(Smart::Auto) | Smart::Auto => Smart::Auto, + Smart::Custom(Smart::Custom(v)) => Smart::Custom(v), + } + } +} + +impl<T> Default for Smart<T> { + fn default() -> Self { + Self::Auto + } +} + +impl<T: Reflect> Reflect for Smart<T> { + fn input() -> CastInfo { + T::input() + AutoValue::input() + } + + fn output() -> CastInfo { + T::output() + AutoValue::output() + } + + fn castable(value: &Value) -> bool { + AutoValue::castable(value) || T::castable(value) + } +} + +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) -> HintedStrResult<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> Fold for Smart<T> { + fn fold(self, outer: Self) -> Self { + use Smart::Custom; + match (self, outer) { + (Custom(inner), Custom(outer)) => Custom(inner.fold(outer)), + // An explicit `auto` should be respected, thus we don't do + // `inner.or(outer)`. + (inner, _) => inner, + } + } +} diff --git a/crates/typst-library/src/foundations/bool.rs b/crates/typst-library/src/foundations/bool.rs new file mode 100644 index 00000000..e88c8c6f --- /dev/null +++ b/crates/typst-library/src/foundations/bool.rs @@ -0,0 +1,26 @@ +use ecow::EcoString; + +use crate::foundations::{ty, Repr}; + +/// A type with two states. +/// +/// The boolean type has two values: `{true}` and `{false}`. It denotes whether +/// something is active or enabled. +/// +/// # Example +/// ```example +/// #false \ +/// #true \ +/// #(1 < 2) +/// ``` +#[ty(cast, title = "Boolean")] +type bool; + +impl Repr for bool { + fn repr(&self) -> EcoString { + match self { + true => "true".into(), + false => "false".into(), + } + } +} diff --git a/crates/typst-library/src/foundations/bytes.rs b/crates/typst-library/src/foundations/bytes.rs new file mode 100644 index 00000000..05fe4763 --- /dev/null +++ b/crates/typst-library/src/foundations/bytes.rs @@ -0,0 +1,262 @@ +use std::borrow::Cow; +use std::fmt::{self, Debug, Formatter}; +use std::ops::{Add, AddAssign, Deref}; +use std::sync::Arc; + +use ecow::{eco_format, EcoString}; +use serde::{Serialize, Serializer}; +use typst_utils::LazyHash; + +use crate::diag::{bail, StrResult}; +use crate::foundations::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value}; + +/// A sequence of bytes. +/// +/// This is conceptually similar to an array of [integers]($int) between `{0}` +/// and `{255}`, but represented much more efficiently. You can iterate over it +/// using a [for loop]($scripting/#loops). +/// +/// You can convert +/// - a [string]($str) or an [array] of integers to bytes with the [`bytes`] +/// constructor +/// - bytes to a string with the [`str`] constructor, with UTF-8 encoding +/// - bytes to an array of integers with the [`array`] constructor +/// +/// When [reading]($read) data from a file, you can decide whether to load it +/// as a string or as raw bytes. +/// +/// ```example +/// #bytes((123, 160, 22, 0)) \ +/// #bytes("Hello 😃") +/// +/// #let data = read( +/// "rhino.png", +/// encoding: none, +/// ) +/// +/// // Magic bytes. +/// #array(data.slice(0, 4)) \ +/// #str(data.slice(1, 4)) +/// ``` +#[ty(scope, cast)] +#[derive(Clone, Hash, Eq, PartialEq)] +pub struct Bytes(Arc<LazyHash<Cow<'static, [u8]>>>); + +impl Bytes { + /// Create a buffer from a static byte slice. + pub fn from_static(slice: &'static [u8]) -> Self { + Self(Arc::new(LazyHash::new(Cow::Borrowed(slice)))) + } + + /// Return `true` if the length is 0. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// 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() + } + + /// 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())) + } + + /// Resolve an index, if it is within bounds. + /// + /// `index == len` is considered in bounds. + fn locate_opt(&self, index: i64) -> Option<usize> { + let wrapped = + if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) }; + + wrapped + .and_then(|v| usize::try_from(v).ok()) + .filter(|&v| v <= self.0.len()) + } +} + +#[scope] +impl Bytes { + /// Converts a value to bytes. + /// + /// - Strings are encoded in UTF-8. + /// - Arrays of integers between `{0}` and `{255}` are converted directly. The + /// dedicated byte representation is much more efficient than the array + /// representation and thus typically used for large byte buffers (e.g. image + /// data). + /// + /// ```example + /// #bytes("Hello 😃") \ + /// #bytes((123, 160, 22, 0)) + /// ``` + #[func(constructor)] + pub fn construct( + /// The value that should be converted to bytes. + value: ToBytes, + ) -> Bytes { + value.0 + } + + /// The length in bytes. + #[func(title = "Length")] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns the byte at the specified index. Returns the default value if + /// the index is out of bounds or fails with an error if no default value + /// was specified. + #[func] + pub fn at( + &self, + /// The index at which to retrieve the byte. + index: i64, + /// A default value to return if the index is out of bounds. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { + self.locate_opt(index) + .and_then(|i| self.0.get(i).map(|&b| Value::Int(b.into()))) + .or(default) + .ok_or_else(|| out_of_bounds_no_default(index, self.len())) + } + + /// Extracts a subslice of the bytes. Fails with an error if the start or end + /// index is out of bounds. + #[func] + pub fn slice( + &self, + /// The start index (inclusive). + start: i64, + /// The end index (exclusive). If omitted, the whole slice until the end + /// is extracted. + #[default] + end: Option<i64>, + /// The number of items to extract. This is equivalent to passing + /// `start + count` as the `end` position. Mutually exclusive with + /// `end`. + #[named] + count: Option<i64>, + ) -> StrResult<Bytes> { + let mut end = end; + if end.is_none() { + end = count.map(|c: i64| start + c); + } + let start = self.locate(start)?; + let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start); + Ok(self.0[start..end].into()) + } +} + +impl Debug for Bytes { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Bytes({})", self.len()) + } +} + +impl Repr for Bytes { + fn repr(&self) -> EcoString { + eco_format!("bytes({})", self.len()) + } +} + +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 From<&[u8]> for Bytes { + fn from(slice: &[u8]) -> Self { + Self(Arc::new(LazyHash::new(slice.to_vec().into()))) + } +} + +impl From<Vec<u8>> for Bytes { + fn from(vec: Vec<u8>) -> Self { + Self(Arc::new(LazyHash::new(vec.into()))) + } +} + +impl Add for Bytes { + type Output = Self; + + fn add(mut self, rhs: Self) -> Self::Output { + self += rhs; + self + } +} + +impl AddAssign for Bytes { + fn add_assign(&mut self, rhs: Self) { + if rhs.is_empty() { + // Nothing to do + } else if self.is_empty() { + *self = rhs; + } else if Arc::strong_count(&self.0) == 1 && matches!(**self.0, Cow::Owned(_)) { + Arc::make_mut(&mut self.0).to_mut().extend_from_slice(&rhs); + } else { + *self = Self::from([self.as_slice(), rhs.as_slice()].concat()); + } + } +} + +impl Serialize for Bytes { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + if serializer.is_human_readable() { + serializer.serialize_str(&eco_format!("{self:?}")) + } else { + serializer.serialize_bytes(self) + } + } +} + +/// A value that can be cast to bytes. +pub struct ToBytes(Bytes); + +cast! { + ToBytes, + v: Str => Self(v.as_bytes().into()), + v: Array => Self(v.iter() + .map(|item| match item { + Value::Int(byte @ 0..=255) => Ok(*byte as u8), + Value::Int(_) => bail!("number must be between 0 and 255"), + value => Err(<u8 as Reflect>::error(value)), + }) + .collect::<Result<Vec<u8>, _>>()? + .into() + ), + v: Bytes => Self(v), +} + +/// The out of bounds access error message. +#[cold] +fn out_of_bounds(index: i64, len: usize) -> EcoString { + eco_format!("byte index out of bounds (index: {index}, len: {len})") +} + +/// The out of bounds access error message when no default value was given. +#[cold] +fn out_of_bounds_no_default(index: i64, len: usize) -> EcoString { + eco_format!( + "byte index out of bounds (index: {index}, len: {len}) \ + and no default value was specified", + ) +} diff --git a/crates/typst-library/src/foundations/calc.rs b/crates/typst-library/src/foundations/calc.rs new file mode 100644 index 00000000..f12ca74c --- /dev/null +++ b/crates/typst-library/src/foundations/calc.rs @@ -0,0 +1,1211 @@ +//! Calculations and processing of numeric values. + +use std::cmp; +use std::cmp::Ordering; + +use az::SaturatingAs; +use typst_syntax::{Span, Spanned}; +use typst_utils::{round_int_with_precision, round_with_precision}; + +use crate::diag::{bail, At, HintedString, SourceResult, StrResult}; +use crate::foundations::{cast, func, ops, Decimal, IntoValue, Module, Scope, Value}; +use crate::layout::{Angle, Fr, Length, Ratio}; + +/// A module with calculation definitions. +pub fn module() -> Module { + let mut scope = Scope::new(); + scope.define_func::<abs>(); + scope.define_func::<pow>(); + scope.define_func::<exp>(); + scope.define_func::<sqrt>(); + scope.define_func::<root>(); + scope.define_func::<sin>(); + scope.define_func::<cos>(); + scope.define_func::<tan>(); + scope.define_func::<asin>(); + scope.define_func::<acos>(); + scope.define_func::<atan>(); + scope.define_func::<atan2>(); + scope.define_func::<sinh>(); + scope.define_func::<cosh>(); + scope.define_func::<tanh>(); + scope.define_func::<log>(); + scope.define_func::<ln>(); + scope.define_func::<fact>(); + scope.define_func::<perm>(); + scope.define_func::<binom>(); + scope.define_func::<gcd>(); + scope.define_func::<lcm>(); + scope.define_func::<floor>(); + scope.define_func::<ceil>(); + scope.define_func::<trunc>(); + scope.define_func::<fract>(); + scope.define_func::<round>(); + scope.define_func::<clamp>(); + scope.define_func::<min>(); + scope.define_func::<max>(); + scope.define_func::<even>(); + scope.define_func::<odd>(); + scope.define_func::<rem>(); + scope.define_func::<div_euclid>(); + scope.define_func::<rem_euclid>(); + scope.define_func::<quo>(); + scope.define("inf", f64::INFINITY); + scope.define("pi", std::f64::consts::PI); + scope.define("tau", std::f64::consts::TAU); + scope.define("e", std::f64::consts::E); + Module::new("calc", scope) +} + +/// Calculates the absolute value of a numeric value. +/// +/// ```example +/// #calc.abs(-5) \ +/// #calc.abs(5pt - 2cm) \ +/// #calc.abs(2fr) \ +/// #calc.abs(decimal("-342.440")) +/// ``` +#[func(title = "Absolute")] +pub fn abs( + /// The value whose absolute value to calculate. + value: ToAbs, +) -> Value { + value.0 +} + +/// A value of which the absolute value can be taken. +pub struct ToAbs(Value); + +cast! { + ToAbs, + v: i64 => Self(v.abs().into_value()), + v: f64 => Self(v.abs().into_value()), + v: Length => Self(Value::Length(v.try_abs() + .ok_or("cannot take absolute value of this length")?)), + v: Angle => Self(Value::Angle(v.abs())), + v: Ratio => Self(Value::Ratio(v.abs())), + v: Fr => Self(Value::Fraction(v.abs())), + v: Decimal => Self(Value::Decimal(v.abs())) +} + +/// Raises a value to some exponent. +/// +/// ```example +/// #calc.pow(2, 3) \ +/// #calc.pow(decimal("2.5"), 2) +/// ``` +#[func(title = "Power")] +pub fn pow( + /// The callsite span. + span: Span, + /// The base of the power. + /// + /// If this is a [`decimal`], the exponent can only be an [integer]($int). + base: DecNum, + /// The exponent of the power. + exponent: Spanned<Num>, +) -> SourceResult<DecNum> { + match exponent.v { + _ if exponent.v.float() == 0.0 && base.is_zero() => { + bail!(span, "zero to the power of zero is undefined") + } + Num::Int(i) if i32::try_from(i).is_err() => { + bail!(exponent.span, "exponent is too large") + } + Num::Float(f) if !f.is_normal() && f != 0.0 => { + bail!(exponent.span, "exponent may not be infinite, subnormal, or NaN") + } + _ => {} + }; + + match (base, exponent.v) { + (DecNum::Int(a), Num::Int(b)) if b >= 0 => a + .checked_pow(b as u32) + .map(DecNum::Int) + .ok_or_else(too_large) + .at(span), + (DecNum::Decimal(a), Num::Int(b)) => { + a.checked_powi(b).map(DecNum::Decimal).ok_or_else(too_large).at(span) + } + (a, b) => { + let Some(a) = a.float() else { + return Err(cant_apply_to_decimal_and_float()).at(span); + }; + + let result = if a == std::f64::consts::E { + b.float().exp() + } else if a == 2.0 { + b.float().exp2() + } else if let Num::Int(b) = b { + a.powi(b as i32) + } else { + a.powf(b.float()) + }; + + if result.is_nan() { + bail!(span, "the result is not a real number") + } + + Ok(DecNum::Float(result)) + } + } +} + +/// Raises a value to some exponent of e. +/// +/// ```example +/// #calc.exp(1) +/// ``` +#[func(title = "Exponential")] +pub fn exp( + /// The callsite span. + span: Span, + /// The exponent of the power. + exponent: Spanned<Num>, +) -> SourceResult<f64> { + match exponent.v { + Num::Int(i) if i32::try_from(i).is_err() => { + bail!(exponent.span, "exponent is too large") + } + Num::Float(f) if !f.is_normal() && f != 0.0 => { + bail!(exponent.span, "exponent may not be infinite, subnormal, or NaN") + } + _ => {} + } + + let result = exponent.v.float().exp(); + if result.is_nan() { + bail!(span, "the result is not a real number") + } + + Ok(result) +} + +/// Calculates the square root of a number. +/// +/// ```example +/// #calc.sqrt(16) \ +/// #calc.sqrt(2.5) +/// ``` +#[func(title = "Square Root")] +pub fn sqrt( + /// The number whose square root to calculate. Must be non-negative. + value: Spanned<Num>, +) -> SourceResult<f64> { + if value.v.float() < 0.0 { + bail!(value.span, "cannot take square root of negative number"); + } + Ok(value.v.float().sqrt()) +} + +/// Calculates the real nth root of a number. +/// +/// If the number is negative, then n must be odd. +/// +/// ```example +/// #calc.root(16.0, 4) \ +/// #calc.root(27.0, 3) +/// ``` +#[func] +pub fn root( + /// The expression to take the root of + radicand: f64, + /// Which root of the radicand to take + index: Spanned<i64>, +) -> SourceResult<f64> { + if index.v == 0 { + bail!(index.span, "cannot take the 0th root of a number"); + } else if radicand < 0.0 { + if index.v % 2 == 0 { + bail!( + index.span, + "negative numbers do not have a real nth root when n is even" + ); + } else { + Ok(-(-radicand).powf(1.0 / index.v as f64)) + } + } else { + Ok(radicand.powf(1.0 / index.v as f64)) + } +} + +/// Calculates the sine of an angle. +/// +/// When called with an integer or a float, they will be interpreted as +/// radians. +/// +/// ```example +/// #calc.sin(1.5) \ +/// #calc.sin(90deg) +/// ``` +#[func(title = "Sine")] +pub fn sin( + /// The angle whose sine to calculate. + angle: AngleLike, +) -> f64 { + match angle { + AngleLike::Angle(a) => a.sin(), + AngleLike::Int(n) => (n as f64).sin(), + AngleLike::Float(n) => n.sin(), + } +} + +/// Calculates the cosine of an angle. +/// +/// When called with an integer or a float, they will be interpreted as +/// radians. +/// +/// ```example +/// #calc.cos(1.5) \ +/// #calc.cos(90deg) +/// ``` +#[func(title = "Cosine")] +pub fn cos( + /// The angle whose cosine to calculate. + angle: AngleLike, +) -> f64 { + match angle { + AngleLike::Angle(a) => a.cos(), + AngleLike::Int(n) => (n as f64).cos(), + AngleLike::Float(n) => n.cos(), + } +} + +/// Calculates the tangent of an angle. +/// +/// When called with an integer or a float, they will be interpreted as +/// radians. +/// +/// ```example +/// #calc.tan(1.5) \ +/// #calc.tan(90deg) +/// ``` +#[func(title = "Tangent")] +pub fn tan( + /// The angle whose tangent to calculate. + angle: AngleLike, +) -> f64 { + match angle { + AngleLike::Angle(a) => a.tan(), + AngleLike::Int(n) => (n as f64).tan(), + AngleLike::Float(n) => n.tan(), + } +} + +/// Calculates the arcsine of a number. +/// +/// ```example +/// #calc.asin(0) \ +/// #calc.asin(1) +/// ``` +#[func(title = "Arcsine")] +pub fn asin( + /// The number whose arcsine to calculate. Must be between -1 and 1. + value: Spanned<Num>, +) -> SourceResult<Angle> { + let val = value.v.float(); + if val < -1.0 || val > 1.0 { + bail!(value.span, "value must be between -1 and 1"); + } + Ok(Angle::rad(val.asin())) +} + +/// Calculates the arccosine of a number. +/// +/// ```example +/// #calc.acos(0) \ +/// #calc.acos(1) +/// ``` +#[func(title = "Arccosine")] +pub fn acos( + /// The number whose arcsine to calculate. Must be between -1 and 1. + value: Spanned<Num>, +) -> SourceResult<Angle> { + let val = value.v.float(); + if val < -1.0 || val > 1.0 { + bail!(value.span, "value must be between -1 and 1"); + } + Ok(Angle::rad(val.acos())) +} + +/// Calculates the arctangent of a number. +/// +/// ```example +/// #calc.atan(0) \ +/// #calc.atan(1) +/// ``` +#[func(title = "Arctangent")] +pub fn atan( + /// The number whose arctangent to calculate. + value: Num, +) -> Angle { + Angle::rad(value.float().atan()) +} + +/// Calculates the four-quadrant arctangent of a coordinate. +/// +/// The arguments are `(x, y)`, not `(y, x)`. +/// +/// ```example +/// #calc.atan2(1, 1) \ +/// #calc.atan2(-2, -3) +/// ``` +#[func(title = "Four-quadrant Arctangent")] +pub fn atan2( + /// The X coordinate. + x: Num, + /// The Y coordinate. + y: Num, +) -> Angle { + Angle::rad(f64::atan2(y.float(), x.float())) +} + +/// Calculates the hyperbolic sine of a hyperbolic angle. +/// +/// ```example +/// #calc.sinh(0) \ +/// #calc.sinh(1.5) +/// ``` +#[func(title = "Hyperbolic Sine")] +pub fn sinh( + /// The hyperbolic angle whose hyperbolic sine to calculate. + value: f64, +) -> f64 { + value.sinh() +} + +/// Calculates the hyperbolic cosine of a hyperbolic angle. +/// +/// ```example +/// #calc.cosh(0) \ +/// #calc.cosh(1.5) +/// ``` +#[func(title = "Hyperbolic Cosine")] +pub fn cosh( + /// The hyperbolic angle whose hyperbolic cosine to calculate. + value: f64, +) -> f64 { + value.cosh() +} + +/// Calculates the hyperbolic tangent of an hyperbolic angle. +/// +/// ```example +/// #calc.tanh(0) \ +/// #calc.tanh(1.5) +/// ``` +#[func(title = "Hyperbolic Tangent")] +pub fn tanh( + /// The hyperbolic angle whose hyperbolic tangent to calculate. + value: f64, +) -> f64 { + value.tanh() +} + +/// Calculates the logarithm of a number. +/// +/// If the base is not specified, the logarithm is calculated in base 10. +/// +/// ```example +/// #calc.log(100) +/// ``` +#[func(title = "Logarithm")] +pub fn log( + /// The callsite span. + span: Span, + /// The number whose logarithm to calculate. Must be strictly positive. + value: Spanned<Num>, + /// The base of the logarithm. May not be zero. + #[named] + #[default(Spanned::new(10.0, Span::detached()))] + base: Spanned<f64>, +) -> SourceResult<f64> { + let number = value.v.float(); + if number <= 0.0 { + bail!(value.span, "value must be strictly positive") + } + + if !base.v.is_normal() { + bail!(base.span, "base may not be zero, NaN, infinite, or subnormal") + } + + let result = if base.v == std::f64::consts::E { + number.ln() + } else if base.v == 2.0 { + number.log2() + } else if base.v == 10.0 { + number.log10() + } else { + number.log(base.v) + }; + + if result.is_infinite() || result.is_nan() { + bail!(span, "the result is not a real number") + } + + Ok(result) +} + +/// Calculates the natural logarithm of a number. +/// +/// ```example +/// #calc.ln(calc.e) +/// ``` +#[func(title = "Natural Logarithm")] +pub fn ln( + /// The callsite span. + span: Span, + /// The number whose logarithm to calculate. Must be strictly positive. + value: Spanned<Num>, +) -> SourceResult<f64> { + let number = value.v.float(); + if number <= 0.0 { + bail!(value.span, "value must be strictly positive") + } + + let result = number.ln(); + if result.is_infinite() { + bail!(span, "result close to -inf") + } + + Ok(result) +} + +/// Calculates the factorial of a number. +/// +/// ```example +/// #calc.fact(5) +/// ``` +#[func(title = "Factorial")] +pub fn fact( + /// The number whose factorial to calculate. Must be non-negative. + number: u64, +) -> StrResult<i64> { + Ok(fact_impl(1, number).ok_or_else(too_large)?) +} + +/// Calculates a permutation. +/// +/// Returns the `k`-permutation of `n`, or the number of ways to choose `k` +/// items from a set of `n` with regard to order. +/// +/// ```example +/// $ "perm"(n, k) &= n!/((n - k)!) \ +/// "perm"(5, 3) &= #calc.perm(5, 3) $ +/// ``` +#[func(title = "Permutation")] +pub fn perm( + /// The base number. Must be non-negative. + base: u64, + /// The number of permutations. Must be non-negative. + numbers: u64, +) -> StrResult<i64> { + // By convention. + if base < numbers { + return Ok(0); + } + + Ok(fact_impl(base - numbers + 1, base).ok_or_else(too_large)?) +} + +/// Calculates the product of a range of numbers. Used to calculate +/// permutations. Returns None if the result is larger than `i64::MAX` +fn fact_impl(start: u64, end: u64) -> Option<i64> { + // By convention + if end + 1 < start { + return Some(0); + } + + let real_start: u64 = cmp::max(1, start); + let mut count: u64 = 1; + for i in real_start..=end { + count = count.checked_mul(i)?; + } + + count.try_into().ok() +} + +/// Calculates a binomial coefficient. +/// +/// Returns the `k`-combination of `n`, or the number of ways to choose `k` +/// items from a set of `n` without regard to order. +/// +/// ```example +/// #calc.binom(10, 5) +/// ``` +#[func(title = "Binomial")] +pub fn binom( + /// The upper coefficient. Must be non-negative. + n: u64, + /// The lower coefficient. Must be non-negative. + k: u64, +) -> StrResult<i64> { + Ok(binom_impl(n, k).ok_or_else(too_large)?) +} + +/// Calculates a binomial coefficient, with `n` the upper coefficient and `k` +/// the lower coefficient. Returns `None` if the result is larger than +/// `i64::MAX` +fn binom_impl(n: u64, k: u64) -> Option<i64> { + if k > n { + return Some(0); + } + + // By symmetry + let real_k = cmp::min(n - k, k); + if real_k == 0 { + return Some(1); + } + + let mut result: u64 = 1; + for i in 0..real_k { + result = result.checked_mul(n - i)?.checked_div(i + 1)?; + } + + result.try_into().ok() +} + +/// Calculates the greatest common divisor of two integers. +/// +/// ```example +/// #calc.gcd(7, 42) +/// ``` +#[func(title = "Greatest Common Divisor")] +pub fn gcd( + /// The first integer. + a: i64, + /// The second integer. + b: i64, +) -> i64 { + let (mut a, mut b) = (a, b); + while b != 0 { + let temp = b; + b = a % b; + a = temp; + } + + a.abs() +} + +/// Calculates the least common multiple of two integers. +/// +/// ```example +/// #calc.lcm(96, 13) +/// ``` +#[func(title = "Least Common Multiple")] +pub fn lcm( + /// The first integer. + a: i64, + /// The second integer. + b: i64, +) -> StrResult<i64> { + if a == b { + return Ok(a.abs()); + } + + Ok(a.checked_div(gcd(a, b)) + .and_then(|gcd| gcd.checked_mul(b)) + .map(|v| v.abs()) + .ok_or_else(too_large)?) +} + +/// Rounds a number down to the nearest integer. +/// +/// If the number is already an integer, it is returned unchanged. +/// +/// Note that this function will always return an [integer]($int), and will +/// error if the resulting [`float`] or [`decimal`] is larger than the maximum +/// 64-bit signed integer or smaller than the minimum for that type. +/// +/// ```example +/// #calc.floor(500.1) +/// #assert(calc.floor(3) == 3) +/// #assert(calc.floor(3.14) == 3) +/// #assert(calc.floor(decimal("-3.14")) == -4) +/// ``` +#[func] +pub fn floor( + /// The number to round down. + value: DecNum, +) -> StrResult<i64> { + match value { + DecNum::Int(n) => Ok(n), + DecNum::Float(n) => Ok(crate::foundations::convert_float_to_int(n.floor()) + .map_err(|_| too_large())?), + DecNum::Decimal(n) => Ok(i64::try_from(n.floor()).map_err(|_| too_large())?), + } +} + +/// Rounds a number up to the nearest integer. +/// +/// If the number is already an integer, it is returned unchanged. +/// +/// Note that this function will always return an [integer]($int), and will +/// error if the resulting [`float`] or [`decimal`] is larger than the maximum +/// 64-bit signed integer or smaller than the minimum for that type. +/// +/// ```example +/// #calc.ceil(500.1) +/// #assert(calc.ceil(3) == 3) +/// #assert(calc.ceil(3.14) == 4) +/// #assert(calc.ceil(decimal("-3.14")) == -3) +/// ``` +#[func] +pub fn ceil( + /// The number to round up. + value: DecNum, +) -> StrResult<i64> { + match value { + DecNum::Int(n) => Ok(n), + DecNum::Float(n) => Ok(crate::foundations::convert_float_to_int(n.ceil()) + .map_err(|_| too_large())?), + DecNum::Decimal(n) => Ok(i64::try_from(n.ceil()).map_err(|_| too_large())?), + } +} + +/// Returns the integer part of a number. +/// +/// If the number is already an integer, it is returned unchanged. +/// +/// Note that this function will always return an [integer]($int), and will +/// error if the resulting [`float`] or [`decimal`] is larger than the maximum +/// 64-bit signed integer or smaller than the minimum for that type. +/// +/// ```example +/// #calc.trunc(15.9) +/// #assert(calc.trunc(3) == 3) +/// #assert(calc.trunc(-3.7) == -3) +/// #assert(calc.trunc(decimal("8493.12949582390")) == 8493) +/// ``` +#[func(title = "Truncate")] +pub fn trunc( + /// The number to truncate. + value: DecNum, +) -> StrResult<i64> { + match value { + DecNum::Int(n) => Ok(n), + DecNum::Float(n) => Ok(crate::foundations::convert_float_to_int(n.trunc()) + .map_err(|_| too_large())?), + DecNum::Decimal(n) => Ok(i64::try_from(n.trunc()).map_err(|_| too_large())?), + } +} + +/// Returns the fractional part of a number. +/// +/// If the number is an integer, returns `0`. +/// +/// ```example +/// #calc.fract(-3.1) +/// #assert(calc.fract(3) == 0) +/// #assert(calc.fract(decimal("234.23949211")) == decimal("0.23949211")) +/// ``` +#[func(title = "Fractional")] +pub fn fract( + /// The number to truncate. + value: DecNum, +) -> DecNum { + match value { + DecNum::Int(_) => DecNum::Int(0), + DecNum::Float(n) => DecNum::Float(n.fract()), + DecNum::Decimal(n) => DecNum::Decimal(n.fract()), + } +} + +/// Rounds a number to the nearest integer away from zero. +/// +/// Optionally, a number of decimal places can be specified. +/// +/// If the number of digits is negative, its absolute value will indicate the +/// amount of significant integer digits to remove before the decimal point. +/// +/// Note that this function will return the same type as the operand. That is, +/// applying `round` to a [`float`] will return a `float`, and to a [`decimal`], +/// another `decimal`. You may explicitly convert the output of this function to +/// an integer with [`int`], but note that such a conversion will error if the +/// `float` or `decimal` is larger than the maximum 64-bit signed integer or +/// smaller than the minimum integer. +/// +/// In addition, this function can error if there is an attempt to round beyond +/// the maximum or minimum integer or `decimal`. If the number is a `float`, +/// such an attempt will cause `{float.inf}` or `{-float.inf}` to be returned +/// for maximum and minimum respectively. +/// +/// ```example +/// #calc.round(3.1415, digits: 2) +/// #assert(calc.round(3) == 3) +/// #assert(calc.round(3.14) == 3) +/// #assert(calc.round(3.5) == 4.0) +/// #assert(calc.round(3333.45, digits: -2) == 3300.0) +/// #assert(calc.round(-48953.45, digits: -3) == -49000.0) +/// #assert(calc.round(3333, digits: -2) == 3300) +/// #assert(calc.round(-48953, digits: -3) == -49000) +/// #assert(calc.round(decimal("-6.5")) == decimal("-7")) +/// #assert(calc.round(decimal("7.123456789"), digits: 6) == decimal("7.123457")) +/// #assert(calc.round(decimal("3333.45"), digits: -2) == decimal("3300")) +/// #assert(calc.round(decimal("-48953.45"), digits: -3) == decimal("-49000")) +/// ``` +#[func] +pub fn round( + /// The number to round. + value: DecNum, + /// If positive, the number of decimal places. + /// + /// If negative, the number of significant integer digits that should be + /// removed before the decimal point. + #[named] + #[default(0)] + digits: i64, +) -> StrResult<DecNum> { + match value { + DecNum::Int(n) => Ok(DecNum::Int( + round_int_with_precision(n, digits.saturating_as::<i16>()) + .ok_or_else(too_large)?, + )), + DecNum::Float(n) => { + Ok(DecNum::Float(round_with_precision(n, digits.saturating_as::<i16>()))) + } + DecNum::Decimal(n) => Ok(DecNum::Decimal( + n.round(digits.saturating_as::<i32>()).ok_or_else(too_large)?, + )), + } +} + +/// Clamps a number between a minimum and maximum value. +/// +/// ```example +/// #calc.clamp(5, 0, 4) +/// #assert(calc.clamp(5, 0, 10) == 5) +/// #assert(calc.clamp(5, 6, 10) == 6) +/// #assert(calc.clamp(decimal("5.45"), 2, decimal("45.9")) == decimal("5.45")) +/// #assert(calc.clamp(decimal("5.45"), decimal("6.75"), 12) == decimal("6.75")) +/// ``` +#[func] +pub fn clamp( + /// The callsite span. + span: Span, + /// The number to clamp. + value: DecNum, + /// The inclusive minimum value. + min: DecNum, + /// The inclusive maximum value. + max: Spanned<DecNum>, +) -> SourceResult<DecNum> { + // Ignore if there are incompatible types (decimal and float) since that + // will cause `apply3` below to error before calling clamp, avoiding a + // panic. + if min + .apply2(max.v, |min, max| max < min, |min, max| max < min, |min, max| max < min) + .unwrap_or(false) + { + bail!(max.span, "max must be greater than or equal to min") + } + + value + .apply3(min, max.v, i64::clamp, f64::clamp, Decimal::clamp) + .ok_or_else(cant_apply_to_decimal_and_float) + .at(span) +} + +/// Determines the minimum of a sequence of values. +/// +/// ```example +/// #calc.min(1, -3, -5, 20, 3, 6) \ +/// #calc.min("typst", "is", "cool") +/// ``` +#[func(title = "Minimum")] +pub fn min( + /// The callsite span. + span: Span, + /// The sequence of values from which to extract the minimum. + /// Must not be empty. + #[variadic] + values: Vec<Spanned<Value>>, +) -> SourceResult<Value> { + minmax(span, values, Ordering::Less) +} + +/// Determines the maximum of a sequence of values. +/// +/// ```example +/// #calc.max(1, -3, -5, 20, 3, 6) \ +/// #calc.max("typst", "is", "cool") +/// ``` +#[func(title = "Maximum")] +pub fn max( + /// The callsite span. + span: Span, + /// The sequence of values from which to extract the maximum. + /// Must not be empty. + #[variadic] + values: Vec<Spanned<Value>>, +) -> SourceResult<Value> { + minmax(span, values, Ordering::Greater) +} + +/// Find the minimum or maximum of a sequence of values. +fn minmax( + span: Span, + values: Vec<Spanned<Value>>, + goal: Ordering, +) -> SourceResult<Value> { + let mut iter = values.into_iter(); + let Some(Spanned { v: mut extremum, .. }) = iter.next() else { + bail!(span, "expected at least one value"); + }; + + for Spanned { v, span } in iter { + let ordering = ops::compare(&v, &extremum).at(span)?; + if ordering == goal { + extremum = v; + } + } + + Ok(extremum) +} + +/// Determines whether an integer is even. +/// +/// ```example +/// #calc.even(4) \ +/// #calc.even(5) \ +/// #range(10).filter(calc.even) +/// ``` +#[func] +pub fn even( + /// The number to check for evenness. + value: i64, +) -> bool { + value % 2 == 0 +} + +/// Determines whether an integer is odd. +/// +/// ```example +/// #calc.odd(4) \ +/// #calc.odd(5) \ +/// #range(10).filter(calc.odd) +/// ``` +#[func] +pub fn odd( + /// The number to check for oddness. + value: i64, +) -> bool { + value % 2 != 0 +} + +/// Calculates the remainder of two numbers. +/// +/// The value `calc.rem(x, y)` always has the same sign as `x`, and is smaller +/// in magnitude than `y`. +/// +/// This can error if given a [`decimal`] input and the dividend is too small in +/// magnitude compared to the divisor. +/// +/// ```example +/// #calc.rem(7, 3) \ +/// #calc.rem(7, -3) \ +/// #calc.rem(-7, 3) \ +/// #calc.rem(-7, -3) \ +/// #calc.rem(1.75, 0.5) +/// ``` +#[func(title = "Remainder")] +pub fn rem( + /// The span of the function call. + span: Span, + /// The dividend of the remainder. + dividend: DecNum, + /// The divisor of the remainder. + divisor: Spanned<DecNum>, +) -> SourceResult<DecNum> { + if divisor.v.is_zero() { + bail!(divisor.span, "divisor must not be zero"); + } + + dividend + .apply2( + divisor.v, + |a, b| Some(DecNum::Int(a % b)), + |a, b| Some(DecNum::Float(a % b)), + |a, b| a.checked_rem(b).map(DecNum::Decimal), + ) + .ok_or_else(cant_apply_to_decimal_and_float) + .at(span)? + .ok_or("dividend too small compared to divisor") + .at(span) +} + +/// Performs euclidean division of two numbers. +/// +/// The result of this computation is that of a division rounded to the integer +/// `{n}` such that the dividend is greater than or equal to `{n}` times the divisor. +/// +/// ```example +/// #calc.div-euclid(7, 3) \ +/// #calc.div-euclid(7, -3) \ +/// #calc.div-euclid(-7, 3) \ +/// #calc.div-euclid(-7, -3) \ +/// #calc.div-euclid(1.75, 0.5) \ +/// #calc.div-euclid(decimal("1.75"), decimal("0.5")) +/// ``` +#[func(title = "Euclidean Division")] +pub fn div_euclid( + /// The callsite span. + span: Span, + /// The dividend of the division. + dividend: DecNum, + /// The divisor of the division. + divisor: Spanned<DecNum>, +) -> SourceResult<DecNum> { + if divisor.v.is_zero() { + bail!(divisor.span, "divisor must not be zero"); + } + + dividend + .apply2( + divisor.v, + |a, b| Some(DecNum::Int(a.div_euclid(b))), + |a, b| Some(DecNum::Float(a.div_euclid(b))), + |a, b| a.checked_div_euclid(b).map(DecNum::Decimal), + ) + .ok_or_else(cant_apply_to_decimal_and_float) + .at(span)? + .ok_or_else(too_large) + .at(span) +} + +/// This calculates the least nonnegative remainder of a division. +/// +/// Warning: Due to a floating point round-off error, the remainder may equal +/// the absolute value of the divisor if the dividend is much smaller in +/// magnitude than the divisor and the dividend is negative. This only applies +/// for floating point inputs. +/// +/// In addition, this can error if given a [`decimal`] input and the dividend is +/// too small in magnitude compared to the divisor. +/// +/// ```example +/// #calc.rem-euclid(7, 3) \ +/// #calc.rem-euclid(7, -3) \ +/// #calc.rem-euclid(-7, 3) \ +/// #calc.rem-euclid(-7, -3) \ +/// #calc.rem-euclid(1.75, 0.5) \ +/// #calc.rem-euclid(decimal("1.75"), decimal("0.5")) +/// ``` +#[func(title = "Euclidean Remainder")] +pub fn rem_euclid( + /// The callsite span. + span: Span, + /// The dividend of the remainder. + dividend: DecNum, + /// The divisor of the remainder. + divisor: Spanned<DecNum>, +) -> SourceResult<DecNum> { + if divisor.v.is_zero() { + bail!(divisor.span, "divisor must not be zero"); + } + + dividend + .apply2( + divisor.v, + |a, b| Some(DecNum::Int(a.rem_euclid(b))), + |a, b| Some(DecNum::Float(a.rem_euclid(b))), + |a, b| a.checked_rem_euclid(b).map(DecNum::Decimal), + ) + .ok_or_else(cant_apply_to_decimal_and_float) + .at(span)? + .ok_or("dividend too small compared to divisor") + .at(span) +} + +/// Calculates the quotient (floored division) of two numbers. +/// +/// Note that this function will always return an [integer]($int), and will +/// error if the resulting [`float`] or [`decimal`] is larger than the maximum +/// 64-bit signed integer or smaller than the minimum for that type. +/// +/// ```example +/// $ "quo"(a, b) &= floor(a/b) \ +/// "quo"(14, 5) &= #calc.quo(14, 5) \ +/// "quo"(3.46, 0.5) &= #calc.quo(3.46, 0.5) $ +/// ``` +#[func(title = "Quotient")] +pub fn quo( + /// The span of the function call. + span: Span, + /// The dividend of the quotient. + dividend: DecNum, + /// The divisor of the quotient. + divisor: Spanned<DecNum>, +) -> SourceResult<i64> { + if divisor.v.is_zero() { + bail!(divisor.span, "divisor must not be zero"); + } + + let divided = dividend + .apply2( + divisor.v, + |a, b| Some(DecNum::Int(a / b)), + |a, b| Some(DecNum::Float(a / b)), + |a, b| a.checked_div(b).map(DecNum::Decimal), + ) + .ok_or_else(cant_apply_to_decimal_and_float) + .at(span)? + .ok_or_else(too_large) + .at(span)?; + + floor(divided).at(span) +} + +/// A value which can be passed to functions that work with integers and floats. +#[derive(Debug, Copy, Clone)] +pub enum Num { + Int(i64), + Float(f64), +} + +impl Num { + fn float(self) -> f64 { + match self { + Self::Int(v) => v as f64, + Self::Float(v) => v, + } + } +} + +cast! { + Num, + self => match self { + Self::Int(v) => v.into_value(), + Self::Float(v) => v.into_value(), + }, + v: i64 => Self::Int(v), + v: f64 => Self::Float(v), +} + +/// A value which can be passed to functions that work with integers, floats, +/// and decimals. +#[derive(Debug, Copy, Clone)] +pub enum DecNum { + Int(i64), + Float(f64), + Decimal(Decimal), +} + +impl DecNum { + /// Checks if this number is equivalent to zero. + fn is_zero(self) -> bool { + match self { + Self::Int(i) => i == 0, + Self::Float(f) => f == 0.0, + Self::Decimal(d) => d.is_zero(), + } + } + + /// If this `DecNum` holds an integer or float, returns a float. + /// Otherwise, returns `None`. + fn float(self) -> Option<f64> { + match self { + Self::Int(i) => Some(i as f64), + Self::Float(f) => Some(f), + Self::Decimal(_) => None, + } + } + + /// If this `DecNum` holds an integer or decimal, returns a decimal. + /// Otherwise, returns `None`. + fn decimal(self) -> Option<Decimal> { + match self { + Self::Int(i) => Some(Decimal::from(i)), + Self::Float(_) => None, + Self::Decimal(d) => Some(d), + } + } + + /// Tries to apply a function to two decimal or numeric arguments. + /// + /// Fails with `None` if one is a float and the other is a decimal. + fn apply2<T>( + self, + other: Self, + int: impl FnOnce(i64, i64) -> T, + float: impl FnOnce(f64, f64) -> T, + decimal: impl FnOnce(Decimal, Decimal) -> T, + ) -> Option<T> { + match (self, other) { + (Self::Int(a), Self::Int(b)) => Some(int(a, b)), + (Self::Decimal(a), Self::Decimal(b)) => Some(decimal(a, b)), + (Self::Decimal(a), Self::Int(b)) => Some(decimal(a, Decimal::from(b))), + (Self::Int(a), Self::Decimal(b)) => Some(decimal(Decimal::from(a), b)), + (a, b) => Some(float(a.float()?, b.float()?)), + } + } + + /// Tries to apply a function to three decimal or numeric arguments. + /// + /// Fails with `None` if one is a float and the other is a decimal. + fn apply3( + self, + other: Self, + third: Self, + int: impl FnOnce(i64, i64, i64) -> i64, + float: impl FnOnce(f64, f64, f64) -> f64, + decimal: impl FnOnce(Decimal, Decimal, Decimal) -> Decimal, + ) -> Option<Self> { + match (self, other, third) { + (Self::Int(a), Self::Int(b), Self::Int(c)) => Some(Self::Int(int(a, b, c))), + (Self::Decimal(a), b, c) => { + Some(Self::Decimal(decimal(a, b.decimal()?, c.decimal()?))) + } + (a, Self::Decimal(b), c) => { + Some(Self::Decimal(decimal(a.decimal()?, b, c.decimal()?))) + } + (a, b, Self::Decimal(c)) => { + Some(Self::Decimal(decimal(a.decimal()?, b.decimal()?, c))) + } + (a, b, c) => Some(Self::Float(float(a.float()?, b.float()?, c.float()?))), + } + } +} + +cast! { + DecNum, + self => match self { + Self::Int(v) => v.into_value(), + Self::Float(v) => v.into_value(), + Self::Decimal(v) => v.into_value(), + }, + v: i64 => Self::Int(v), + v: f64 => Self::Float(v), + v: Decimal => Self::Decimal(v), +} + +/// A value that can be passed to a trigonometric function. +pub enum AngleLike { + Int(i64), + Float(f64), + Angle(Angle), +} + +cast! { + AngleLike, + v: i64 => Self::Int(v), + v: f64 => Self::Float(v), + v: Angle => Self::Angle(v), +} + +/// The error message when the result is too large to be represented. +#[cold] +fn too_large() -> &'static str { + "the result is too large" +} + +/// The hinted error message when trying to apply an operation to decimal and +/// float operands. +#[cold] +fn cant_apply_to_decimal_and_float() -> HintedString { + HintedString::new("cannot apply this operation to a decimal and a float".into()) + .with_hint( + "if loss of precision is acceptable, explicitly cast the \ + decimal to a float with `float(value)`", + ) +} diff --git a/crates/typst-library/src/foundations/cast.rs b/crates/typst-library/src/foundations/cast.rs new file mode 100644 index 00000000..84f38f36 --- /dev/null +++ b/crates/typst-library/src/foundations/cast.rs @@ -0,0 +1,499 @@ +#[rustfmt::skip] +#[doc(inline)] +pub use typst_macros::{cast, Cast}; + +use std::borrow::Cow; +use std::fmt::Write; +use std::hash::Hash; +use std::ops::Add; + +use ecow::eco_format; +use smallvec::SmallVec; +use typst_syntax::{Span, Spanned}; +use unicode_math_class::MathClass; + +use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult}; +use crate::foundations::{array, repr, NativeElement, Packed, Repr, Str, Type, Value}; + +/// 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 what can be cast into this value. + fn input() -> CastInfo; + + /// Describe what this value can be cast into. + fn output() -> 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 unacceptable value type. + /// + /// ```ignore + /// assert_eq!( + /// <i64 as Reflect>::error(&Value::None), + /// "expected integer, found none", + /// ); + /// ``` + fn error(found: &Value) -> HintedString { + Self::input().error(found) + } +} + +impl Reflect for Value { + fn input() -> CastInfo { + CastInfo::Any + } + + fn output() -> CastInfo { + CastInfo::Any + } + + fn castable(_: &Value) -> bool { + true + } +} + +impl<T: Reflect> Reflect for Spanned<T> { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl<T: NativeElement + Reflect> Reflect for Packed<T> { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl<T: Reflect> Reflect for StrResult<T> { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl<T: Reflect> Reflect for HintedStrResult<T> { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl<T: Reflect> Reflect for SourceResult<T> { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl<T: Reflect> Reflect for &T { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl<T: Reflect> Reflect for &mut T { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +/// Cast a Rust type into a Typst [`Value`]. +/// +/// See also: [`Reflect`]. +pub trait IntoValue { + /// Cast this type into a value. + fn into_value(self) -> Value; +} + +impl IntoValue for Value { + fn into_value(self) -> Value { + self + } +} + +impl IntoValue for (&Str, &Value) { + fn into_value(self) -> Value { + Value::Array(array![self.0.clone(), self.1.clone()]) + } +} + +impl<T: IntoValue + Clone> IntoValue for Cow<'_, T> { + fn into_value(self) -> Value { + self.into_owned().into_value() + } +} + +impl<T: NativeElement + IntoValue> IntoValue for Packed<T> { + fn into_value(self) -> Value { + Value::Content(self.pack()) + } +} + +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 HintedStrResult<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) + } +} + +impl<T: IntoValue> IntoValue for fn() -> T { + fn into_value(self) -> Value { + self().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) -> HintedStrResult<Self>; +} + +impl FromValue for Value { + fn from_value(value: Value) -> HintedStrResult<Self> { + Ok(value) + } +} + +impl<T: NativeElement + FromValue> FromValue for Packed<T> { + fn from_value(mut value: Value) -> HintedStrResult<Self> { + if let Value::Content(content) = value { + match content.into_packed::<T>() { + Ok(packed) => return Ok(packed), + Err(content) => value = Value::Content(content), + } + } + let val = T::from_value(value)?; + Ok(Packed::new(val)) + } +} + +impl<T: FromValue> FromValue<Spanned<Value>> for T { + fn from_value(value: Spanned<Value>) -> HintedStrResult<Self> { + T::from_value(value.v) + } +} + +impl<T: FromValue> FromValue<Spanned<Value>> for Spanned<T> { + fn from_value(value: Spanned<Value>) -> HintedStrResult<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, PartialEq, Hash, 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(Type), + /// 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) -> HintedString { + let mut matching_type = false; + let mut parts = vec![]; + + self.walk(|info| match info { + CastInfo::Any => parts.push("anything".into()), + CastInfo::Value(value, _) => { + parts.push(value.repr()); + if value.ty() == found.ty() { + matching_type = true; + } + } + CastInfo::Type(ty) => parts.push(eco_format!("{ty}")), + CastInfo::Union(_) => {} + }); + + let mut msg = String::from("expected "); + if parts.is_empty() { + msg.push_str(" nothing"); + } + + msg.push_str(&repr::separated_list(&parts, "or")); + + if !matching_type { + msg.push_str(", found "); + write!(msg, "{}", found.ty()).unwrap(); + } + + let mut msg: HintedString = msg.into(); + + if let Value::Int(i) = found { + if !matching_type && parts.iter().any(|p| p == "length") { + msg.hint(eco_format!("a length needs a unit - did you mean {i}pt?")); + } + } else if let Value::Str(s) = found { + if !matching_type && parts.iter().any(|p| p == "label") { + if typst_syntax::is_valid_label_literal_id(s) { + msg.hint(eco_format!( + "use `<{s}>` or `label({})` to create a label", + s.repr() + )); + } else { + msg.hint(eco_format!("use `label({})` to create a label", s.repr())); + } + } + } else if let Value::Decimal(_) = found { + if !matching_type && parts.iter().any(|p| p == "float") { + msg.hint(eco_format!( + "if loss of precision is acceptable, explicitly cast the \ + decimal to a float with `float(value)`" + )); + } + } + + msg + } + + /// Walk all contained non-union infos. + pub fn walk<F>(&self, mut f: F) + where + F: FnMut(&Self), + { + fn inner<F>(info: &CastInfo, f: &mut F) + where + F: FnMut(&CastInfo), + { + if let CastInfo::Union(infos) = info { + for child in infos { + inner(child, f); + } + } else { + f(info); + } + } + + inner(self, &mut f) + } +} + +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 an argument. +pub trait Container { + /// The contained type. + type Inner; +} + +impl<T> Container for Option<T> { + type Inner = T; +} + +impl<T> Container for Vec<T> { + type Inner = T; +} + +impl<T, const N: usize> Container for SmallVec<[T; N]> { + type Inner = T; +} + +/// An uninhabitable type. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum Never {} + +impl Reflect for Never { + fn input() -> CastInfo { + CastInfo::Union(vec![]) + } + + fn output() -> 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) -> HintedStrResult<Self> { + Err(Self::error(&value)) + } +} + +cast! { + MathClass, + self => IntoValue::into_value(match self { + MathClass::Normal => "normal", + MathClass::Alphabetic => "alphabetic", + MathClass::Binary => "binary", + MathClass::Closing => "closing", + MathClass::Diacritic => "diacritic", + MathClass::Fence => "fence", + MathClass::GlyphPart => "glyph-part", + MathClass::Large => "large", + MathClass::Opening => "opening", + MathClass::Punctuation => "punctuation", + MathClass::Relation => "relation", + MathClass::Space => "space", + MathClass::Unary => "unary", + MathClass::Vary => "vary", + MathClass::Special => "special", + }), + /// The default class for non-special things. + "normal" => MathClass::Normal, + /// Punctuation, e.g. a comma. + "punctuation" => MathClass::Punctuation, + /// An opening delimiter, e.g. `(`. + "opening" => MathClass::Opening, + /// A closing delimiter, e.g. `)`. + "closing" => MathClass::Closing, + /// A delimiter that is the same on both sides, e.g. `|`. + "fence" => MathClass::Fence, + /// A large operator like `sum`. + "large" => MathClass::Large, + /// A relation like `=` or `prec`. + "relation" => MathClass::Relation, + /// A unary operator like `not`. + "unary" => MathClass::Unary, + /// A binary operator like `times`. + "binary" => MathClass::Binary, + /// An operator that can be both unary or binary like `+`. + "vary" => MathClass::Vary, +} diff --git a/crates/typst-library/src/foundations/content.rs b/crates/typst-library/src/foundations/content.rs new file mode 100644 index 00000000..a274b8bf --- /dev/null +++ b/crates/typst-library/src/foundations/content.rs @@ -0,0 +1,1007 @@ +use std::any::TypeId; +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::iter::{self, Sum}; +use std::marker::PhantomData; +use std::ops::{Add, AddAssign, Deref, DerefMut}; +use std::sync::Arc; + +use comemo::Tracked; +use ecow::{eco_format, EcoString}; +use serde::{Serialize, Serializer}; +use smallvec::smallvec; +use typst_syntax::Span; +use typst_utils::{fat, singleton, LazyHash, SmallBitSet}; + +use crate::diag::{SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + elem, func, scope, ty, Context, Dict, Element, Fields, IntoValue, Label, + NativeElement, Recipe, RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, + Value, +}; +use crate::introspection::Location; +use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides}; +use crate::model::{Destination, EmphElem, LinkElem, StrongElem}; +use crate::text::UnderlineElem; + +/// A piece of document content. +/// +/// This type is at the heart of Typst. All markup you write and most +/// [functions]($function) you call produce content values. You can create a +/// content value by enclosing markup in square brackets. This is also how you +/// pass content to functions. +/// +/// # Example +/// ```example +/// Type of *Hello!* is +/// #type([*Hello!*]) +/// ``` +/// +/// Content can be added with the `+` operator, +/// [joined together]($scripting/#blocks) and multiplied with integers. Wherever +/// content is expected, you can also pass a [string]($str) or `{none}`. +/// +/// # Representation +/// Content consists of elements with fields. When constructing an element with +/// its _element function,_ you provide these fields as arguments and when you +/// have a content value, you can access its fields with [field access +/// syntax]($scripting/#field-access). +/// +/// Some fields are required: These must be provided when constructing an +/// element and as a consequence, they are always available through field access +/// on content of that type. Required fields are marked as such in the +/// documentation. +/// +/// Most fields are optional: Like required fields, they can be passed to the +/// element function to configure them for a single element. However, these can +/// also be configured with [set rules]($styling/#set-rules) to apply them to +/// all elements within a scope. Optional fields are only available with field +/// access syntax when they were explicitly passed to the element function, not +/// when they result from a set rule. +/// +/// Each element has a default appearance. However, you can also completely +/// customize its appearance with a [show rule]($styling/#show-rules). The show +/// rule is passed the element. It can access the element's field and produce +/// arbitrary content from it. +/// +/// In the web app, you can hover over a content variable to see exactly which +/// elements the content is composed of and what fields they have. +/// Alternatively, you can inspect the output of the [`repr`] function. +#[ty(scope, cast)] +#[derive(Clone, Hash)] +#[allow(clippy::derived_hash_with_manual_eq)] +pub struct Content { + /// The partially element-dependent inner data. + inner: Arc<Inner<dyn Bounds>>, + /// The element's source code location. + span: Span, +} + +/// The inner representation behind the `Arc`. +#[derive(Hash)] +struct Inner<T: ?Sized + 'static> { + /// An optional label attached to the element. + label: Option<Label>, + /// The element's location which identifies it in the layouted output. + location: Option<Location>, + /// Manages the element during realization. + /// - If bit 0 is set, the element is prepared. + /// - If bit n is set, the element is guarded against the n-th show rule + /// recipe from the top of the style chain (counting from 1). + lifecycle: SmallBitSet, + /// The element's raw data. + elem: LazyHash<T>, +} + +impl Content { + /// Creates a new content from an element. + pub fn new<T: NativeElement>(elem: T) -> Self { + Self { + inner: Arc::new(Inner { + label: None, + location: None, + lifecycle: SmallBitSet::new(), + elem: elem.into(), + }), + span: Span::detached(), + } + } + + /// Creates a empty sequence content. + pub fn empty() -> Self { + singleton!(Content, SequenceElem::default().pack()).clone() + } + + /// Get the element of this content. + pub fn elem(&self) -> Element { + self.inner.elem.dyn_elem() + } + + /// Get the span of the content. + pub fn span(&self) -> Span { + self.span + } + + /// Set the span of the content. + pub fn spanned(mut self, span: Span) -> Self { + if self.span.is_detached() { + self.span = span; + } + self + } + + /// Get the label of the content. + pub fn label(&self) -> Option<Label> { + self.inner.label + } + + /// Attach a label to the content. + pub fn labelled(mut self, label: Label) -> Self { + self.set_label(label); + self + } + + /// Set the label of the content. + pub fn set_label(&mut self, label: Label) { + self.make_mut().label = Some(label); + } + + /// Assigns a location to the content. + /// + /// This identifies the content and e.g. makes it linkable by + /// `.linked(Destination::Location(loc))`. + /// + /// Useful in combination with [`Location::variant`]. + pub fn located(mut self, loc: Location) -> Self { + self.set_location(loc); + self + } + + /// Set the location of the content. + pub fn set_location(&mut self, location: Location) { + self.make_mut().location = Some(location); + } + + /// Check whether a show rule recipe is disabled. + pub fn is_guarded(&self, index: RecipeIndex) -> bool { + self.inner.lifecycle.contains(index.0) + } + + /// Disable a show rule recipe. + pub fn guarded(mut self, index: RecipeIndex) -> Self { + self.make_mut().lifecycle.insert(index.0); + self + } + + /// Whether this content has already been prepared. + pub fn is_prepared(&self) -> bool { + self.inner.lifecycle.contains(0) + } + + /// Mark this content as prepared. + pub fn mark_prepared(&mut self) { + self.make_mut().lifecycle.insert(0); + } + + /// Get a field by ID. + /// + /// This is the preferred way to access fields. However, you can only use it + /// if you have set the field IDs yourself or are using the field IDs + /// generated by the `#[elem]` macro. + pub fn get( + &self, + id: u8, + styles: Option<StyleChain>, + ) -> Result<Value, FieldAccessError> { + if id == 255 { + if let Some(label) = self.label() { + return Ok(label.into_value()); + } + } + match styles { + Some(styles) => self.inner.elem.field_with_styles(id, styles), + None => self.inner.elem.field(id), + } + } + + /// Get a field by name. + /// + /// If you have access to the field IDs of the element, use [`Self::get`] + /// instead. + pub fn get_by_name(&self, name: &str) -> Result<Value, FieldAccessError> { + if name == "label" { + if let Some(label) = self.label() { + return Ok(label.into_value()); + } + } + let id = self.elem().field_id(name).ok_or(FieldAccessError::Unknown)?; + self.get(id, None) + } + + /// Get a field by ID, returning a missing field error if it does not exist. + /// + /// This is the preferred way to access fields. However, you can only use it + /// if you have set the field IDs yourself or are using the field IDs + /// generated by the `#[elem]` macro. + pub fn field(&self, id: u8) -> StrResult<Value> { + self.get(id, None) + .map_err(|e| e.message(self, self.elem().field_name(id).unwrap())) + } + + /// Get a field by name, returning a missing field error if it does not + /// exist. + /// + /// If you have access to the field IDs of the element, use [`Self::field`] + /// instead. + pub fn field_by_name(&self, name: &str) -> StrResult<Value> { + self.get_by_name(name).map_err(|e| e.message(self, name)) + } + + /// Resolve all fields with the styles and save them in-place. + pub fn materialize(&mut self, styles: StyleChain) { + self.make_mut().elem.materialize(styles); + } + + /// Create a new sequence element from multiples elements. + pub fn sequence(iter: impl IntoIterator<Item = Self>) -> Self { + let vec: Vec<_> = iter.into_iter().collect(); + if vec.is_empty() { + Self::empty() + } else if vec.len() == 1 { + vec.into_iter().next().unwrap() + } else { + SequenceElem::new(vec).into() + } + } + + /// Whether the contained element is of type `T`. + pub fn is<T: NativeElement>(&self) -> bool { + self.inner.elem.dyn_type_id() == TypeId::of::<T>() + } + + /// Downcasts the element to a packed value. + pub fn to_packed<T: NativeElement>(&self) -> Option<&Packed<T>> { + Packed::from_ref(self) + } + + /// Downcasts the element to a mutable packed value. + pub fn to_packed_mut<T: NativeElement>(&mut self) -> Option<&mut Packed<T>> { + Packed::from_mut(self) + } + + /// Downcasts the element into an owned packed value. + pub fn into_packed<T: NativeElement>(self) -> Result<Packed<T>, Self> { + Packed::from_owned(self) + } + + /// Extract the raw underlying element. + pub fn unpack<T: NativeElement>(self) -> Result<T, Self> { + self.into_packed::<T>().map(Packed::unpack) + } + + /// Makes sure the content is not shared and returns a mutable reference to + /// the inner data. + fn make_mut(&mut self) -> &mut Inner<dyn Bounds> { + let arc = &mut self.inner; + if Arc::strong_count(arc) > 1 || Arc::weak_count(arc) > 0 { + *self = arc.elem.dyn_clone(arc, self.span); + } + Arc::get_mut(&mut self.inner).unwrap() + } + + /// Whether the contained element has the given capability. + pub fn can<C>(&self) -> bool + where + C: ?Sized + 'static, + { + self.elem().can::<C>() + } + + /// Cast to a trait object if the contained element has the given + /// capability. + pub fn with<C>(&self) -> Option<&C> + where + C: ?Sized + 'static, + { + // Safety: The vtable comes from the `Capable` implementation which + // guarantees to return a matching vtable for `Packed<T>` and `C`. + // Since any `Packed<T>` is a repr(transparent) `Content`, we can also + // use a `*const Content` pointer. + let vtable = self.elem().vtable()(TypeId::of::<C>())?; + let data = self as *const Content as *const (); + Some(unsafe { &*fat::from_raw_parts(data, vtable.as_ptr()) }) + } + + /// 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, + { + // Safety: The vtable comes from the `Capable` implementation which + // guarantees to return a matching vtable for `Packed<T>` and `C`. + // Since any `Packed<T>` is a repr(transparent) `Content`, we can also + // use a `*const Content` pointer. + // + // The resulting trait object contains an `&mut Packed<T>`. We do _not_ + // need to ensure that we hold the only reference to the `Arc` here + // because `Packed<T>`'s DerefMut impl will take care of that if + // mutable access is required. + let vtable = self.elem().vtable()(TypeId::of::<C>())?; + let data = self as *mut Content as *mut (); + Some(unsafe { &mut *fat::from_raw_parts_mut(data, vtable.as_ptr()) }) + } + + /// Whether the content is an empty sequence. + pub fn is_empty(&self) -> bool { + let Some(sequence) = self.to_packed::<SequenceElem>() else { + return false; + }; + + sequence.children.is_empty() + } + + /// Also auto expands sequence of sequences into flat sequence + pub fn sequence_recursive_for_each<'a>(&'a self, f: &mut impl FnMut(&'a Self)) { + if let Some(sequence) = self.to_packed::<SequenceElem>() { + for child in &sequence.children { + child.sequence_recursive_for_each(f); + } + } else { + f(self); + } + } + + /// Style this content with a recipe, eagerly applying it if possible. + pub fn styled_with_recipe( + self, + engine: &mut Engine, + context: Tracked<Context>, + recipe: Recipe, + ) -> SourceResult<Self> { + if recipe.selector().is_none() { + recipe.apply(engine, context, self) + } else { + Ok(self.styled(recipe)) + } + } + + /// Repeat this content `count` times. + pub fn repeat(&self, count: usize) -> Self { + Self::sequence(std::iter::repeat_with(|| self.clone()).take(count)) + } + + /// Style this content with a style entry. + pub fn styled(mut self, style: impl Into<Style>) -> Self { + if let Some(style_elem) = self.to_packed_mut::<StyledElem>() { + style_elem.styles.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 let Some(style_elem) = self.to_packed_mut::<StyledElem>() { + style_elem.styles.apply(styles); + self + } else { + StyledElem::new(self, styles).into() + } + } + + /// Style this content with a full style map in-place. + pub fn style_in_place(&mut self, styles: Styles) { + if styles.is_empty() { + return; + } + + if let Some(style_elem) = self.to_packed_mut::<StyledElem>() { + style_elem.styles.apply(styles); + } else { + *self = StyledElem::new(std::mem::take(self), styles).into(); + } + } + + /// Queries the content tree for all elements that match the given selector. + /// + /// Elements produced in `show` rules will not be included in the results. + pub fn query(&self, selector: Selector) -> Vec<Content> { + let mut results = Vec::new(); + self.traverse(&mut |element| { + if selector.matches(&element, None) { + 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. + pub fn query_first(&self, selector: Selector) -> Option<Content> { + let mut result = None; + self.traverse(&mut |element| { + if result.is_none() && selector.matches(&element, None) { + 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<F>(&self, f: &mut F) + where + F: FnMut(Content), + { + f(self.clone()); + + self.inner + .elem + .fields() + .into_iter() + .for_each(|(_, value)| walk_value(value, f)); + + /// Walks a given value to find any content that matches the selector. + fn walk_value<F>(value: Value, f: &mut F) + where + F: FnMut(Content), + { + match value { + Value::Content(content) => content.traverse(f), + Value::Array(array) => { + for value in array { + walk_value(value, f); + } + } + _ => {} + } + } + } +} + +impl Content { + /// Strongly emphasize this content. + pub fn strong(self) -> Self { + StrongElem::new(self).pack() + } + + /// Emphasize this content. + pub fn emph(self) -> Self { + EmphElem::new(self).pack() + } + + /// Underline this content. + pub fn underlined(self) -> Self { + UnderlineElem::new(self).pack() + } + + /// Link the content somewhere. + pub fn linked(self, dest: Destination) -> Self { + self.styled(LinkElem::set_dests(smallvec![dest])) + } + + /// Set alignments for this content. + pub fn aligned(self, align: Alignment) -> Self { + self.styled(AlignElem::set_alignment(align)) + } + + /// Pad this content at the sides. + pub fn padded(self, padding: Sides<Rel<Length>>) -> Self { + PadElem::new(self) + .with_left(padding.left) + .with_top(padding.top) + .with_right(padding.right) + .with_bottom(padding.bottom) + .pack() + } + + /// Transform this content's contents without affecting layout. + pub fn moved(self, delta: Axes<Rel<Length>>) -> Self { + MoveElem::new(self).with_dx(delta.x).with_dy(delta.y).pack() + } +} + +#[scope] +impl Content { + /// The content's element function. This function can be used to create the element + /// contained in this content. It can be used in set and show rules for the + /// element. Can be compared with global functions to check whether you have + /// a specific + /// kind of element. + #[func] + pub fn func(&self) -> Element { + self.elem() + } + + /// Whether the content has the specified field. + #[func] + pub fn has( + &self, + /// The field to look for. + field: Str, + ) -> bool { + if field.as_str() == "label" { + return self.label().is_some(); + } + + let Some(id) = self.elem().field_id(&field) else { + return false; + }; + + self.inner.elem.has(id) + } + + /// Access the specified field on the content. Returns the default value if + /// the field does not exist or fails with an error if no default value was + /// specified. + #[func] + pub fn at( + &self, + /// The field to access. + field: Str, + /// A default value to return if the field does not exist. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { + self.get_by_name(&field) + .or_else(|e| default.ok_or(e)) + .map_err(|e| e.message_no_default(self, &field)) + } + + /// Returns the fields of this content. + /// + /// ```example + /// #rect( + /// width: 10cm, + /// height: 10cm, + /// ).fields() + /// ``` + #[func] + pub fn fields(&self) -> Dict { + let mut dict = self.inner.elem.fields(); + if let Some(label) = self.label() { + dict.insert("label".into(), label.into_value()); + } + dict + } + + /// The location of the content. This is only available on content returned + /// by [query] or provided by a [show rule]($reference/styling/#show-rules), + /// for other content it will be `{none}`. The resulting location can be + /// used with [counters]($counter), [state] and [queries]($query). + #[func] + pub fn location(&self) -> Option<Location> { + self.inner.location + } +} + +impl Default for Content { + fn default() -> Self { + Self::empty() + } +} + +impl Debug for Content { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.inner.elem.fmt(f) + } +} + +impl<T: NativeElement> From<T> for Content { + fn from(value: T) -> Self { + Self::new(value) + } +} + +impl PartialEq for Content { + fn eq(&self, other: &Self) -> bool { + // Additional short circuit for different elements. + self.elem() == other.elem() && self.inner.elem.dyn_eq(other) + } +} + +impl Repr for Content { + fn repr(&self) -> EcoString { + self.inner.elem.repr() + } +} + +impl Add for Content { + type Output = Self; + + fn add(self, mut rhs: Self) -> Self::Output { + let mut lhs = self; + match (lhs.to_packed_mut::<SequenceElem>(), rhs.to_packed_mut::<SequenceElem>()) { + (Some(seq_lhs), Some(rhs)) => { + seq_lhs.children.extend(rhs.children.iter().cloned()); + lhs + } + (Some(seq_lhs), None) => { + seq_lhs.children.push(rhs); + lhs + } + (None, Some(rhs_seq)) => { + rhs_seq.children.insert(0, lhs); + rhs + } + (None, None) => Self::sequence([lhs, rhs]), + } + } +} + +impl<'a> Add<&'a Self> for Content { + type Output = Self; + + fn add(self, rhs: &'a Self) -> Self::Output { + let mut lhs = self; + match (lhs.to_packed_mut::<SequenceElem>(), rhs.to_packed::<SequenceElem>()) { + (Some(seq_lhs), Some(rhs)) => { + seq_lhs.children.extend(rhs.children.iter().cloned()); + lhs + } + (Some(seq_lhs), None) => { + seq_lhs.children.push(rhs.clone()); + lhs + } + (None, Some(_)) => { + let mut rhs = rhs.clone(); + rhs.to_packed_mut::<SequenceElem>().unwrap().children.insert(0, lhs); + rhs + } + (None, None) => Self::sequence([lhs, rhs.clone()]), + } + } +} + +impl AddAssign for Content { + fn add_assign(&mut self, rhs: Self) { + *self = std::mem::take(self) + rhs; + } +} + +impl AddAssign<&Self> 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 Serialize for Content { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.collect_map( + iter::once(("func".into(), self.func().name().into_value())) + .chain(self.fields()), + ) + } +} + +/// The trait that combines all the other traits into a trait object. +trait Bounds: Debug + Repr + Fields + Send + Sync + 'static { + fn dyn_type_id(&self) -> TypeId; + fn dyn_elem(&self) -> Element; + fn dyn_clone(&self, inner: &Inner<dyn Bounds>, span: Span) -> Content; + fn dyn_hash(&self, hasher: &mut dyn Hasher); + fn dyn_eq(&self, other: &Content) -> bool; +} + +impl<T: NativeElement> Bounds for T { + fn dyn_type_id(&self) -> TypeId { + TypeId::of::<Self>() + } + + fn dyn_elem(&self) -> Element { + Self::elem() + } + + fn dyn_clone(&self, inner: &Inner<dyn Bounds>, span: Span) -> Content { + Content { + inner: Arc::new(Inner { + label: inner.label, + location: inner.location, + lifecycle: inner.lifecycle.clone(), + elem: LazyHash::reuse(self.clone(), &inner.elem), + }), + span, + } + } + + fn dyn_hash(&self, mut state: &mut dyn Hasher) { + TypeId::of::<Self>().hash(&mut state); + self.hash(&mut state); + } + + fn dyn_eq(&self, other: &Content) -> bool { + let Some(other) = other.to_packed::<Self>() else { + return false; + }; + *self == **other + } +} + +impl Hash for dyn Bounds { + fn hash<H: Hasher>(&self, state: &mut H) { + self.dyn_hash(state); + } +} + +/// A packed element of a static type. +#[derive(Clone, PartialEq, Hash)] +#[repr(transparent)] +pub struct Packed<T: NativeElement>( + /// Invariant: Must be of type `T`. + Content, + PhantomData<T>, +); + +impl<T: NativeElement> Packed<T> { + /// Pack element while retaining its static type. + pub fn new(element: T) -> Self { + // Safety: The element is known to be of type `T`. + Packed(element.pack(), PhantomData) + } + + /// Try to cast type-erased content into a statically known packed element. + pub fn from_ref(content: &Content) -> Option<&Self> { + if content.is::<T>() { + // Safety: + // - We have checked the type. + // - Packed<T> is repr(transparent). + return Some(unsafe { std::mem::transmute::<&Content, &Packed<T>>(content) }); + } + None + } + + /// Try to cast type-erased content into a statically known packed element. + pub fn from_mut(content: &mut Content) -> Option<&mut Self> { + if content.is::<T>() { + // Safety: + // - We have checked the type. + // - Packed<T> is repr(transparent). + return Some(unsafe { + std::mem::transmute::<&mut Content, &mut Packed<T>>(content) + }); + } + None + } + + /// Try to cast type-erased content into a statically known packed element. + pub fn from_owned(content: Content) -> Result<Self, Content> { + if content.is::<T>() { + // Safety: + // - We have checked the type. + // - Packed<T> is repr(transparent). + return Ok(unsafe { std::mem::transmute::<Content, Packed<T>>(content) }); + } + Err(content) + } + + /// Pack back into content. + pub fn pack(self) -> Content { + self.0 + } + + /// Extract the raw underlying element. + pub fn unpack(self) -> T { + // This function doesn't yet need owned self, but might in the future. + (*self).clone() + } + + /// The element's span. + pub fn span(&self) -> Span { + self.0.span() + } + + /// Set the span of the element. + pub fn spanned(self, span: Span) -> Self { + Self(self.0.spanned(span), PhantomData) + } + + /// Accesses the label of the element. + pub fn label(&self) -> Option<Label> { + self.0.label() + } + + /// Accesses the location of the element. + pub fn location(&self) -> Option<Location> { + self.0.location() + } + + /// Sets the location of the element. + pub fn set_location(&mut self, location: Location) { + self.0.set_location(location); + } +} + +impl<T: NativeElement> AsRef<T> for Packed<T> { + fn as_ref(&self) -> &T { + self + } +} + +impl<T: NativeElement> AsMut<T> for Packed<T> { + fn as_mut(&mut self) -> &mut T { + self + } +} + +impl<T: NativeElement> Deref for Packed<T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + // Safety: + // - Packed<T> guarantees that the content trait object wraps + // an element of type `T`. + // - This downcast works the same way as dyn Any's does. We can't reuse + // that one because we don't want to pay the cost for every deref. + let elem = &*self.0.inner.elem; + unsafe { &*(elem as *const dyn Bounds as *const T) } + } +} + +impl<T: NativeElement> DerefMut for Packed<T> { + fn deref_mut(&mut self) -> &mut Self::Target { + // Safety: + // - Packed<T> guarantees that the content trait object wraps + // an element of type `T`. + // - We have guaranteed unique access thanks to `make_mut`. + // - This downcast works the same way as dyn Any's does. We can't reuse + // that one because we don't want to pay the cost for every deref. + let elem = &mut *self.0.make_mut().elem; + unsafe { &mut *(elem as *mut dyn Bounds as *mut T) } + } +} + +impl<T: NativeElement + Debug> Debug for Packed<T> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +/// A sequence of content. +#[elem(Debug, Repr, PartialEq)] +pub struct SequenceElem { + /// The elements. + #[required] + pub children: Vec<Content>, +} + +impl Debug for SequenceElem { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Sequence ")?; + f.debug_list().entries(&self.children).finish() + } +} + +// Derive is currently incompatible with `elem` macro. +#[allow(clippy::derivable_impls)] +impl Default for SequenceElem { + fn default() -> Self { + Self { children: Default::default() } + } +} + +impl PartialEq for SequenceElem { + fn eq(&self, other: &Self) -> bool { + self.children.iter().eq(other.children.iter()) + } +} + +impl Repr for SequenceElem { + fn repr(&self) -> EcoString { + if self.children.is_empty() { + "[]".into() + } else { + let elements = crate::foundations::repr::pretty_array_like( + &self.children.iter().map(|c| c.inner.elem.repr()).collect::<Vec<_>>(), + false, + ); + eco_format!("sequence{}", elements) + } + } +} + +/// Content alongside styles. +#[elem(Debug, Repr, PartialEq)] +pub struct StyledElem { + /// The content. + #[required] + pub child: Content, + /// The styles. + #[required] + pub styles: Styles, +} + +impl Debug for StyledElem { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + for style in self.styles.iter() { + writeln!(f, "#{style:?}")?; + } + self.child.fmt(f) + } +} + +impl PartialEq for StyledElem { + fn eq(&self, other: &Self) -> bool { + self.child == other.child + } +} + +impl Repr for StyledElem { + fn repr(&self) -> EcoString { + eco_format!("styled(child: {}, ..)", self.child.repr()) + } +} + +/// 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); +} + +/// An error arising when trying to access a field of content. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum FieldAccessError { + Unknown, + Unset, + Internal, +} + +impl FieldAccessError { + /// Formats the error message given the content and the field name. + #[cold] + pub fn message(self, content: &Content, field: &str) -> EcoString { + let elem_name = content.elem().name(); + match self { + FieldAccessError::Unknown => { + eco_format!("{elem_name} does not have field {}", field.repr()) + } + FieldAccessError::Unset => { + eco_format!( + "field {} in {elem_name} is not known at this point", + field.repr() + ) + } + FieldAccessError::Internal => { + eco_format!( + "internal error when accessing field {} in {elem_name} – this is a bug", + field.repr() + ) + } + } + } + + /// Formats the error message for an `at` calls without a default value. + #[cold] + pub fn message_no_default(self, content: &Content, field: &str) -> EcoString { + let mut msg = self.message(content, field); + msg.push_str(" and no default was specified"); + msg + } +} diff --git a/crates/typst-library/src/foundations/context.rs b/crates/typst-library/src/foundations/context.rs new file mode 100644 index 00000000..bf4bdcd2 --- /dev/null +++ b/crates/typst-library/src/foundations/context.rs @@ -0,0 +1,85 @@ +use comemo::Track; + +use crate::diag::{bail, Hint, HintedStrResult, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + elem, Args, Construct, Content, Func, Packed, Show, StyleChain, Value, +}; +use crate::introspection::{Locatable, Location}; + +/// Data that is contextually made available to code. +/// +/// _Contextual_ functions and expressions require the presence of certain +/// pieces of context to be evaluated. This includes things like `text.lang`, +/// `measure`, or `counter(heading).get()`. +#[derive(Debug, Default, Clone, Hash)] +pub struct Context<'a> { + /// The location in the document. + pub location: Option<Location>, + /// The active styles. + pub styles: Option<StyleChain<'a>>, +} + +impl<'a> Context<'a> { + /// An empty context. + pub fn none() -> Self { + Self::default() + } + + /// Create a new context from its parts. + pub fn new(location: Option<Location>, styles: Option<StyleChain<'a>>) -> Self { + Self { location, styles } + } +} + +#[comemo::track] +impl<'a> Context<'a> { + /// Try to extract the location. + pub fn location(&self) -> HintedStrResult<Location> { + require(self.location) + } + + /// Try to extract the styles. + pub fn styles(&self) -> HintedStrResult<StyleChain<'a>> { + require(self.styles) + } + + /// Guard access to the introspector by requiring at least some piece of context. + pub fn introspect(&self) -> HintedStrResult<()> { + require(self.location.map(|_| ()).or(self.styles.map(|_| ()))) + } +} + +/// Extracts an optional piece of context, yielding an error with hints if +/// it isn't available. +fn require<T>(val: Option<T>) -> HintedStrResult<T> { + val.ok_or("can only be used when context is known") + .hint("try wrapping this in a `context` expression") + .hint( + "the `context` expression should wrap everything that depends on this function", + ) +} + +/// Executes a `context` block. +#[elem(Construct, Locatable, Show)] +pub struct ContextElem { + /// The function to call with the context. + #[required] + #[internal] + func: Func, +} + +impl Construct for ContextElem { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> { + bail!(args.span, "cannot be constructed manually"); + } +} + +impl Show for Packed<ContextElem> { + #[typst_macros::time(name = "context", span = self.span())] + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let loc = self.location().unwrap(); + let context = Context::new(Some(loc), Some(styles)); + Ok(self.func.call::<[Value; 0]>(engine, context.track(), [])?.display()) + } +} diff --git a/crates/typst-library/src/foundations/datetime.rs b/crates/typst-library/src/foundations/datetime.rs new file mode 100644 index 00000000..d15cd417 --- /dev/null +++ b/crates/typst-library/src/foundations/datetime.rs @@ -0,0 +1,580 @@ +use std::cmp::Ordering; +use std::hash::Hash; +use std::ops::{Add, Sub}; + +use ecow::{eco_format, EcoString, EcoVec}; +use time::error::{Format, InvalidFormatDescription}; +use time::macros::format_description; +use time::{format_description, Month, PrimitiveDateTime}; + +use crate::diag::{bail, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, func, repr, scope, ty, Dict, Duration, Repr, Smart, Str, Value, +}; +use crate::World; + +/// Represents a date, a time, or a combination of both. +/// +/// Can be created by either specifying a custom datetime using this type's +/// constructor function or getting the current date with +/// [`datetime.today`]($datetime.today). +/// +/// # Example +/// ```example +/// #let date = datetime( +/// year: 2020, +/// month: 10, +/// day: 4, +/// ) +/// +/// #date.display() \ +/// #date.display( +/// "y:[year repr:last_two]" +/// ) +/// +/// #let time = datetime( +/// hour: 18, +/// minute: 2, +/// second: 23, +/// ) +/// +/// #time.display() \ +/// #time.display( +/// "h:[hour repr:12][period]" +/// ) +/// ``` +/// +/// # Datetime and Duration +/// You can get a [duration] by subtracting two datetime: +/// ```example +/// #let first-of-march = datetime(day: 1, month: 3, year: 2024) +/// #let first-of-jan = datetime(day: 1, month: 1, year: 2024) +/// #let distance = first-of-march - first-of-jan +/// #distance.hours() +/// ``` +/// +/// You can also add/subtract a datetime and a duration to retrieve a new, +/// offset datetime: +/// ```example +/// #let date = datetime(day: 1, month: 3, year: 2024) +/// #let two-days = duration(days: 2) +/// #let two-days-earlier = date - two-days +/// #let two-days-later = date + two-days +/// +/// #date.display() \ +/// #two-days-earlier.display() \ +/// #two-days-later.display() +/// ``` +/// +/// # Format +/// You can specify a customized formatting using the +/// [`display`]($datetime.display) method. The format of a datetime is +/// specified by providing _components_ with a specified number of _modifiers_. +/// A component represents a certain part of the datetime that you want to +/// display, and with the help of modifiers you can define how you want to +/// display that component. In order to display a component, you wrap the name +/// of the component in square brackets (e.g. `[[year]]` will display the year). +/// In order to add modifiers, you add a space after the component name followed +/// by the name of the modifier, a colon and the value of the modifier (e.g. +/// `[[month repr:short]]` will display the short representation of the month). +/// +/// The possible combination of components and their respective modifiers is as +/// follows: +/// +/// - `year`: Displays the year of the datetime. +/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the +/// year is padded. +/// - `repr` Can be either `full` in which case the full year is displayed or +/// `last_two` in which case only the last two digits are displayed. +/// - `sign`: Can be either `automatic` or `mandatory`. Specifies when the +/// sign should be displayed. +/// - `month`: Displays the month of the datetime. +/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the +/// month is padded. +/// - `repr`: Can be either `numerical`, `long` or `short`. Specifies if the +/// month should be displayed as a number or a word. Unfortunately, when +/// choosing the word representation, it can currently only display the +/// English version. In the future, it is planned to support localization. +/// - `day`: Displays the day of the datetime. +/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the +/// day is padded. +/// - `week_number`: Displays the week number of the datetime. +/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the +/// week number is padded. +/// - `repr`: Can be either `ISO`, `sunday` or `monday`. In the case of `ISO`, +/// week numbers are between 1 and 53, while the other ones are between 0 +/// and 53. +/// - `weekday`: Displays the weekday of the date. +/// - `repr` Can be either `long`, `short`, `sunday` or `monday`. In the case +/// of `long` and `short`, the corresponding English name will be displayed +/// (same as for the month, other languages are currently not supported). In +/// the case of `sunday` and `monday`, the numerical value will be displayed +/// (assuming Sunday and Monday as the first day of the week, respectively). +/// - `one_indexed`: Can be either `true` or `false`. Defines whether the +/// numerical representation of the week starts with 0 or 1. +/// - `hour`: Displays the hour of the date. +/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the +/// hour is padded. +/// - `repr`: Can be either `24` or `12`. Changes whether the hour is +/// displayed in the 24-hour or 12-hour format. +/// - `period`: The AM/PM part of the hour +/// - `case`: Can be `lower` to display it in lower case and `upper` to +/// display it in upper case. +/// - `minute`: Displays the minute of the date. +/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the +/// minute is padded. +/// - `second`: Displays the second of the date. +/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the +/// second is padded. +/// +/// Keep in mind that not always all components can be used. For example, if you +/// create a new datetime with `{datetime(year: 2023, month: 10, day: 13)}`, it +/// will be stored as a plain date internally, meaning that you cannot use +/// components such as `hour` or `minute`, which would only work on datetimes +/// that have a specified time. +#[ty(scope, cast)] +#[derive(Debug, 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 { + /// 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))) + } + + /// Try to parse a dictionary as a TOML date. + pub fn from_toml_dict(dict: &Dict) -> Option<Self> { + if dict.len() != 1 { + return None; + } + + let Ok(Value::Str(string)) = dict.get("$__toml_private_datetime") else { + return None; + }; + + if let Ok(d) = time::PrimitiveDateTime::parse( + string, + &format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]Z"), + ) { + Self::from_ymd_hms( + d.year(), + d.month() as u8, + d.day(), + d.hour(), + d.minute(), + d.second(), + ) + } else if let Ok(d) = time::PrimitiveDateTime::parse( + string, + &format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]"), + ) { + Self::from_ymd_hms( + d.year(), + d.month() as u8, + d.day(), + d.hour(), + d.minute(), + d.second(), + ) + } else if let Ok(d) = + time::Date::parse(string, &format_description!("[year]-[month]-[day]")) + { + Self::from_ymd(d.year(), d.month() as u8, d.day()) + } else if let Ok(d) = + time::Time::parse(string, &format_description!("[hour]:[minute]:[second]")) + { + Self::from_hms(d.hour(), d.minute(), d.second()) + } else { + None + } + } + + /// Which kind of variant this datetime stores. + pub fn kind(&self) -> &'static str { + match self { + Datetime::Datetime(_) => "datetime", + Datetime::Date(_) => "date", + Datetime::Time(_) => "time", + } + } +} + +#[scope] +impl Datetime { + /// Creates a new datetime. + /// + /// You can specify the [datetime] using a year, month, day, hour, minute, + /// and second. + /// + /// _Note_: Depending on which components of the datetime you specify, Typst + /// will store it in one of the following three ways: + /// * If you specify year, month and day, Typst will store just a date. + /// * If you specify hour, minute and second, Typst will store just a time. + /// * If you specify all of year, month, day, hour, minute and second, Typst + /// will store a full datetime. + /// + /// Depending on how it is stored, the [`display`]($datetime.display) method + /// will choose a different formatting by default. + /// + /// ```example + /// #datetime( + /// year: 2012, + /// month: 8, + /// day: 3, + /// ).display() + /// ``` + #[func(constructor)] + pub fn construct( + /// The year of the datetime. + #[named] + year: Option<i32>, + /// The month of the datetime. + #[named] + month: Option<Month>, + /// The day of the datetime. + #[named] + day: Option<u8>, + /// The hour of the datetime. + #[named] + hour: Option<u8>, + /// The minute of the datetime. + #[named] + minute: Option<u8>, + /// The second of the datetime. + #[named] + second: Option<u8>, + ) -> StrResult<Datetime> { + let time = match (hour, minute, second) { + (Some(hour), Some(minute), Some(second)) => { + match time::Time::from_hms(hour, minute, second) { + Ok(time) => Some(time), + Err(_) => bail!("time is invalid"), + } + } + (None, None, None) => None, + _ => bail!("time is incomplete"), + }; + + let date = match (year, month, day) { + (Some(year), Some(month), Some(day)) => { + match time::Date::from_calendar_date(year, month, day) { + Ok(date) => Some(date), + Err(_) => bail!("date is invalid"), + } + } + (None, None, None) => None, + _ => bail!("date is incomplete"), + }; + + Ok(match (date, time) { + (Some(date), Some(time)) => { + Datetime::Datetime(PrimitiveDateTime::new(date, time)) + } + (Some(date), None) => Datetime::Date(date), + (None, Some(time)) => Datetime::Time(time), + (None, None) => { + bail!("at least one of date or time must be fully specified") + } + }) + } + + /// Returns the current date. + /// + /// ```example + /// Today's date is + /// #datetime.today().display(). + /// ``` + #[func] + pub fn today( + /// The engine. + engine: &mut Engine, + /// An offset to apply to the current UTC date. If set to `{auto}`, the + /// offset will be the local offset. + #[named] + #[default] + offset: Smart<i64>, + ) -> StrResult<Datetime> { + Ok(engine + .world + .today(offset.custom()) + .ok_or("unable to get the current date")?) + } + + /// Displays the datetime in a specified format. + /// + /// Depending on whether you have defined just a date, a time or both, the + /// default format will be different. If you specified a date, it will be + /// `[[year]-[month]-[day]]`. If you specified a time, it will be + /// `[[hour]:[minute]:[second]]`. In the case of a datetime, it will be + /// `[[year]-[month]-[day] [hour]:[minute]:[second]]`. + /// + /// See the [format syntax]($datetime/#format) for more information. + #[func] + pub fn display( + &self, + /// The format used to display the datetime. + #[default] + pattern: Smart<DisplayPattern>, + ) -> StrResult<EcoString> { + let pat = |s| format_description::parse_borrowed::<2>(s).unwrap(); + let result = match pattern { + Smart::Auto => match self { + Self::Date(date) => date.format(&pat("[year]-[month]-[day]")), + Self::Time(time) => time.format(&pat("[hour]:[minute]:[second]")), + Self::Datetime(datetime) => { + datetime.format(&pat("[year]-[month]-[day] [hour]:[minute]:[second]")) + } + }, + + Smart::Custom(DisplayPattern(_, format)) => match self { + Self::Date(date) => date.format(&format), + Self::Time(time) => time.format(&format), + Self::Datetime(datetime) => datetime.format(&format), + }, + }; + result.map(EcoString::from).map_err(format_time_format_error) + } + + /// The year if it was specified, or `{none}` for times without a date. + #[func] + pub fn year(&self) -> Option<i32> { + match self { + Self::Date(date) => Some(date.year()), + Self::Time(_) => None, + Self::Datetime(datetime) => Some(datetime.year()), + } + } + + /// The month if it was specified, or `{none}` for times without a date. + #[func] + pub fn month(&self) -> Option<u8> { + match self { + Self::Date(date) => Some(date.month().into()), + Self::Time(_) => None, + Self::Datetime(datetime) => Some(datetime.month().into()), + } + } + + /// The weekday (counting Monday as 1) or `{none}` for times without a date. + #[func] + pub fn weekday(&self) -> Option<u8> { + match self { + Self::Date(date) => Some(date.weekday().number_from_monday()), + Self::Time(_) => None, + Self::Datetime(datetime) => Some(datetime.weekday().number_from_monday()), + } + } + + /// The day if it was specified, or `{none}` for times without a date. + #[func] + pub fn day(&self) -> Option<u8> { + match self { + Self::Date(date) => Some(date.day()), + Self::Time(_) => None, + Self::Datetime(datetime) => Some(datetime.day()), + } + } + + /// The hour if it was specified, or `{none}` for dates without a time. + #[func] + pub fn hour(&self) -> Option<u8> { + match self { + Self::Date(_) => None, + Self::Time(time) => Some(time.hour()), + Self::Datetime(datetime) => Some(datetime.hour()), + } + } + + /// The minute if it was specified, or `{none}` for dates without a time. + #[func] + pub fn minute(&self) -> Option<u8> { + match self { + Self::Date(_) => None, + Self::Time(time) => Some(time.minute()), + Self::Datetime(datetime) => Some(datetime.minute()), + } + } + + /// The second if it was specified, or `{none}` for dates without a time. + #[func] + pub fn second(&self) -> Option<u8> { + match self { + Self::Date(_) => None, + Self::Time(time) => Some(time.second()), + Self::Datetime(datetime) => Some(datetime.second()), + } + } + + /// The ordinal (day of the year), or `{none}` for times without a date. + #[func] + pub fn ordinal(&self) -> Option<u16> { + match self { + Self::Datetime(datetime) => Some(datetime.ordinal()), + Self::Date(date) => Some(date.ordinal()), + Self::Time(_) => None, + } + } +} + +impl Repr for Datetime { + fn repr(&self) -> EcoString { + let year = self.year().map(|y| eco_format!("year: {}", (y as i64).repr())); + let month = self.month().map(|m| eco_format!("month: {}", (m as i64).repr())); + let day = self.day().map(|d| eco_format!("day: {}", (d as i64).repr())); + let hour = self.hour().map(|h| eco_format!("hour: {}", (h as i64).repr())); + let minute = self.minute().map(|m| eco_format!("minute: {}", (m as i64).repr())); + let second = self.second().map(|s| eco_format!("second: {}", (s as i64).repr())); + let filtered = [year, month, day, hour, minute, second] + .into_iter() + .flatten() + .collect::<EcoVec<_>>(); + + eco_format!("datetime{}", &repr::pretty_array_like(&filtered, false)) + } +} + +impl PartialOrd for Datetime { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + match (self, other) { + (Self::Datetime(a), Self::Datetime(b)) => a.partial_cmp(b), + (Self::Date(a), Self::Date(b)) => a.partial_cmp(b), + (Self::Time(a), Self::Time(b)) => a.partial_cmp(b), + _ => None, + } + } +} + +impl Add<Duration> for Datetime { + type Output = Self; + + fn add(self, rhs: Duration) -> Self::Output { + let rhs: time::Duration = rhs.into(); + match self { + Self::Datetime(datetime) => Self::Datetime(datetime + rhs), + Self::Date(date) => Self::Date(date + rhs), + Self::Time(time) => Self::Time(time + rhs), + } + } +} + +impl Sub<Duration> for Datetime { + type Output = Self; + + fn sub(self, rhs: Duration) -> Self::Output { + let rhs: time::Duration = rhs.into(); + match self { + Self::Datetime(datetime) => Self::Datetime(datetime - rhs), + Self::Date(date) => Self::Date(date - rhs), + Self::Time(time) => Self::Time(time - rhs), + } + } +} + +impl Sub for Datetime { + type Output = StrResult<Duration>; + + fn sub(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (Self::Datetime(a), Self::Datetime(b)) => Ok((a - b).into()), + (Self::Date(a), Self::Date(b)) => Ok((a - b).into()), + (Self::Time(a), Self::Time(b)) => Ok((a - b).into()), + (a, b) => bail!("cannot subtract {} from {}", b.kind(), a.kind()), + } + } +} + +/// A format in which a datetime can be displayed. +pub struct DisplayPattern(Str, format_description::OwnedFormatItem); + +cast! { + DisplayPattern, + self => self.0.into_value(), + v: Str => { + let item = format_description::parse_owned::<2>(&v) + .map_err(format_time_invalid_format_description_error)?; + Self(v, item) + } +} + +cast! { + Month, + v: u8 => Self::try_from(v).map_err(|_| "month is invalid")? +} + +/// 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), + Format::InsufficientTypeInformation { .. } => { + "failed to format datetime (insufficient information)".into() + } + err => eco_format!("failed to format datetime in the requested format ({err})"), + } +} + +/// 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) + } + err => eco_format!("failed to parse datetime format ({err})"), + } +} diff --git a/crates/typst-library/src/foundations/decimal.rs b/crates/typst-library/src/foundations/decimal.rs new file mode 100644 index 00000000..cf11e1dd --- /dev/null +++ b/crates/typst-library/src/foundations/decimal.rs @@ -0,0 +1,499 @@ +use std::fmt::{self, Display, Formatter}; +use std::hash::{Hash, Hasher}; +use std::ops::Neg; +use std::str::FromStr; + +use ecow::{eco_format, EcoString}; +use rust_decimal::MathematicalOps; +use typst_syntax::{ast, Span, Spanned}; + +use crate::diag::{warning, At, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{cast, func, repr, scope, ty, Repr, Str}; +use crate::World; + +/// A fixed-point decimal number type. +/// +/// This type should be used for precise arithmetic operations on numbers +/// represented in base 10. A typical use case is representing currency. +/// +/// # Example +/// ```example +/// Decimal: #(decimal("0.1") + decimal("0.2")) \ +/// Float: #(0.1 + 0.2) +/// ``` +/// +/// # Construction and casts +/// To create a decimal number, use the `{decimal(string)}` constructor, such as +/// in `{decimal("3.141592653")}` **(note the double quotes!)**. This +/// constructor preserves all given fractional digits, provided they are +/// representable as per the limits specified below (otherwise, an error is +/// raised). +/// +/// You can also convert any [integer]($int) to a decimal with the +/// `{decimal(int)}` constructor, e.g. `{decimal(59)}`. However, note that +/// constructing a decimal from a [floating-point number]($float), while +/// supported, **is an imprecise conversion and therefore discouraged.** A +/// warning will be raised if Typst detects that there was an accidental `float` +/// to `decimal` cast through its constructor, e.g. if writing `{decimal(3.14)}` +/// (note the lack of double quotes, indicating this is an accidental `float` +/// cast and therefore imprecise). It is recommended to use strings for +/// constant decimal values instead (e.g. `{decimal("3.14")}`). +/// +/// The precision of a `float` to `decimal` cast can be slightly improved by +/// rounding the result to 15 digits with [`calc.round`]($calc.round), but there +/// are still no precision guarantees for that kind of conversion. +/// +/// # Operations +/// Basic arithmetic operations are supported on two decimals and on pairs of +/// decimals and integers. +/// +/// Built-in operations between `float` and `decimal` are not supported in order +/// to guard against accidental loss of precision. They will raise an error +/// instead. +/// +/// Certain `calc` functions, such as trigonometric functions and power between +/// two real numbers, are also only supported for `float` (although raising +/// `decimal` to integer exponents is supported). You can opt into potentially +/// imprecise operations with the `{float(decimal)}` constructor, which casts +/// the `decimal` number into a `float`, allowing for operations without +/// precision guarantees. +/// +/// # Displaying decimals +/// To display a decimal, simply insert the value into the document. To only +/// display a certain number of digits, [round]($calc.round) the decimal first. +/// Localized formatting of decimals and other numbers is not yet supported, but +/// planned for the future. +/// +/// You can convert decimals to strings using the [`str`] constructor. This way, +/// you can post-process the displayed representation, e.g. to replace the +/// period with a comma (as a stand-in for proper built-in localization to +/// languages that use the comma). +/// +/// # Precision and limits +/// A `decimal` number has a limit of 28 to 29 significant base-10 digits. This +/// includes the sum of digits before and after the decimal point. As such, +/// numbers with more fractional digits have a smaller range. The maximum and +/// minimum `decimal` numbers have a value of `{79228162514264337593543950335}` +/// and `{-79228162514264337593543950335}` respectively. In contrast with +/// [`float`], this type does not support infinity or NaN, so overflowing or +/// underflowing operations will raise an error. +/// +/// Typical operations between `decimal` numbers, such as addition, +/// multiplication, and [power]($calc.pow) to an integer, will be highly precise +/// due to their fixed-point representation. Note, however, that multiplication +/// and division may not preserve all digits in some edge cases: while they are +/// considered precise, digits past the limits specified above are rounded off +/// and lost, so some loss of precision beyond the maximum representable digits +/// is possible. Note that this behavior can be observed not only when dividing, +/// but also when multiplying by numbers between 0 and 1, as both operations can +/// push a number's fractional digits beyond the limits described above, leading +/// to rounding. When those two operations do not surpass the digit limits, they +/// are fully precise. +#[ty(scope, cast)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Decimal(rust_decimal::Decimal); + +impl Decimal { + pub const ZERO: Self = Self(rust_decimal::Decimal::ZERO); + pub const ONE: Self = Self(rust_decimal::Decimal::ONE); + pub const MIN: Self = Self(rust_decimal::Decimal::MIN); + pub const MAX: Self = Self(rust_decimal::Decimal::MAX); + + /// Whether this decimal value is zero. + pub const fn is_zero(self) -> bool { + self.0.is_zero() + } + + /// Whether this decimal value is negative. + pub const fn is_negative(self) -> bool { + self.0.is_sign_negative() + } + + /// Whether this decimal has fractional part equal to zero (is an integer). + pub fn is_integer(self) -> bool { + self.0.is_integer() + } + + /// Computes the absolute value of this decimal. + pub fn abs(self) -> Self { + Self(self.0.abs()) + } + + /// Computes the largest integer less than or equal to this decimal. + /// + /// A decimal is returned as this may not be within `i64`'s range of + /// values. + pub fn floor(self) -> Self { + Self(self.0.floor()) + } + + /// Computes the smallest integer greater than or equal to this decimal. + /// + /// A decimal is returned as this may not be within `i64`'s range of + /// values. + pub fn ceil(self) -> Self { + Self(self.0.ceil()) + } + + /// Returns the integer part of this decimal. + pub fn trunc(self) -> Self { + Self(self.0.trunc()) + } + + /// Returns the fractional part of this decimal (with the integer part set + /// to zero). + pub fn fract(self) -> Self { + Self(self.0.fract()) + } + + /// Rounds this decimal up to the specified amount of digits with the + /// traditional rounding rules, using the "midpoint away from zero" + /// strategy (6.5 -> 7, -6.5 -> -7). + /// + /// If given a negative amount of digits, rounds to integer digits instead + /// with the same rounding strategy. For example, rounding to -3 digits + /// will turn 34567.89 into 35000.00 and -34567.89 into -35000.00. + /// + /// Note that this can return `None` when using negative digits where the + /// rounded number would overflow the available range for decimals. + pub fn round(self, digits: i32) -> Option<Self> { + // Positive digits can be handled by just rounding with rust_decimal. + if let Ok(positive_digits) = u32::try_from(digits) { + return Some(Self(self.0.round_dp_with_strategy( + positive_digits, + rust_decimal::RoundingStrategy::MidpointAwayFromZero, + ))); + } + + // We received negative digits, so we round to integer digits. + let mut num = self.0; + let old_scale = num.scale(); + let digits = -digits as u32; + + let (Ok(_), Some(ten_to_digits)) = ( + // Same as dividing by 10^digits. + num.set_scale(old_scale + digits), + rust_decimal::Decimal::TEN.checked_powi(digits as i64), + ) else { + // Scaling more than any possible amount of integer digits. + let mut zero = rust_decimal::Decimal::ZERO; + zero.set_sign_negative(self.is_negative()); + return Some(Self(zero)); + }; + + // Round to this integer digit. + num = num.round_dp_with_strategy( + 0, + rust_decimal::RoundingStrategy::MidpointAwayFromZero, + ); + + // Multiply by 10^digits again, which can overflow and fail. + num.checked_mul(ten_to_digits).map(Self) + } + + /// Attempts to add two decimals. + /// + /// Returns `None` on overflow or underflow. + pub fn checked_add(self, other: Self) -> Option<Self> { + self.0.checked_add(other.0).map(Self) + } + + /// Attempts to subtract a decimal from another. + /// + /// Returns `None` on overflow or underflow. + pub fn checked_sub(self, other: Self) -> Option<Self> { + self.0.checked_sub(other.0).map(Self) + } + + /// Attempts to multiply two decimals. + /// + /// Returns `None` on overflow or underflow. + pub fn checked_mul(self, other: Self) -> Option<Self> { + self.0.checked_mul(other.0).map(Self) + } + + /// Attempts to divide two decimals. + /// + /// Returns `None` if `other` is zero, as well as on overflow or underflow. + pub fn checked_div(self, other: Self) -> Option<Self> { + self.0.checked_div(other.0).map(Self) + } + + /// Attempts to obtain the quotient of Euclidean division between two + /// decimals. Implemented similarly to [`f64::div_euclid`]. + /// + /// The returned quotient is truncated and adjusted if the remainder was + /// negative. + /// + /// Returns `None` if `other` is zero, as well as on overflow or underflow. + pub fn checked_div_euclid(self, other: Self) -> Option<Self> { + let q = self.0.checked_div(other.0)?.trunc(); + if self + .0 + .checked_rem(other.0) + .as_ref() + .is_some_and(rust_decimal::Decimal::is_sign_negative) + { + return if other.0.is_sign_positive() { + q.checked_sub(rust_decimal::Decimal::ONE).map(Self) + } else { + q.checked_add(rust_decimal::Decimal::ONE).map(Self) + }; + } + Some(Self(q)) + } + + /// Attempts to obtain the remainder of Euclidean division between two + /// decimals. Implemented similarly to [`f64::rem_euclid`]. + /// + /// The returned decimal `r` is non-negative within the range + /// `0.0 <= r < other.abs()`. + /// + /// Returns `None` if `other` is zero, as well as on overflow or underflow. + pub fn checked_rem_euclid(self, other: Self) -> Option<Self> { + let r = self.0.checked_rem(other.0)?; + Some(Self(if r.is_sign_negative() { r.checked_add(other.0.abs())? } else { r })) + } + + /// Attempts to calculate the remainder of the division of two decimals. + /// + /// Returns `None` if `other` is zero, as well as on overflow or underflow. + pub fn checked_rem(self, other: Self) -> Option<Self> { + self.0.checked_rem(other.0).map(Self) + } + + /// Attempts to take one decimal to the power of an integer. + /// + /// Returns `None` for invalid operands, as well as on overflow or + /// underflow. + pub fn checked_powi(self, other: i64) -> Option<Self> { + self.0.checked_powi(other).map(Self) + } +} + +#[scope] +impl Decimal { + /// Converts a value to a `decimal`. + /// + /// It is recommended to use a string to construct the decimal number, or an + /// [integer]($int) (if desired). The string must contain a number in the + /// format `{"3.14159"}` (or `{"-3.141519"}` for negative numbers). The + /// fractional digits are fully preserved; if that's not possible due to the + /// limit of significant digits (around 28 to 29) having been reached, an + /// error is raised as the given decimal number wouldn't be representable. + /// + /// While this constructor can be used with [floating-point numbers]($float) + /// to cast them to `decimal`, doing so is **discouraged** as **this cast is + /// inherently imprecise.** It is easy to accidentally perform this cast by + /// writing `{decimal(1.234)}` (note the lack of double quotes), which is + /// why Typst will emit a warning in that case. Please write + /// `{decimal("1.234")}` instead for that particular case (initialization of + /// a constant decimal). Also note that floats that are NaN or infinite + /// cannot be cast to decimals and will raise an error. + /// + /// ```example + /// #decimal("1.222222222222222") + /// ``` + #[func(constructor)] + pub fn construct( + engine: &mut Engine, + /// The value that should be converted to a decimal. + value: Spanned<ToDecimal>, + ) -> SourceResult<Decimal> { + match value.v { + ToDecimal::Str(str) => Self::from_str(&str.replace(repr::MINUS_SIGN, "-")) + .map_err(|_| eco_format!("invalid decimal: {str}")) + .at(value.span), + ToDecimal::Int(int) => Ok(Self::from(int)), + ToDecimal::Float(float) => { + warn_on_float_literal(engine, value.span); + Self::try_from(float) + .map_err(|_| { + eco_format!( + "float is not a valid decimal: {}", + repr::format_float(float, None, true, "") + ) + }) + .at(value.span) + } + } + } +} + +/// Emits a warning when a decimal is constructed from a float literal. +fn warn_on_float_literal(engine: &mut Engine, span: Span) -> Option<()> { + let id = span.id()?; + let source = engine.world.source(id).ok()?; + let node = source.find(span)?; + if node.is::<ast::Float>() { + engine.sink.warn(warning!( + span, + "creating a decimal using imprecise float literal"; + hint: "use a string in the decimal constructor to avoid loss \ + of precision: `decimal({})`", + node.text().repr() + )); + } + Some(()) +} + +impl FromStr for Decimal { + type Err = rust_decimal::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + rust_decimal::Decimal::from_str_exact(s).map(Self) + } +} + +impl From<i64> for Decimal { + fn from(value: i64) -> Self { + Self(rust_decimal::Decimal::from(value)) + } +} + +impl TryFrom<f64> for Decimal { + type Error = (); + + /// Attempts to convert a Decimal to a float. + /// + /// This can fail if the float is infinite or NaN, or otherwise cannot be + /// represented by a decimal number. + fn try_from(value: f64) -> Result<Self, Self::Error> { + rust_decimal::Decimal::from_f64_retain(value).map(Self).ok_or(()) + } +} + +impl TryFrom<Decimal> for f64 { + type Error = rust_decimal::Error; + + /// Attempts to convert a Decimal to a float. + /// + /// This should in principle be infallible according to the implementation, + /// but we mirror the decimal implementation's API either way. + fn try_from(value: Decimal) -> Result<Self, Self::Error> { + value.0.try_into() + } +} + +impl TryFrom<Decimal> for i64 { + type Error = rust_decimal::Error; + + /// Attempts to convert a Decimal to an integer. + /// + /// Returns an error if the decimal has a fractional part, or if there + /// would be overflow or underflow. + fn try_from(value: Decimal) -> Result<Self, Self::Error> { + value.0.try_into() + } +} + +impl Display for Decimal { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if self.0.is_sign_negative() { + f.write_str(repr::MINUS_SIGN)?; + } + self.0.abs().fmt(f) + } +} + +impl Repr for Decimal { + fn repr(&self) -> EcoString { + eco_format!("decimal({})", eco_format!("{}", self.0).repr()) + } +} + +impl Neg for Decimal { + type Output = Self; + + fn neg(self) -> Self { + Self(-self.0) + } +} + +impl Hash for Decimal { + fn hash<H: Hasher>(&self, state: &mut H) { + // `rust_decimal`'s Hash implementation normalizes decimals before + // hashing them. This means decimals with different scales but + // equivalent value not only compare equal but also hash equally. Here, + // we hash all bytes explicitly to ensure the scale is also considered. + // This means that 123.314 == 123.31400, but 123.314.hash() != + // 123.31400.hash(). + // + // Note that this implies that equal decimals can have different hashes, + // which might generate problems with certain data structures, such as + // HashSet and HashMap. + self.0.serialize().hash(state); + } +} + +/// A value that can be cast to a decimal. +pub enum ToDecimal { + /// A string with the decimal's representation. + Str(EcoString), + /// An integer to be converted to the equivalent decimal. + Int(i64), + /// A float to be converted to the equivalent decimal. + Float(f64), +} + +cast! { + ToDecimal, + v: i64 => Self::Int(v), + v: f64 => Self::Float(v), + v: Str => Self::Str(EcoString::from(v)), +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use typst_utils::hash128; + + use super::Decimal; + + #[test] + fn test_decimals_with_equal_scales_hash_identically() { + let a = Decimal::from_str("3.14").unwrap(); + let b = Decimal::from_str("3.14").unwrap(); + assert_eq!(a, b); + assert_eq!(hash128(&a), hash128(&b)); + } + + #[test] + fn test_decimals_with_different_scales_hash_differently() { + let a = Decimal::from_str("3.140").unwrap(); + let b = Decimal::from_str("3.14000").unwrap(); + assert_eq!(a, b); + assert_ne!(hash128(&a), hash128(&b)); + } + + #[track_caller] + fn test_round(value: &str, digits: i32, expected: &str) { + assert_eq!( + Decimal::from_str(value).unwrap().round(digits), + Some(Decimal::from_str(expected).unwrap()), + ); + } + + #[test] + fn test_decimal_positive_round() { + test_round("312.55553", 0, "313.00000"); + test_round("312.55553", 3, "312.556"); + test_round("312.5555300000", 3, "312.556"); + test_round("-312.55553", 3, "-312.556"); + test_round("312.55553", 28, "312.55553"); + test_round("312.55553", 2341, "312.55553"); + test_round("-312.55553", 2341, "-312.55553"); + } + + #[test] + fn test_decimal_negative_round() { + test_round("4596.55553", -1, "4600"); + test_round("4596.555530000000", -1, "4600"); + test_round("-4596.55553", -3, "-5000"); + test_round("4596.55553", -28, "0"); + test_round("-4596.55553", -2341, "0"); + assert_eq!(Decimal::MAX.round(-1), None); + } +} diff --git a/crates/typst-library/src/foundations/dict.rs b/crates/typst-library/src/foundations/dict.rs new file mode 100644 index 00000000..e4ab54e7 --- /dev/null +++ b/crates/typst-library/src/foundations/dict.rs @@ -0,0 +1,395 @@ +use std::fmt::{Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::ops::{Add, AddAssign}; +use std::sync::Arc; + +use ecow::{eco_format, EcoString}; +use indexmap::IndexMap; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use typst_syntax::is_ident; +use typst_utils::ArcExt; + +use crate::diag::{Hint, HintedStrResult, StrResult}; +use crate::foundations::{ + array, cast, func, repr, scope, ty, Array, Module, Repr, Str, Value, +}; + +/// 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::foundations::IndexMap::new(); + $(map.insert($key.into(), $crate::foundations::IntoValue::into_value($value));)* + $crate::foundations::Dict::from(map) + }}; +} + +#[doc(inline)] +pub use crate::__dict as dict; + +/// A map from string keys to values. +/// +/// You can construct a dictionary by enclosing comma-separated `key: value` +/// pairs in parentheses. The values do not have to be of the same type. Since +/// empty parentheses already yield an empty array, you have to use the special +/// `(:)` syntax to create an empty dictionary. +/// +/// A dictionary is conceptually similar to an array, but it is indexed by +/// strings instead of integers. You can access and create dictionary entries +/// with the `.at()` method. If you know the key statically, you can +/// alternatively use [field access notation]($scripting/#fields) (`.key`) to +/// access the value. Dictionaries can be added with the `+` operator and +/// [joined together]($scripting/#blocks). To check whether a key is present in +/// the dictionary, use the `in` keyword. +/// +/// You can iterate over the pairs in a dictionary using a [for +/// loop]($scripting/#loops). This will iterate in the order the pairs were +/// inserted / declared. +/// +/// # Example +/// ```example +/// #let dict = ( +/// name: "Typst", +/// born: 2019, +/// ) +/// +/// #dict.name \ +/// #(dict.launch = 20) +/// #dict.len() \ +/// #dict.keys() \ +/// #dict.values() \ +/// #dict.at("born") \ +/// #dict.insert("city", "Berlin ") +/// #("name" in dict) +/// ``` +#[ty(scope, cast, name = "dictionary")] +#[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() + } + + /// Borrow the value at the given key. + pub fn get(&self, key: &str) -> StrResult<&Value> { + self.0.get(key).ok_or_else(|| missing_key(key)) + } + + /// Mutably borrow the value the given `key` maps to. + pub fn at_mut(&mut self, key: &str) -> HintedStrResult<&mut Value> { + Arc::make_mut(&mut self.0) + .get_mut(key) + .ok_or_else(|| missing_key(key)) + .hint("use `insert` to add or update values") + } + + /// 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) + .shift_remove(key) + .ok_or_else(|| missing_key(key)) + } + + /// Whether the dictionary contains a specific key. + pub fn contains(&self, key: &str) -> bool { + self.0.contains_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(); + } + } + + /// Iterate over pairs of references to the contained keys and values. + pub fn iter(&self) -> indexmap::map::Iter<Str, Value> { + self.0.iter() + } + + /// Check if there is any remaining pair, and if so return an + /// "unexpected key" error. + pub fn finish(&self, expected: &[&str]) -> StrResult<()> { + let mut iter = self.iter().peekable(); + if iter.peek().is_none() { + return Ok(()); + } + let unexpected: Vec<&str> = iter.map(|kv| kv.0.as_str()).collect(); + + Err(Self::unexpected_keys(unexpected, Some(expected))) + } + + // Return an "unexpected key" error string. + pub fn unexpected_keys( + unexpected: Vec<&str>, + hint_expected: Option<&[&str]>, + ) -> EcoString { + let format_as_list = |arr: &[&str]| { + repr::separated_list( + &arr.iter().map(|s| eco_format!("\"{s}\"")).collect::<Vec<_>>(), + "and", + ) + }; + + let mut msg = String::from(match unexpected.len() { + 1 => "unexpected key ", + _ => "unexpected keys ", + }); + + msg.push_str(&format_as_list(&unexpected[..])); + + if let Some(expected) = hint_expected { + msg.push_str(", valid keys are "); + msg.push_str(&format_as_list(expected)); + } + + msg.into() + } +} + +#[scope] +impl Dict { + /// Converts a value into a dictionary. + /// + /// Note that this function is only intended for conversion of a + /// dictionary-like value to a dictionary, not for creation of a dictionary + /// from individual pairs. Use the dictionary syntax `(key: value)` instead. + /// + /// ```example + /// #dictionary(sys).at("version") + /// ``` + #[func(constructor)] + pub fn construct( + /// The value that should be converted to a dictionary. + value: ToDict, + ) -> Dict { + value.0 + } + + /// The number of pairs in the dictionary. + #[func(title = "Length")] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns the value associated with the specified key in the dictionary. + /// May be used on the left-hand side of an assignment if the key is already + /// present in the dictionary. Returns the default value if the key is not + /// part of the dictionary or fails with an error if no default value was + /// specified. + #[func] + pub fn at( + &self, + /// The key at which to retrieve the item. + key: Str, + /// A default value to return if the key is not part of the dictionary. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { + self.0 + .get(&key) + .cloned() + .or(default) + .ok_or_else(|| missing_key_no_default(&key)) + } + + /// Inserts a new pair into the dictionary. If the dictionary already + /// contains this key, the value is updated. + #[func] + pub fn insert( + &mut self, + /// The key of the pair that should be inserted. + key: Str, + /// The value of the pair that should be inserted. + value: Value, + ) { + Arc::make_mut(&mut self.0).insert(key, value); + } + + /// Removes a pair from the dictionary by key and return the value. + #[func] + pub fn remove( + &mut self, + /// The key of the pair to remove. + key: Str, + /// A default value to return if the key does not exist. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { + Arc::make_mut(&mut self.0) + .shift_remove(&key) + .or(default) + .ok_or_else(|| missing_key(&key)) + } + + /// Returns the keys of the dictionary as an array in insertion order. + #[func] + pub fn keys(&self) -> Array { + self.0.keys().cloned().map(Value::Str).collect() + } + + /// Returns the values of the dictionary as an array in insertion order. + #[func] + pub fn values(&self) -> Array { + self.0.values().cloned().collect() + } + + /// Returns the keys and values of the dictionary as an array of pairs. Each + /// pair is represented as an array of length two. + #[func] + pub fn pairs(&self) -> Array { + self.0 + .iter() + .map(|(k, v)| Value::Array(array![k.clone(), v.clone()])) + .collect() + } +} + +/// A value that can be cast to dictionary. +pub struct ToDict(Dict); + +cast! { + ToDict, + v: Module => Self(v.scope().iter().map(|(k, v, _)| (Str::from(k.clone()), v.clone())).collect()), +} + +impl Debug for Dict { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_map().entries(self.0.iter()).finish() + } +} + +impl Repr for Dict { + fn repr(&self) -> EcoString { + if self.is_empty() { + return "(:)".into(); + } + + let max = 40; + let mut pieces: Vec<_> = self + .iter() + .take(max) + .map(|(key, value)| { + if is_ident(key) { + eco_format!("{key}: {}", value.repr()) + } else { + eco_format!("{}: {}", key.repr(), value.repr()) + } + }) + .collect(); + + if self.len() > max { + pieces.push(eco_format!(".. ({} pairs omitted)", self.len() - max)); + } + + repr::pretty_array_like(&pieces, false).into() + } +} + +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 Serialize for Dict { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + self.0.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Dict { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + Ok(IndexMap::<Str, Value>::deserialize(deserializer)?.into()) + } +} + +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 {}", key.repr()) +} + +/// The missing key access error message when no default was given. +#[cold] +fn missing_key_no_default(key: &str) -> EcoString { + eco_format!( + "dictionary does not contain key {} \ + and no default value was specified", + key.repr() + ) +} diff --git a/crates/typst-library/src/foundations/duration.rs b/crates/typst-library/src/foundations/duration.rs new file mode 100644 index 00000000..94d44fb2 --- /dev/null +++ b/crates/typst-library/src/foundations/duration.rs @@ -0,0 +1,215 @@ +use std::fmt::{self, Debug, Formatter}; +use std::ops::{Add, Div, Mul, Neg, Sub}; + +use ecow::{eco_format, EcoString}; +use time::ext::NumericalDuration; + +use crate::foundations::{func, repr, scope, ty, Repr}; + +/// Represents a positive or negative span of time. +#[ty(scope, cast)] +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Duration(time::Duration); + +impl Duration { + /// Whether the duration is empty / zero. + pub fn is_zero(&self) -> bool { + self.0.is_zero() + } +} + +#[scope] +impl Duration { + /// Creates a new duration. + /// + /// You can specify the [duration] using weeks, days, hours, minutes and + /// seconds. You can also get a duration by subtracting two + /// [datetimes]($datetime). + /// + /// ```example + /// #duration( + /// days: 3, + /// hours: 12, + /// ).hours() + /// ``` + #[func(constructor)] + pub fn construct( + /// The number of seconds. + #[named] + #[default(0)] + seconds: i64, + /// The number of minutes. + #[named] + #[default(0)] + minutes: i64, + /// The number of hours. + #[named] + #[default(0)] + hours: i64, + /// The number of days. + #[named] + #[default(0)] + days: i64, + /// The number of weeks. + #[named] + #[default(0)] + weeks: i64, + ) -> Duration { + Duration::from( + time::Duration::seconds(seconds) + + time::Duration::minutes(minutes) + + time::Duration::hours(hours) + + time::Duration::days(days) + + time::Duration::weeks(weeks), + ) + } + + /// The duration expressed in seconds. + /// + /// This function returns the total duration represented in seconds as a + /// floating-point number rather than the second component of the duration. + #[func] + pub fn seconds(&self) -> f64 { + self.0.as_seconds_f64() + } + + /// The duration expressed in minutes. + /// + /// This function returns the total duration represented in minutes as a + /// floating-point number rather than the second component of the duration. + #[func] + pub fn minutes(&self) -> f64 { + self.seconds() / 60.0 + } + + /// The duration expressed in hours. + /// + /// This function returns the total duration represented in hours as a + /// floating-point number rather than the second component of the duration. + #[func] + pub fn hours(&self) -> f64 { + self.seconds() / 3_600.0 + } + + /// The duration expressed in days. + /// + /// This function returns the total duration represented in days as a + /// floating-point number rather than the second component of the duration. + #[func] + pub fn days(&self) -> f64 { + self.seconds() / 86_400.0 + } + + /// The duration expressed in weeks. + /// + /// This function returns the total duration represented in weeks as a + /// floating-point number rather than the second component of the duration. + #[func] + pub fn weeks(&self) -> f64 { + self.seconds() / 604_800.0 + } +} + +impl Debug for Duration { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Repr for Duration { + fn repr(&self) -> EcoString { + let mut tmp = self.0; + let mut vec = Vec::with_capacity(5); + + let weeks = tmp.whole_seconds() / 604_800.0 as i64; + if weeks != 0 { + vec.push(eco_format!("weeks: {}", weeks.repr())); + } + tmp -= weeks.weeks(); + + let days = tmp.whole_days(); + if days != 0 { + vec.push(eco_format!("days: {}", days.repr())); + } + tmp -= days.days(); + + let hours = tmp.whole_hours(); + if hours != 0 { + vec.push(eco_format!("hours: {}", hours.repr())); + } + tmp -= hours.hours(); + + let minutes = tmp.whole_minutes(); + if minutes != 0 { + vec.push(eco_format!("minutes: {}", minutes.repr())); + } + tmp -= minutes.minutes(); + + let seconds = tmp.whole_seconds(); + if seconds != 0 { + vec.push(eco_format!("seconds: {}", seconds.repr())); + } + + eco_format!("duration{}", &repr::pretty_array_like(&vec, false)) + } +} + +impl From<time::Duration> for Duration { + fn from(value: time::Duration) -> Self { + Self(value) + } +} + +impl From<Duration> for time::Duration { + fn from(value: Duration) -> Self { + value.0 + } +} + +impl Add for Duration { + type Output = Duration; + + fn add(self, rhs: Self) -> Self::Output { + Duration(self.0 + rhs.0) + } +} + +impl Sub for Duration { + type Output = Duration; + + fn sub(self, rhs: Self) -> Self::Output { + Duration(self.0 - rhs.0) + } +} + +impl Neg for Duration { + type Output = Duration; + + fn neg(self) -> Self::Output { + Duration(-self.0) + } +} + +impl Mul<f64> for Duration { + type Output = Duration; + + fn mul(self, rhs: f64) -> Self::Output { + Duration(self.0 * rhs) + } +} + +impl Div<f64> for Duration { + type Output = Duration; + + fn div(self, rhs: f64) -> Self::Output { + Duration(self.0 / rhs) + } +} + +impl Div for Duration { + type Output = f64; + + fn div(self, rhs: Self) -> Self::Output { + self.0 / rhs.0 + } +} diff --git a/crates/typst-library/src/foundations/element.rs b/crates/typst-library/src/foundations/element.rs new file mode 100644 index 00000000..8da71965 --- /dev/null +++ b/crates/typst-library/src/foundations/element.rs @@ -0,0 +1,333 @@ +use std::any::TypeId; +use std::cmp::Ordering; +use std::fmt::{self, Debug}; +use std::hash::Hash; +use std::ptr::NonNull; + +use ecow::EcoString; +use once_cell::sync::Lazy; +use smallvec::SmallVec; +#[doc(inline)] +pub use typst_macros::elem; +use typst_utils::Static; + +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{ + cast, Args, Content, Dict, FieldAccessError, Func, ParamInfo, Repr, Scope, Selector, + StyleChain, Styles, Value, +}; +use crate::text::{Lang, Region}; + +/// A document element. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct Element(Static<NativeElementData>); + +impl Element { + /// Get the element for `T`. + pub fn of<T: NativeElement>() -> Self { + T::elem() + } + + /// The element's normal name (e.g. `enum`). + pub fn name(self) -> &'static str { + self.0.name + } + + /// The element's title case name, for use in documentation + /// (e.g. `Numbered List`). + pub fn title(&self) -> &'static str { + self.0.title + } + + /// Documentation for the element (as Markdown). + pub fn docs(&self) -> &'static str { + self.0.docs + } + + /// Search keywords for the element. + pub fn keywords(&self) -> &'static [&'static str] { + self.0.keywords + } + + /// Construct an instance of this element. + pub fn construct( + self, + engine: &mut Engine, + args: &mut Args, + ) -> SourceResult<Content> { + (self.0.construct)(engine, args) + } + + /// Execute the set rule for the element and return the resulting style map. + pub fn set(self, engine: &mut Engine, mut args: Args) -> SourceResult<Styles> { + let styles = (self.0.set)(engine, &mut args)?; + args.finish()?; + Ok(styles) + } + + /// Whether the element has the given capability. + pub fn can<C>(self) -> bool + where + C: ?Sized + 'static, + { + self.can_type_id(TypeId::of::<C>()) + } + + /// Whether the element has the given capability where the capability is + /// given by a `TypeId`. + pub fn can_type_id(self, type_id: TypeId) -> bool { + (self.0.vtable)(type_id).is_some() + } + + /// The VTable for capabilities dispatch. + pub fn vtable(self) -> fn(of: TypeId) -> Option<NonNull<()>> { + self.0.vtable + } + + /// Create a selector for this element. + pub fn select(self) -> Selector { + Selector::Elem(self, None) + } + + /// Create a selector for this element, filtering for those that + /// [fields](crate::foundations::Content::field) match the given argument. + pub fn where_(self, fields: SmallVec<[(u8, Value); 1]>) -> Selector { + Selector::Elem(self, Some(fields)) + } + + /// The element's associated scope of sub-definition. + pub fn scope(&self) -> &'static Scope { + &(self.0).0.scope + } + + /// Details about the element's fields. + pub fn params(&self) -> &'static [ParamInfo] { + &(self.0).0.params + } + + /// Extract the field ID for the given field name. + pub fn field_id(&self, name: &str) -> Option<u8> { + if name == "label" { + return Some(255); + } + (self.0.field_id)(name) + } + + /// Extract the field name for the given field ID. + pub fn field_name(&self, id: u8) -> Option<&'static str> { + if id == 255 { + return Some("label"); + } + (self.0.field_name)(id) + } + + /// Extract the value of the field for the given field ID and style chain. + pub fn field_from_styles( + &self, + id: u8, + styles: StyleChain, + ) -> Result<Value, FieldAccessError> { + (self.0.field_from_styles)(id, styles) + } + + /// The element's local name, if any. + pub fn local_name(&self, lang: Lang, region: Option<Region>) -> Option<&'static str> { + (self.0).0.local_name.map(|f| f(lang, region)) + } +} + +impl Debug for Element { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Element({})", self.name()) + } +} + +impl Repr for Element { + fn repr(&self) -> EcoString { + self.name().into() + } +} + +impl Ord for Element { + fn cmp(&self, other: &Self) -> Ordering { + self.name().cmp(other.name()) + } +} + +impl PartialOrd for Element { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +cast! { + Element, + self => Value::Func(self.into()), + v: Func => v.element().ok_or("expected element")?, +} + +/// A Typst element that is defined by a native Rust type. +pub trait NativeElement: + Debug + + Clone + + PartialEq + + Hash + + Construct + + Set + + Capable + + Fields + + Repr + + Send + + Sync + + 'static +{ + /// Get the element for the native Rust element. + fn elem() -> Element + where + Self: Sized, + { + Element::from(Self::data()) + } + + /// Pack the element into type-erased content. + fn pack(self) -> Content + where + Self: Sized, + { + Content::new(self) + } + + /// Get the element data for the native Rust element. + fn data() -> &'static NativeElementData + where + Self: Sized; +} + +/// Used to cast an element to a trait object for a trait it implements. +/// +/// # Safety +/// If the `vtable` function returns `Some(p)`, then `p` must be a valid pointer +/// to a vtable of `Packed<Self>` w.r.t to the trait `C` where `capability` is +/// `TypeId::of::<dyn C>()`. +pub unsafe trait Capable { + /// Get the pointer to the vtable for the given capability / trait. + fn vtable(capability: TypeId) -> Option<NonNull<()>>; +} + +/// Defines how fields of an element are accessed. +pub trait Fields { + /// An enum with the fields of the element. + type Enum + where + Self: Sized; + + /// Whether the element has the given field set. + fn has(&self, id: u8) -> bool; + + /// Get the field with the given field ID. + fn field(&self, id: u8) -> Result<Value, FieldAccessError>; + + /// Get the field with the given ID in the presence of styles. + fn field_with_styles( + &self, + id: u8, + styles: StyleChain, + ) -> Result<Value, FieldAccessError>; + + /// Get the field with the given ID from the styles. + fn field_from_styles(id: u8, styles: StyleChain) -> Result<Value, FieldAccessError> + where + Self: Sized; + + /// Resolve all fields with the styles and save them in-place. + fn materialize(&mut self, styles: StyleChain); + + /// Get the fields of the element. + fn fields(&self) -> Dict; +} + +/// 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(engine: &mut Engine, args: &mut Args) -> SourceResult<Content> + where + Self: Sized; +} + +/// An element's set rule. +pub trait Set { + /// Parse relevant arguments into style properties for this element. + fn set(engine: &mut Engine, args: &mut Args) -> SourceResult<Styles> + where + Self: Sized; +} + +/// Defines a native element. +#[derive(Debug)] +pub struct NativeElementData { + /// The element's normal name (e.g. `align`), as exposed to Typst. + pub name: &'static str, + /// The element's title case name (e.g. `Align`). + pub title: &'static str, + /// The documentation for this element as a string. + pub docs: &'static str, + /// A list of alternate search terms for this element. + pub keywords: &'static [&'static str], + /// The constructor for this element (see [`Construct`]). + pub construct: fn(&mut Engine, &mut Args) -> SourceResult<Content>, + /// Executes this element's set rule (see [`Set`]). + pub set: fn(&mut Engine, &mut Args) -> SourceResult<Styles>, + /// Gets the vtable for one of this element's capabilities + /// (see [`Capable`]). + pub vtable: fn(capability: TypeId) -> Option<NonNull<()>>, + /// Gets the numeric index of this field by its name. + pub field_id: fn(name: &str) -> Option<u8>, + /// Gets the name of a field by its numeric index. + pub field_name: fn(u8) -> Option<&'static str>, + /// Get the field with the given ID in the presence of styles (see [`Fields`]). + pub field_from_styles: fn(u8, StyleChain) -> Result<Value, FieldAccessError>, + /// Gets the localized name for this element (see [`LocalName`][crate::text::LocalName]). + pub local_name: Option<fn(Lang, Option<Region>) -> &'static str>, + pub scope: Lazy<Scope>, + /// A list of parameter information for each field. + pub params: Lazy<Vec<ParamInfo>>, +} + +impl From<&'static NativeElementData> for Element { + fn from(data: &'static NativeElementData) -> Self { + Self(Static(data)) + } +} + +cast! { + &'static NativeElementData, + self => Element::from(self).into_value(), +} + +/// 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, engine: &mut Engine, styles: StyleChain) + -> SourceResult<()>; +} + +/// Defines a built-in show rule for an element. +pub trait Show { + /// Execute the base recipe for this element. + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content>; +} + +/// Defines built-in show set rules for an element. +/// +/// This is a bit more powerful than a user-defined show-set because it can +/// access the element's fields. +pub trait ShowSet { + /// 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 show_set(&self, styles: StyleChain) -> Styles; +} diff --git a/crates/typst-library/src/foundations/fields.rs b/crates/typst-library/src/foundations/fields.rs new file mode 100644 index 00000000..422f30b8 --- /dev/null +++ b/crates/typst-library/src/foundations/fields.rs @@ -0,0 +1,91 @@ +//! Fields on values. + +use ecow::{eco_format, EcoString}; + +use crate::diag::StrResult; +use crate::foundations::{IntoValue, Type, Value, Version}; +use crate::layout::{Alignment, Length, Rel}; +use crate::visualize::Stroke; + +/// Try to access a field on a value. +/// +/// This function is exclusively for types which have predefined fields, such as +/// stroke and length. +pub(crate) fn field(value: &Value, field: &str) -> StrResult<Value> { + let ty = value.ty(); + let nope = || Err(no_fields(ty)); + let missing = || Err(missing_field(ty, field)); + + // Special cases, such as module and dict, are handled by Value itself + let result = match value { + Value::Version(version) => match version.component(field) { + Ok(i) => i.into_value(), + Err(_) => return missing(), + }, + Value::Length(length) => match field { + "em" => length.em.get().into_value(), + "abs" => length.abs.into_value(), + _ => return missing(), + }, + Value::Relative(rel) => match field { + "ratio" => rel.rel.into_value(), + "length" => rel.abs.into_value(), + _ => return missing(), + }, + Value::Dyn(dynamic) => { + if let Some(stroke) = dynamic.downcast::<Stroke>() { + match field { + "paint" => stroke.paint.clone().into_value(), + "thickness" => stroke.thickness.into_value(), + "cap" => stroke.cap.into_value(), + "join" => stroke.join.into_value(), + "dash" => stroke.dash.clone().into_value(), + "miter-limit" => { + stroke.miter_limit.map(|limit| limit.get()).into_value() + } + _ => return missing(), + } + } else if let Some(align) = dynamic.downcast::<Alignment>() { + match field { + "x" => align.x().into_value(), + "y" => align.y().into_value(), + _ => return missing(), + } + } else { + return nope(); + } + } + _ => return nope(), + }; + + Ok(result) +} + +/// The error message for a type not supporting field access. +#[cold] +fn no_fields(ty: Type) -> EcoString { + eco_format!("cannot access fields on type {ty}") +} + +/// The missing field error message. +#[cold] +fn missing_field(ty: Type, field: &str) -> EcoString { + eco_format!("{ty} does not contain field \"{field}\"") +} + +/// List the available fields for a type. +pub fn fields_on(ty: Type) -> &'static [&'static str] { + if ty == Type::of::<Version>() { + &Version::COMPONENTS + } else if ty == Type::of::<Length>() { + &["em", "abs"] + } else if ty == Type::of::<Rel>() { + &["ratio", "length"] + } else if ty == Type::of::<Stroke>() { + &["paint", "thickness", "cap", "join", "dash", "miter-limit"] + } else if ty == Type::of::<Alignment>() { + &["x", "y"] + } else { + &[] + } +} diff --git a/crates/typst-library/src/foundations/float.rs b/crates/typst-library/src/foundations/float.rs new file mode 100644 index 00000000..bb3232ee --- /dev/null +++ b/crates/typst-library/src/foundations/float.rs @@ -0,0 +1,190 @@ +use std::num::ParseFloatError; + +use ecow::{eco_format, EcoString}; + +use crate::diag::{bail, StrResult}; +use crate::foundations::{ + cast, func, repr, scope, ty, Bytes, Decimal, Endianness, Repr, Str, +}; +use crate::layout::Ratio; + +/// A floating-point number. +/// +/// A limited-precision representation of a real number. Typst uses 64 bits to +/// store floats. Wherever a float is expected, you can also pass an +/// [integer]($int). +/// +/// You can convert a value to a float with this type's constructor. +/// +/// NaN and positive infinity are available as `{float.nan}` and `{float.inf}` +/// respectively. +/// +/// # Example +/// ```example +/// #3.14 \ +/// #1e4 \ +/// #(10 / 4) +/// ``` +#[ty(scope, cast, name = "float")] +type f64; + +#[scope] +impl f64 { + /// Positive infinity. + const INF: f64 = f64::INFINITY; + + /// A NaN value, as defined by the + /// [IEEE 754 standard](https://en.wikipedia.org/wiki/IEEE_754). + const NAN: f64 = f64::NAN; + + /// Converts a value to a float. + /// + /// - Booleans are converted to `0.0` or `1.0`. + /// - Integers are converted to the closest 64-bit float. For integers with + /// absolute value less than `{calc.pow(2, 53)}`, this conversion is + /// exact. + /// - Ratios are divided by 100%. + /// - Strings are parsed in base 10 to the closest 64-bit float. Exponential + /// notation is supported. + /// + /// ```example + /// #float(false) \ + /// #float(true) \ + /// #float(4) \ + /// #float(40%) \ + /// #float("2.7") \ + /// #float("1e5") + /// ``` + #[func(constructor)] + pub fn construct( + /// The value that should be converted to a float. + value: ToFloat, + ) -> f64 { + value.0 + } + + /// Checks if a float is not a number. + /// + /// In IEEE 754, more than one bit pattern represents a NaN. This function + /// returns `true` if the float is any of those bit patterns. + /// + /// ```example + /// #float.is-nan(0) \ + /// #float.is-nan(1) \ + /// #float.is-nan(float.nan) + /// ``` + #[func] + pub fn is_nan(self) -> bool { + f64::is_nan(self) + } + + /// Checks if a float is infinite. + /// + /// Floats can represent positive infinity and negative infinity. This + /// function returns `{true}` if the float is an infinity. + /// + /// ```example + /// #float.is-infinite(0) \ + /// #float.is-infinite(1) \ + /// #float.is-infinite(float.inf) + /// ``` + #[func] + pub fn is_infinite(self) -> bool { + f64::is_infinite(self) + } + + /// Calculates the sign of a floating point number. + /// + /// - If the number is positive (including `{+0.0}`), returns `{1.0}`. + /// - If the number is negative (including `{-0.0}`), returns `{-1.0}`. + /// - If the number is NaN, returns `{float.nan}`. + /// + /// ```example + /// #(5.0).signum() \ + /// #(-5.0).signum() \ + /// #(0.0).signum() \ + /// #float.nan.signum() + /// ``` + #[func] + pub fn signum(self) -> f64 { + f64::signum(self) + } + + /// Converts bytes to a float. + /// + /// ```example + /// #float.from-bytes(bytes((0, 0, 0, 0, 0, 0, 240, 63))) \ + /// #float.from-bytes(bytes((63, 240, 0, 0, 0, 0, 0, 0)), endian: "big") + /// ``` + #[func] + pub fn from_bytes( + /// The bytes that should be converted to a float. + /// + /// Must be of length exactly 8 so that the result fits into a 64-bit + /// float. + bytes: Bytes, + /// The endianness of the conversion. + #[named] + #[default(Endianness::Little)] + endian: Endianness, + ) -> StrResult<f64> { + // Convert slice to an array of length 8. + let buf: [u8; 8] = match bytes.as_ref().try_into() { + Ok(buffer) => buffer, + Err(_) => bail!("bytes must have a length of exactly 8"), + }; + + Ok(match endian { + Endianness::Little => f64::from_le_bytes(buf), + Endianness::Big => f64::from_be_bytes(buf), + }) + } + + /// Converts a float to bytes. + /// + /// ```example + /// #array(1.0.to-bytes(endian: "big")) \ + /// #array(1.0.to-bytes()) + /// ``` + #[func] + pub fn to_bytes( + self, + /// The endianness of the conversion. + #[named] + #[default(Endianness::Little)] + endian: Endianness, + ) -> Bytes { + match endian { + Endianness::Little => self.to_le_bytes(), + Endianness::Big => self.to_be_bytes(), + } + .as_slice() + .into() + } +} + +impl Repr for f64 { + fn repr(&self) -> EcoString { + repr::format_float(*self, None, true, "") + } +} + +/// A value that can be cast to a float. +pub struct ToFloat(f64); + +cast! { + ToFloat, + v: f64 => Self(v), + v: bool => Self(v as i64 as f64), + v: i64 => Self(v as f64), + v: Decimal => Self(f64::try_from(v).map_err(|_| eco_format!("invalid float: {}", v))?), + v: Ratio => Self(v.get()), + v: Str => Self( + parse_float(v.clone().into()) + .map_err(|_| eco_format!("invalid float: {}", v))? + ), +} + +fn parse_float(s: EcoString) -> Result<f64, ParseFloatError> { + s.replace(repr::MINUS_SIGN, "-").parse() +} diff --git a/crates/typst-library/src/foundations/func.rs b/crates/typst-library/src/foundations/func.rs new file mode 100644 index 00000000..1b40714b --- /dev/null +++ b/crates/typst-library/src/foundations/func.rs @@ -0,0 +1,541 @@ +#[doc(inline)] +pub use typst_macros::func; + +use std::fmt::{self, Debug, Formatter}; +use std::sync::Arc; + +use comemo::{Tracked, TrackedMut}; +use ecow::{eco_format, EcoString}; +use once_cell::sync::Lazy; +use typst_syntax::{ast, Span, SyntaxNode}; +use typst_utils::{singleton, LazyHash, Static}; + +use crate::diag::{bail, SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, repr, scope, ty, Args, CastInfo, Content, Context, Element, IntoArgs, Scope, + Selector, Type, Value, +}; + +/// A mapping from argument values to a return value. +/// +/// You can call a function by writing a comma-separated list of function +/// _arguments_ enclosed in parentheses directly after the function name. +/// Additionally, you can pass any number of trailing content blocks arguments +/// to a function _after_ the normal argument list. If the normal argument list +/// would become empty, it can be omitted. Typst supports positional and named +/// arguments. The former are identified by position and type, while the latter +/// are written as `name: value`. +/// +/// Within math mode, function calls have special behaviour. See the +/// [math documentation]($category/math) for more details. +/// +/// # Example +/// ```example +/// // Call a function. +/// #list([A], [B]) +/// +/// // Named arguments and trailing +/// // content blocks. +/// #enum(start: 2)[A][B] +/// +/// // Version without parentheses. +/// #list[A][B] +/// ``` +/// +/// Functions are a fundamental building block of Typst. Typst provides +/// functions for a variety of typesetting tasks. Moreover, the markup you write +/// is backed by functions and all styling happens through functions. This +/// reference lists all available functions and how you can use them. Please +/// also refer to the documentation about [set]($styling/#set-rules) and +/// [show]($styling/#show-rules) rules to learn about additional ways you can +/// work with functions in Typst. +/// +/// # Element functions +/// Some functions are associated with _elements_ like [headings]($heading) or +/// [tables]($table). When called, these create an element of their respective +/// kind. In contrast to normal functions, they can further be used in [set +/// rules]($styling/#set-rules), [show rules]($styling/#show-rules), and +/// [selectors]($selector). +/// +/// # Function scopes +/// Functions can hold related definitions in their own scope, similar to a +/// [module]($scripting/#modules). Examples of this are +/// [`assert.eq`]($assert.eq) or [`list.item`]($list.item). However, this +/// feature is currently only available for built-in functions. +/// +/// # Defining functions +/// You can define your own function with a [let binding]($scripting/#bindings) +/// that has a parameter list after the binding's name. The parameter list can +/// contain mandatory positional parameters, named parameters with default +/// values and [argument sinks]($arguments). +/// +/// The right-hand side of a function binding is the function body, which can be +/// a block or any other expression. It defines the function's return value and +/// can depend on the parameters. If the function body is a [code +/// block]($scripting/#blocks), the return value is the result of joining the +/// values of each expression in the block. +/// +/// Within a function body, the `return` keyword can be used to exit early and +/// optionally specify a return value. If no explicit return value is given, the +/// body evaluates to the result of joining all expressions preceding the +/// `return`. +/// +/// Functions that don't return any meaningful value return [`none`] instead. +/// The return type of such functions is not explicitly specified in the +/// documentation. (An example of this is [`array.push`]). +/// +/// ```example +/// #let alert(body, fill: red) = { +/// set text(white) +/// set align(center) +/// rect( +/// fill: fill, +/// inset: 8pt, +/// radius: 4pt, +/// [*Warning:\ #body*], +/// ) +/// } +/// +/// #alert[ +/// Danger is imminent! +/// ] +/// +/// #alert(fill: blue)[ +/// KEEP OFF TRACKS +/// ] +/// ``` +/// +/// # Importing functions +/// Functions can be imported from one file ([`module`]($scripting/#modules)) into +/// another using `{import}`. For example, assume that we have defined the `alert` +/// function from the previous example in a file called `foo.typ`. We can import +/// it into another file by writing `{import "foo.typ": alert}`. +/// +/// # Unnamed functions { #unnamed } +/// You can also created an unnamed function without creating a binding by +/// specifying a parameter list followed by `=>` and the function body. If your +/// function has just one parameter, the parentheses around the parameter list +/// are optional. Unnamed functions are mainly useful for show rules, but also +/// for settable properties that take functions like the page function's +/// [`footer`]($page.footer) property. +/// +/// ```example +/// #show "once?": it => [#it #it] +/// once? +/// ``` +/// +/// # Note on function purity +/// In Typst, all functions are _pure._ This means that for the same +/// arguments, they always return the same result. They cannot "remember" things to +/// produce another value when they are called a second time. +/// +/// The only exception are built-in methods like +/// [`array.push(value)`]($array.push). These can modify the values they are +/// called on. +#[ty(scope, cast, name = "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<NativeFuncData>), + /// A function for an element. + Element(Element), + /// A user-defined closure. + Closure(Arc<LazyHash<Closure>>), + /// A nested function with pre-applied arguments. + With(Arc<(Func, Args)>), +} + +impl Func { + /// The function's name (e.g. `min`). + /// + /// Returns `None` if this is an anonymous closure. + pub fn name(&self) -> Option<&str> { + match &self.repr { + Repr::Native(native) => Some(native.name), + Repr::Element(elem) => Some(elem.name()), + Repr::Closure(closure) => closure.name(), + Repr::With(with) => with.0.name(), + } + } + + /// The function's title case name, for use in documentation (e.g. `Minimum`). + /// + /// Returns `None` if this is a closure. + pub fn title(&self) -> Option<&'static str> { + match &self.repr { + Repr::Native(native) => Some(native.title), + Repr::Element(elem) => Some(elem.title()), + Repr::Closure(_) => None, + Repr::With(with) => with.0.title(), + } + } + + /// Documentation for the function (as Markdown). + pub fn docs(&self) -> Option<&'static str> { + match &self.repr { + Repr::Native(native) => Some(native.docs), + Repr::Element(elem) => Some(elem.docs()), + Repr::Closure(_) => None, + Repr::With(with) => with.0.docs(), + } + } + + /// Whether the function is known to be contextual. + pub fn contextual(&self) -> Option<bool> { + match &self.repr { + Repr::Native(native) => Some(native.contextual), + _ => None, + } + } + + /// Get details about this function's parameters if available. + pub fn params(&self) -> Option<&'static [ParamInfo]> { + match &self.repr { + Repr::Native(native) => Some(&native.0.params), + Repr::Element(elem) => Some(elem.params()), + Repr::Closure(_) => None, + Repr::With(with) => with.0.params(), + } + } + + /// Get the parameter info for a parameter with the given name if it exist. + pub fn param(&self, name: &str) -> Option<&'static ParamInfo> { + self.params()?.iter().find(|param| param.name == name) + } + + /// Get details about the function's return type. + pub fn returns(&self) -> Option<&'static CastInfo> { + match &self.repr { + Repr::Native(native) => Some(&native.0.returns), + Repr::Element(_) => { + Some(singleton!(CastInfo, CastInfo::Type(Type::of::<Content>()))) + } + Repr::Closure(_) => None, + Repr::With(with) => with.0.returns(), + } + } + + /// Search keywords for the function. + pub fn keywords(&self) -> &'static [&'static str] { + match &self.repr { + Repr::Native(native) => native.keywords, + Repr::Element(elem) => elem.keywords(), + Repr::Closure(_) => &[], + Repr::With(with) => with.0.keywords(), + } + } + + /// The function's associated scope of sub-definition. + pub fn scope(&self) -> Option<&'static Scope> { + match &self.repr { + Repr::Native(native) => Some(&native.0.scope), + Repr::Element(elem) => Some(elem.scope()), + Repr::Closure(_) => None, + Repr::With(with) => with.0.scope(), + } + } + + /// Get a field from this function's scope, if possible. + pub fn field(&self, field: &str) -> StrResult<&'static Value> { + let scope = + self.scope().ok_or("cannot access fields on user-defined functions")?; + match scope.get(field) { + Some(field) => Ok(field), + None => match self.name() { + Some(name) => bail!("function `{name}` does not contain field `{field}`"), + None => bail!("function does not contain field `{field}`"), + }, + } + } + + /// Extract the element function, if it is one. + pub fn element(&self) -> Option<Element> { + match self.repr { + Repr::Element(func) => Some(func), + _ => None, + } + } + + /// Call the function with the given context and arguments. + pub fn call<A: IntoArgs>( + &self, + engine: &mut Engine, + context: Tracked<Context>, + args: A, + ) -> SourceResult<Value> { + self.call_impl(engine, context, args.into_args(self.span)) + } + + /// Non-generic implementation of `call`. + #[typst_macros::time(name = "func call", span = self.span())] + fn call_impl( + &self, + engine: &mut Engine, + context: Tracked<Context>, + mut args: Args, + ) -> SourceResult<Value> { + match &self.repr { + Repr::Native(native) => { + let value = (native.function)(engine, context, &mut args)?; + args.finish()?; + Ok(value) + } + Repr::Element(func) => { + let value = func.construct(engine, &mut args)?; + args.finish()?; + Ok(Value::Content(value)) + } + Repr::Closure(closure) => (engine.routines.eval_closure)( + self, + closure, + engine.routines, + engine.world, + engine.introspector, + engine.traced, + TrackedMut::reborrow_mut(&mut engine.sink), + engine.route.track(), + context, + args, + ), + Repr::With(with) => { + args.items = with.1.items.iter().cloned().chain(args.items).collect(); + with.0.call(engine, context, args) + } + } + } + + /// 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 + } +} + +#[scope] +impl Func { + /// Returns a new function that has the given arguments pre-applied. + #[func] + pub fn with( + self, + /// The real arguments (the other argument is just for the docs). + /// The docs argument cannot be called `args`. + args: &mut Args, + /// The arguments to apply to the function. + #[external] + #[variadic] + arguments: Vec<Value>, + ) -> Func { + let span = self.span; + Self { + repr: Repr::With(Arc::new((self, args.take()))), + span, + } + } + + /// Returns a selector that filters for elements belonging to this function + /// whose fields have the values of the given arguments. + /// + /// ```example + /// #show heading.where(level: 2): set text(blue) + /// = Section + /// == Subsection + /// === Sub-subsection + /// ``` + #[func] + pub fn where_( + self, + /// The real arguments (the other argument is just for the docs). + /// The docs argument cannot be called `args`. + args: &mut Args, + /// The fields to filter for. + #[variadic] + #[external] + fields: Vec<Value>, + ) -> StrResult<Selector> { + let fields = args.to_named(); + args.items.retain(|arg| arg.name.is_none()); + + let element = self + .element() + .ok_or("`where()` can only be called on element functions")?; + + let fields = fields + .into_iter() + .map(|(key, value)| { + element.field_id(&key).map(|id| (id, value)).ok_or_else(|| { + eco_format!( + "element `{}` does not have field `{}`", + element.name(), + key + ) + }) + }) + .collect::<StrResult<smallvec::SmallVec<_>>>()?; + + Ok(element.where_(fields)) + } +} + +impl Debug for Func { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Func({})", self.name().unwrap_or("..")) + } +} + +impl repr::Repr for Func { + fn repr(&self) -> EcoString { + match self.name() { + Some(name) => name.into(), + None => "(..) => ..".into(), + } + } +} + +impl PartialEq for Func { + fn eq(&self, other: &Self) -> bool { + self.repr == other.repr + } +} + +impl PartialEq<&NativeFuncData> for Func { + fn eq(&self, other: &&NativeFuncData) -> bool { + match &self.repr { + Repr::Native(native) => native.function == other.function, + _ => false, + } + } +} + +impl From<Repr> for Func { + fn from(repr: Repr) -> Self { + Self { repr, span: Span::detached() } + } +} + +impl From<Element> for Func { + fn from(func: Element) -> Self { + Repr::Element(func).into() + } +} + +/// A Typst function that is defined by a native Rust type that shadows a +/// native Rust function. +pub trait NativeFunc { + /// Get the function for the native Rust type. + fn func() -> Func { + Func::from(Self::data()) + } + + /// Get the function data for the native Rust type. + fn data() -> &'static NativeFuncData; +} + +/// Defines a native function. +#[derive(Debug)] +pub struct NativeFuncData { + /// Invokes the function from Typst. + pub function: fn(&mut Engine, Tracked<Context>, &mut Args) -> SourceResult<Value>, + /// The function's normal name (e.g. `align`), as exposed to Typst. + pub name: &'static str, + /// The function's title case name (e.g. `Align`). + pub title: &'static str, + /// The documentation for this function as a string. + pub docs: &'static str, + /// A list of alternate search terms for this function. + pub keywords: &'static [&'static str], + /// Whether this function makes use of context. + pub contextual: bool, + pub scope: Lazy<Scope>, + /// A list of parameter information for each parameter. + pub params: Lazy<Vec<ParamInfo>>, + /// Information about the return value of this function. + pub returns: Lazy<CastInfo>, +} + +impl From<&'static NativeFuncData> for Func { + fn from(data: &'static NativeFuncData) -> Self { + Repr::Native(Static(data)).into() + } +} + +cast! { + &'static NativeFuncData, + self => Func::from(self).into_value(), +} + +/// Describes a function parameter. +#[derive(Debug, Clone)] +pub struct ParamInfo { + /// The parameter's name. + pub name: &'static str, + /// Documentation for the parameter. + pub docs: &'static str, + /// Describe what values this parameter accepts. + pub input: 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(Debug, Hash)] +pub struct Closure { + /// The closure's syntax node. Must be either castable to `ast::Closure` or + /// `ast::Expr`. In the latter case, this is a synthesized closure without + /// any parameters (used by `context` expressions). + pub node: SyntaxNode, + /// Default values of named parameters. + pub defaults: Vec<Value>, + /// Captured values from outer scopes. + pub captured: Scope, + /// The number of positional parameters in the closure. + pub num_pos_params: usize, +} + +impl Closure { + /// The name of the closure. + pub fn name(&self) -> Option<&str> { + self.node.cast::<ast::Closure>()?.name().map(|ident| ident.as_str()) + } +} + +impl From<Closure> for Func { + fn from(closure: Closure) -> Self { + Repr::Closure(Arc::new(LazyHash::new(closure))).into() + } +} + +cast! { + Closure, + self => Value::Func(self.into()), +} diff --git a/crates/typst-library/src/foundations/int.rs b/crates/typst-library/src/foundations/int.rs new file mode 100644 index 00000000..e936353c --- /dev/null +++ b/crates/typst-library/src/foundations/int.rs @@ -0,0 +1,478 @@ +use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError}; + +use ecow::{eco_format, EcoString}; + +use crate::diag::{bail, StrResult}; +use crate::foundations::{ + cast, func, repr, scope, ty, Bytes, Cast, Decimal, Repr, Str, Value, +}; + +/// A whole number. +/// +/// The number can be negative, zero, or positive. As Typst uses 64 bits to +/// store integers, integers cannot be smaller than `{-9223372036854775808}` or +/// larger than `{9223372036854775807}`. +/// +/// The number can also be specified as hexadecimal, octal, or binary by +/// starting it with a zero followed by either `x`, `o`, or `b`. +/// +/// You can convert a value to an integer with this type's constructor. +/// +/// # Example +/// ```example +/// #(1 + 2) \ +/// #(2 - 5) \ +/// #(3 + 4 < 8) +/// +/// #0xff \ +/// #0o10 \ +/// #0b1001 +/// ``` +#[ty(scope, cast, name = "int", title = "Integer")] +type i64; + +#[scope] +impl i64 { + /// Converts a value to an integer. Raises an error if there is an attempt + /// to produce an integer larger than the maximum 64-bit signed integer + /// or smaller than the minimum 64-bit signed integer. + /// + /// - Booleans are converted to `0` or `1`. + /// - Floats and decimals are truncated to the next 64-bit integer. + /// - Strings are parsed in base 10. + /// + /// ```example + /// #int(false) \ + /// #int(true) \ + /// #int(2.7) \ + /// #int(decimal("3.8")) \ + /// #(int("27") + int("4")) + /// ``` + #[func(constructor)] + pub fn construct( + /// The value that should be converted to an integer. + value: ToInt, + ) -> i64 { + value.0 + } + + /// Calculates the sign of an integer. + /// + /// - If the number is positive, returns `{1}`. + /// - If the number is negative, returns `{-1}`. + /// - If the number is zero, returns `{0}`. + /// + /// ```example + /// #(5).signum() \ + /// #(-5).signum() \ + /// #(0).signum() + /// ``` + #[func] + pub fn signum(self) -> i64 { + i64::signum(self) + } + + /// Calculates the bitwise NOT of an integer. + /// + /// For the purposes of this function, the operand is treated as a signed + /// integer of 64 bits. + /// + /// ```example + /// #4.bit-not() \ + /// #(-1).bit-not() + /// ``` + #[func(title = "Bitwise NOT")] + pub fn bit_not(self) -> i64 { + !self + } + + /// Calculates the bitwise AND between two integers. + /// + /// For the purposes of this function, the operands are treated as signed + /// integers of 64 bits. + /// + /// ```example + /// #128.bit-and(192) + /// ``` + #[func(title = "Bitwise AND")] + pub fn bit_and( + self, + /// The right-hand operand of the bitwise AND. + rhs: i64, + ) -> i64 { + self & rhs + } + + /// Calculates the bitwise OR between two integers. + /// + /// For the purposes of this function, the operands are treated as signed + /// integers of 64 bits. + /// + /// ```example + /// #64.bit-or(32) + /// ``` + #[func(title = "Bitwise OR")] + pub fn bit_or( + self, + /// The right-hand operand of the bitwise OR. + rhs: i64, + ) -> i64 { + self | rhs + } + + /// Calculates the bitwise XOR between two integers. + /// + /// For the purposes of this function, the operands are treated as signed + /// integers of 64 bits. + /// + /// ```example + /// #64.bit-xor(96) + /// ``` + #[func(title = "Bitwise XOR")] + pub fn bit_xor( + self, + /// The right-hand operand of the bitwise XOR. + rhs: i64, + ) -> i64 { + self ^ rhs + } + + /// Shifts the operand's bits to the left by the specified amount. + /// + /// For the purposes of this function, the operand is treated as a signed + /// integer of 64 bits. An error will occur if the result is too large to + /// fit in a 64-bit integer. + /// + /// ```example + /// #33.bit-lshift(2) \ + /// #(-1).bit-lshift(3) + /// ``` + #[func(title = "Bitwise Left Shift")] + pub fn bit_lshift( + self, + /// The amount of bits to shift. Must not be negative. + shift: u32, + ) -> StrResult<i64> { + Ok(self.checked_shl(shift).ok_or("the result is too large")?) + } + + /// Shifts the operand's bits to the right by the specified amount. + /// Performs an arithmetic shift by default (extends the sign bit to the left, + /// such that negative numbers stay negative), but that can be changed by the + /// `logical` parameter. + /// + /// For the purposes of this function, the operand is treated as a signed + /// integer of 64 bits. + /// + /// ```example + /// #64.bit-rshift(2) \ + /// #(-8).bit-rshift(2) \ + /// #(-8).bit-rshift(2, logical: true) + /// ``` + #[func(title = "Bitwise Right Shift")] + pub fn bit_rshift( + self, + /// The amount of bits to shift. Must not be negative. + /// + /// Shifts larger than 63 are allowed and will cause the return value to + /// saturate. For non-negative numbers, the return value saturates at + /// `{0}`, while, for negative numbers, it saturates at `{-1}` if + /// `logical` is set to `{false}`, or `{0}` if it is `{true}`. This + /// behavior is consistent with just applying this operation multiple + /// times. Therefore, the shift will always succeed. + shift: u32, + /// Toggles whether a logical (unsigned) right shift should be performed + /// instead of arithmetic right shift. + /// If this is `{true}`, negative operands will not preserve their sign + /// bit, and bits which appear to the left after the shift will be + /// `{0}`. This parameter has no effect on non-negative operands. + #[named] + #[default(false)] + logical: bool, + ) -> i64 { + if logical { + if shift >= u64::BITS { + // Excessive logical right shift would be equivalent to setting + // all bits to zero. Using `.min(63)` is not enough for logical + // right shift, since `-1 >> 63` returns 1, whereas + // `calc.bit-rshift(-1, 64)` should return the same as + // `(-1 >> 63) >> 1`, which is zero. + 0 + } else { + // Here we reinterpret the signed integer's bits as unsigned to + // perform logical right shift, and then reinterpret back as signed. + // This is valid as, according to the Rust reference, casting between + // two integers of same size (i64 <-> u64) is a no-op (two's complement + // is used). + // Reference: + // https://doc.rust-lang.org/stable/reference/expressions/operator-expr.html#numeric-cast + ((self as u64) >> shift) as i64 + } + } else { + // Saturate at -1 (negative) or 0 (otherwise) on excessive arithmetic + // right shift. Shifting those numbers any further does not change + // them, so it is consistent. + let shift = shift.min(i64::BITS - 1); + self >> shift + } + } + + /// Converts bytes to an integer. + /// + /// ```example + /// #int.from-bytes(bytes((0, 0, 0, 0, 0, 0, 0, 1))) \ + /// #int.from-bytes(bytes((1, 0, 0, 0, 0, 0, 0, 0)), endian: "big") + /// ``` + #[func] + pub fn from_bytes( + /// The bytes that should be converted to an integer. + /// + /// Must be of length at most 8 so that the result fits into a 64-bit + /// signed integer. + bytes: Bytes, + /// The endianness of the conversion. + #[named] + #[default(Endianness::Little)] + endian: Endianness, + /// Whether the bytes should be treated as a signed integer. If this is + /// `{true}` and the most significant bit is set, the resulting number + /// will negative. + #[named] + #[default(true)] + signed: bool, + ) -> StrResult<i64> { + let len = bytes.len(); + if len == 0 { + return Ok(0); + } else if len > 8 { + bail!("too many bytes to convert to a 64 bit number"); + } + + // `decimal` will hold the part of the buffer that should be filled with + // the input bytes, `rest` will remain as is or be filled with 0xFF for + // negative numbers if signed is true. + // + // – big-endian: `decimal` will be the rightmost bytes of the buffer. + // - little-endian: `decimal` will be the leftmost bytes of the buffer. + let mut buf = [0u8; 8]; + let (rest, decimal) = match endian { + Endianness::Big => buf.split_at_mut(8 - len), + Endianness::Little => { + let (first, second) = buf.split_at_mut(len); + (second, first) + } + }; + + decimal.copy_from_slice(bytes.as_ref()); + + // Perform sign-extension if necessary. + if signed { + let most_significant_byte = match endian { + Endianness::Big => decimal[0], + Endianness::Little => decimal[len - 1], + }; + + if most_significant_byte & 0b1000_0000 != 0 { + rest.fill(0xFF); + } + } + + Ok(match endian { + Endianness::Big => i64::from_be_bytes(buf), + Endianness::Little => i64::from_le_bytes(buf), + }) + } + + /// Converts an integer to bytes. + /// + /// ```example + /// #array(10000.to-bytes(endian: "big")) \ + /// #array(10000.to-bytes(size: 4)) + /// ``` + #[func] + pub fn to_bytes( + self, + /// The endianness of the conversion. + #[named] + #[default(Endianness::Little)] + endian: Endianness, + /// The size in bytes of the resulting bytes (must be at least zero). If + /// the integer is too large to fit in the specified size, the + /// conversion will truncate the remaining bytes based on the + /// endianness. To keep the same resulting value, if the endianness is + /// big-endian, the truncation will happen at the rightmost bytes. + /// Otherwise, if the endianness is little-endian, the truncation will + /// happen at the leftmost bytes. + /// + /// Be aware that if the integer is negative and the size is not enough + /// to make the number fit, when passing the resulting bytes to + /// `int.from-bytes`, the resulting number might be positive, as the + /// most significant bit might not be set to 1. + #[named] + #[default(8)] + size: usize, + ) -> Bytes { + let array = match endian { + Endianness::Big => self.to_be_bytes(), + Endianness::Little => self.to_le_bytes(), + }; + + let mut buf = vec![0u8; size]; + match endian { + Endianness::Big => { + // Copy the bytes from the array to the buffer, starting from + // the end of the buffer. + let buf_start = size.saturating_sub(8); + let array_start = 8usize.saturating_sub(size); + buf[buf_start..].copy_from_slice(&array[array_start..]) + } + Endianness::Little => { + // Copy the bytes from the array to the buffer, starting from + // the beginning of the buffer. + let end = size.min(8); + buf[..end].copy_from_slice(&array[..end]) + } + } + + Bytes::from(buf) + } +} + +impl Repr for i64 { + fn repr(&self) -> EcoString { + eco_format!("{:?}", self) + } +} + +/// Represents the byte order used for converting integers and floats to bytes +/// and vice versa. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum Endianness { + /// Big-endian byte order: The highest-value byte is at the beginning of the + /// bytes. + Big, + /// Little-endian byte order: The lowest-value byte is at the beginning of + /// the bytes. + Little, +} + +/// A value that can be cast to an integer. +pub struct ToInt(i64); + +cast! { + ToInt, + v: i64 => Self(v), + v: bool => Self(v as i64), + v: f64 => Self(convert_float_to_int(v)?), + v: Decimal => Self(i64::try_from(v).map_err(|_| eco_format!("number too large"))?), + v: Str => Self(parse_int(&v).map_err(|_| eco_format!("invalid integer: {}", v))?), +} + +pub fn convert_float_to_int(f: f64) -> StrResult<i64> { + if f <= i64::MIN as f64 - 1.0 || f >= i64::MAX as f64 + 1.0 { + Err(eco_format!("number too large")) + } else { + Ok(f as i64) + } +} + +fn parse_int(mut s: &str) -> Result<i64, ParseIntError> { + let mut sign = 1; + if let Some(rest) = s.strip_prefix('-').or_else(|| s.strip_prefix(repr::MINUS_SIGN)) { + sign = -1; + s = rest; + } + if sign == -1 && s == "9223372036854775808" { + return Ok(i64::MIN); + } + Ok(sign * s.parse::<i64>()?) +} + +macro_rules! signed_int { + ($($ty:ty)*) => { + $(cast! { + $ty, + self => Value::Int(self as _), + v: i64 => v.try_into().map_err(|_| "number too large")?, + })* + } +} + +macro_rules! unsigned_int { + ($($ty:ty)*) => { + $(cast! { + $ty, + self => { + #[allow(irrefutable_let_patterns)] + if let Ok(int) = i64::try_from(self) { + Value::Int(int) + } else { + // Some u64 are too large to be cast as i64 + // In that case, we accept that there may be a + // precision loss, and use a floating point number + Value::Float(self as _) + } + }, + v: i64 => v.try_into().map_err(|_| { + if v < 0 { + "number must be at least zero" + } else { + "number too large" + } + })?, + })* + } +} + +signed_int! { i8 i16 i32 isize } +unsigned_int! { u8 u16 u32 u64 usize } + +cast! { + NonZeroI64, + self => Value::Int(self.get() as _), + v: i64 => v.try_into() + .map_err(|_| if v == 0 { + "number must not be zero" + } else { + "number too large" + })?, +} + +cast! { + NonZeroIsize, + self => Value::Int(self.get() as _), + v: i64 => v + .try_into() + .and_then(|v: isize| v.try_into()) + .map_err(|_| if v == 0 { + "number must not be zero" + } else { + "number too large" + })?, +} + +cast! { + NonZeroU64, + self => Value::Int(self.get() as _), + v: i64 => v + .try_into() + .and_then(|v: u64| v.try_into()) + .map_err(|_| if v <= 0 { + "number must be positive" + } else { + "number too large" + })?, +} + +cast! { + NonZeroUsize, + self => Value::Int(self.get() as _), + v: i64 => v + .try_into() + .and_then(|v: usize| v.try_into()) + .map_err(|_| if v <= 0 { + "number must be positive" + } else { + "number too large" + })?, +} diff --git a/crates/typst-library/src/foundations/label.rs b/crates/typst-library/src/foundations/label.rs new file mode 100644 index 00000000..726958df --- /dev/null +++ b/crates/typst-library/src/foundations/label.rs @@ -0,0 +1,93 @@ +use ecow::{eco_format, EcoString}; +use typst_utils::PicoStr; + +use crate::foundations::{func, scope, ty, Repr}; + +/// A label for an element. +/// +/// Inserting a label into content attaches it to the closest preceding element +/// that is not a space. The preceding element must be in the same scope as the +/// label, which means that `[Hello #[<label>]]`, for instance, wouldn't work. +/// +/// A labelled element can be [referenced]($ref), [queried]($query) for, and +/// [styled]($styling) through its label. +/// +/// Once constructed, you can get the name of a label using +/// [`str`]($str/#constructor). +/// +/// # Example +/// ```example +/// #show <a>: set text(blue) +/// #show label("b"): set text(red) +/// +/// = Heading <a> +/// *Strong* #label("b") +/// ``` +/// +/// # Syntax +/// This function also has dedicated syntax: You can create a label by enclosing +/// its name in angle brackets. This works both in markup and code. A label's +/// name can contain letters, numbers, `_`, `-`, `:`, and `.`. +/// +/// Note that there is a syntactical difference when using the dedicated syntax +/// for this function. In the code below, the `[<a>]` terminates the heading and +/// thus attaches to the heading itself, whereas the `[#label("b")]` is part of +/// the heading and thus attaches to the heading's text. +/// +/// ```typ +/// // Equivalent to `#heading[Introduction] <a>`. +/// = Introduction <a> +/// +/// // Equivalent to `#heading[Conclusion #label("b")]`. +/// = Conclusion #label("b") +/// ``` +/// +/// Currently, labels can only be attached to elements in markup mode, not in +/// code mode. This might change in the future. +#[ty(scope, cast)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Label(PicoStr); + +impl Label { + /// Creates a label from a string, interning it. + pub fn new(name: impl Into<PicoStr>) -> Self { + Self(name.into()) + } + + /// Resolves the label to a string. + pub fn as_str(&self) -> &'static str { + self.0.resolve() + } + + /// Turns this label into its inner interned string. + pub fn into_inner(self) -> PicoStr { + self.0 + } +} + +#[scope] +impl Label { + /// Creates a label from a string. + #[func(constructor)] + pub fn construct( + /// The name of the label. + name: PicoStr, + ) -> Label { + Self(name) + } +} + +impl Repr for Label { + fn repr(&self) -> EcoString { + eco_format!("<{}>", self.as_str()) + } +} + +impl From<Label> for PicoStr { + fn from(value: Label) -> Self { + value.into_inner() + } +} + +/// Indicates that an element cannot be labelled. +pub trait Unlabellable {} diff --git a/crates/typst-library/src/foundations/mod.rs b/crates/typst-library/src/foundations/mod.rs new file mode 100644 index 00000000..a6d6c253 --- /dev/null +++ b/crates/typst-library/src/foundations/mod.rs @@ -0,0 +1,302 @@ +//! Foundational types and functions. + +pub mod calc; +pub mod ops; +pub mod repr; +pub mod sys; + +mod args; +mod array; +mod auto; +mod bool; +mod bytes; +mod cast; +mod content; +mod context; +mod datetime; +mod decimal; +mod dict; +mod duration; +mod element; +mod fields; +mod float; +mod func; +mod int; +mod label; +mod module; +mod none; +mod plugin; +mod scope; +mod selector; +mod str; +mod styles; +mod symbol; +mod ty; +mod value; +mod version; + +pub use self::args::*; +pub use self::array::*; +pub use self::auto::*; +pub use self::bytes::*; +pub use self::cast::*; +pub use self::content::*; +pub use self::context::*; +pub use self::datetime::*; +pub use self::decimal::*; +pub use self::dict::*; +pub use self::duration::*; +pub use self::element::*; +pub use self::fields::*; +pub use self::float::*; +pub use self::func::*; +pub use self::int::*; +pub use self::label::*; +pub use self::module::*; +pub use self::none::*; +pub use self::plugin::*; +pub use self::repr::Repr; +pub use self::scope::*; +pub use self::selector::*; +pub use self::str::*; +pub use self::styles::*; +pub use self::symbol::*; +pub use self::ty::*; +pub use self::value::*; +pub use self::version::*; +pub use typst_macros::{scope, ty}; + +#[rustfmt::skip] +#[doc(hidden)] +pub use { + ecow::{eco_format, eco_vec}, + indexmap::IndexMap, + once_cell::sync::Lazy, +}; + +use ecow::EcoString; +use typst_syntax::Spanned; + +use crate::diag::{bail, SourceResult, StrResult}; +use crate::engine::Engine; +use crate::routines::EvalMode; + +/// Foundational types and functions. +/// +/// Here, you'll find documentation for basic data types like [integers]($int) +/// and [strings]($str) as well as details about core computational functions. +#[category] +pub static FOUNDATIONS: Category; + +/// Hook up all `foundations` definitions. +pub(super) fn define(global: &mut Scope, inputs: Dict) { + global.category(FOUNDATIONS); + global.define_type::<bool>(); + global.define_type::<i64>(); + global.define_type::<f64>(); + global.define_type::<Str>(); + global.define_type::<Label>(); + global.define_type::<Bytes>(); + global.define_type::<Content>(); + global.define_type::<Array>(); + global.define_type::<Dict>(); + global.define_type::<Func>(); + global.define_type::<Args>(); + global.define_type::<Type>(); + global.define_type::<Module>(); + global.define_type::<Regex>(); + global.define_type::<Selector>(); + global.define_type::<Datetime>(); + global.define_type::<Decimal>(); + global.define_type::<Symbol>(); + global.define_type::<Duration>(); + global.define_type::<Version>(); + global.define_type::<Plugin>(); + global.define_func::<repr::repr>(); + global.define_func::<panic>(); + global.define_func::<assert>(); + global.define_func::<eval>(); + global.define_func::<style>(); + global.define_module(calc::module()); + global.define_module(sys::module(inputs)); +} + +/// Fails with an error. +/// +/// Arguments are displayed to the user (not rendered in the document) as +/// strings, converting with `repr` if necessary. +/// +/// # Example +/// The code below produces the error `panicked with: "this is wrong"`. +/// ```typ +/// #panic("this is wrong") +/// ``` +#[func(keywords = ["error"])] +pub fn panic( + /// The values to panic with and display to the user. + #[variadic] + values: Vec<Value>, +) -> StrResult<Never> { + let mut msg = EcoString::from("panicked"); + if !values.is_empty() { + msg.push_str(" with: "); + for (i, value) in values.iter().enumerate() { + if i > 0 { + msg.push_str(", "); + } + msg.push_str(&value.repr()); + } + } + Err(msg) +} + +/// Ensures that a condition is fulfilled. +/// +/// Fails with an error if the condition is not fulfilled. Does not +/// produce any output in the document. +/// +/// If you wish to test equality between two values, see +/// [`assert.eq`]($assert.eq) and [`assert.ne`]($assert.ne). +/// +/// # Example +/// ```typ +/// #assert(1 < 2, message: "math broke") +/// ``` +#[func(scope)] +pub fn assert( + /// The condition that must be true for the assertion to pass. + condition: bool, + /// The error message when the assertion fails. + #[named] + message: Option<EcoString>, +) -> StrResult<NoneValue> { + if !condition { + if let Some(message) = message { + bail!("assertion failed: {message}"); + } else { + bail!("assertion failed"); + } + } + Ok(NoneValue) +} + +#[scope] +impl assert { + /// Ensures that two values are equal. + /// + /// Fails with an error if the first value is not equal to the second. Does not + /// produce any output in the document. + /// + /// ```typ + /// #assert.eq(10, 10) + /// ``` + #[func(title = "Assert Equal")] + pub fn eq( + /// The first value to compare. + left: Value, + /// The second value to compare. + right: Value, + /// An optional message to display on error instead of the representations + /// of the compared values. + #[named] + message: Option<EcoString>, + ) -> StrResult<NoneValue> { + if left != right { + if let Some(message) = message { + bail!("equality assertion failed: {message}"); + } else { + bail!( + "equality assertion failed: value {} was not equal to {}", + left.repr(), + right.repr() + ); + } + } + Ok(NoneValue) + } + + /// Ensures that two values are not equal. + /// + /// Fails with an error if the first value is equal to the second. Does not + /// produce any output in the document. + /// + /// ```typ + /// #assert.ne(3, 4) + /// ``` + #[func(title = "Assert Not Equal")] + pub fn ne( + /// The first value to compare. + left: Value, + /// The second value to compare. + right: Value, + /// An optional message to display on error instead of the representations + /// of the compared values. + #[named] + message: Option<EcoString>, + ) -> StrResult<NoneValue> { + if left == right { + if let Some(message) = message { + bail!("inequality assertion failed: {message}"); + } else { + bail!( + "inequality assertion failed: value {} was equal to {}", + left.repr(), + right.repr() + ); + } + } + Ok(NoneValue) + } +} + +/// Evaluates a string as Typst code. +/// +/// This function should only be used as a last resort. +/// +/// # Example +/// ```example +/// #eval("1 + 1") \ +/// #eval("(1, 2, 3, 4)").len() \ +/// #eval("*Markup!*", mode: "markup") \ +/// ``` +#[func(title = "Evaluate")] +pub fn eval( + /// The engine. + engine: &mut Engine, + /// A string of Typst code to evaluate. + source: Spanned<String>, + /// The [syntactical mode]($reference/syntax/#modes) in which the string is + /// parsed. + /// + /// ```example + /// #eval("= Heading", mode: "markup") + /// #eval("1_2^3", mode: "math") + /// ``` + #[named] + #[default(EvalMode::Code)] + mode: EvalMode, + /// A scope of definitions that are made available. + /// + /// ```example + /// #eval("x + 1", scope: (x: 2)) \ + /// #eval( + /// "abc/xyz", + /// mode: "math", + /// scope: ( + /// abc: $a + b + c$, + /// xyz: $x + y + z$, + /// ), + /// ) + /// ``` + #[named] + #[default] + scope: Dict, +) -> SourceResult<Value> { + let Spanned { v: text, span } = source; + let dict = scope; + let mut scope = Scope::new(); + for (key, value) in dict { + scope.define_spanned(key, value, span); + } + (engine.routines.eval_string)(engine.routines, engine.world, &text, span, mode, scope) +} diff --git a/crates/typst-library/src/foundations/module.rs b/crates/typst-library/src/foundations/module.rs new file mode 100644 index 00000000..a476d6af --- /dev/null +++ b/crates/typst-library/src/foundations/module.rs @@ -0,0 +1,142 @@ +use std::fmt::{self, Debug, Formatter}; +use std::sync::Arc; + +use ecow::{eco_format, EcoString}; +use typst_syntax::FileId; + +use crate::diag::StrResult; +use crate::foundations::{repr, ty, Content, Scope, Value}; + +/// An evaluated module, either built-in or resulting from a file. +/// +/// You can access definitions from the module using +/// [field access notation]($scripting/#fields) and interact with it using the +/// [import and include syntaxes]($scripting/#modules). Alternatively, it is +/// possible to convert a module to a dictionary, and therefore access its +/// contents dynamically, using the +/// [dictionary constructor]($dictionary/#constructor). +/// +/// # Example +/// ```example +/// <<< #import "utils.typ" +/// <<< #utils.add(2, 5) +/// +/// <<< #import utils: sub +/// <<< #sub(1, 4) +/// >>> #7 +/// >>> +/// >>> #(-3) +/// ``` +#[ty(cast)] +#[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(Debug, Clone, Hash)] +struct Repr { + /// The top-level definitions that were bound in this module. + scope: Scope, + /// The module's layoutable contents. + content: Content, + /// The id of the file which defines the module, if any. + file_id: Option<FileId>, +} + +impl Module { + /// Create a new module. + pub fn new(name: impl Into<EcoString>, scope: Scope) -> Self { + Self { + name: name.into(), + inner: Arc::new(Repr { scope, content: Content::empty(), file_id: None }), + } + } + + /// 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 + } + + /// Update the module's file id. + pub fn with_file_id(mut self, file_id: FileId) -> Self { + Arc::make_mut(&mut self.inner).file_id = Some(file_id); + 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 file id. + /// + /// Some modules are not associated with a file, like the built-in modules. + pub fn file_id(&self) -> Option<FileId> { + self.inner.file_id + } + + /// 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 field(&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 { + f.debug_struct("Module") + .field("name", &self.name) + .field("scope", &self.inner.scope) + .field("content", &self.inner.content) + .finish() + } +} + +impl repr::Repr for Module { + fn repr(&self) -> EcoString { + eco_format!("<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/crates/typst-library/src/foundations/none.rs b/crates/typst-library/src/foundations/none.rs new file mode 100644 index 00000000..d376c0c5 --- /dev/null +++ b/crates/typst-library/src/foundations/none.rs @@ -0,0 +1,114 @@ +use std::fmt::{self, Debug, Formatter}; + +use ecow::EcoString; +use serde::{Serialize, Serializer}; + +use crate::diag::HintedStrResult; +use crate::foundations::{ + cast, ty, CastInfo, FromValue, IntoValue, Reflect, Repr, Type, Value, +}; + +/// A value that indicates the absence of any other value. +/// +/// The none type has exactly one value: `{none}`. +/// +/// When inserted into the document, it is not visible. This is also the value +/// that is produced by empty code blocks. It can be +/// [joined]($scripting/#blocks) with any value, yielding the other value. +/// +/// # Example +/// ```example +/// Not visible: #none +/// ``` +#[ty(cast, name = "none")] +#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct NoneValue; + +impl Reflect for NoneValue { + fn input() -> CastInfo { + CastInfo::Type(Type::of::<Self>()) + } + + fn output() -> CastInfo { + CastInfo::Type(Type::of::<Self>()) + } + + 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) -> HintedStrResult<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") + } +} + +impl Repr for NoneValue { + fn repr(&self) -> EcoString { + "none".into() + } +} + +impl Serialize for NoneValue { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_none() + } +} + +cast! { + (), + self => Value::None, + _: NoneValue => (), +} + +impl<T: Reflect> Reflect for Option<T> { + fn input() -> CastInfo { + T::input() + NoneValue::input() + } + + fn output() -> CastInfo { + T::output() + NoneValue::output() + } + + 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) -> HintedStrResult<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/crates/typst-library/src/foundations/ops.rs b/crates/typst-library/src/foundations/ops.rs new file mode 100644 index 00000000..ba36137f --- /dev/null +++ b/crates/typst-library/src/foundations/ops.rs @@ -0,0 +1,583 @@ +//! Operations on values. + +use std::cmp::Ordering; + +use ecow::eco_format; +use typst_utils::Numeric; + +use crate::diag::{bail, HintedStrResult, StrResult}; +use crate::foundations::{format_str, Datetime, IntoValue, Regex, Repr, Value}; +use crate::layout::{Alignment, Length, Rel}; +use crate::text::TextElem; +use crate::visualize::Stroke; + +/// Bail with a type mismatch error. +macro_rules! mismatch { + ($fmt:expr, $($value:expr),* $(,)?) => { + return Err(eco_format!($fmt, $($value.ty()),*).into()) + }; +} + +/// Join a value with another value. +pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> { + use 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}")), + (Bytes(a), Bytes(b)) => Bytes(a + b), + (Content(a), Content(b)) => Content(a + b), + (Content(a), Symbol(b)) => Content(a + TextElem::packed(b.get())), + (Content(a), Str(b)) => Content(a + TextElem::packed(b)), + (Str(a), Content(b)) => Content(TextElem::packed(a) + b), + (Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b), + (Array(a), Array(b)) => Array(a + b), + (Dict(a), Dict(b)) => Dict(a + b), + + // Type compatibility. + (Type(a), Str(b)) => Str(format_str!("{a}{b}")), + (Str(a), Type(b)) => Str(format_str!("{a}{b}")), + + (a, b) => mismatch!("cannot join {} with {}", a, b), + }) +} + +/// Apply the unary plus operator to a value. +pub fn pos(value: Value) -> HintedStrResult<Value> { + use Value::*; + Ok(match value { + Int(v) => Int(v), + Float(v) => Float(v), + Decimal(v) => Decimal(v), + Length(v) => Length(v), + Angle(v) => Angle(v), + Ratio(v) => Ratio(v), + Relative(v) => Relative(v), + Fraction(v) => Fraction(v), + Symbol(_) | Str(_) | Bytes(_) | Content(_) | Array(_) | Dict(_) | Datetime(_) => { + mismatch!("cannot apply unary '+' to {}", value) + } + Dyn(d) => { + if d.is::<Alignment>() { + mismatch!("cannot apply unary '+' to {}", d) + } else { + mismatch!("cannot apply '+' to {}", d) + } + } + v => mismatch!("cannot apply '+' to {}", v), + }) +} + +/// Compute the negation of a value. +pub fn neg(value: Value) -> HintedStrResult<Value> { + use Value::*; + Ok(match value { + Int(v) => Int(v.checked_neg().ok_or_else(too_large)?), + Float(v) => Float(-v), + Decimal(v) => Decimal(-v), + Length(v) => Length(-v), + Angle(v) => Angle(-v), + Ratio(v) => Ratio(-v), + Relative(v) => Relative(-v), + Fraction(v) => Fraction(-v), + Duration(v) => Duration(-v), + Datetime(_) => mismatch!("cannot apply unary '-' to {}", value), + v => mismatch!("cannot apply '-' to {}", v), + }) +} + +/// Compute the sum of two values. +pub fn add(lhs: Value, rhs: Value) -> HintedStrResult<Value> { + use Value::*; + Ok(match (lhs, rhs) { + (a, None) => a, + (None, b) => b, + + (Int(a), Int(b)) => Int(a.checked_add(b).ok_or_else(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), + + (Decimal(a), Decimal(b)) => Decimal(a.checked_add(b).ok_or_else(too_large)?), + (Decimal(a), Int(b)) => Decimal( + a.checked_add(crate::foundations::Decimal::from(b)) + .ok_or_else(too_large)?, + ), + (Int(a), Decimal(b)) => Decimal( + crate::foundations::Decimal::from(a) + .checked_add(b) + .ok_or_else(too_large)?, + ), + + (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}")), + (Bytes(a), Bytes(b)) => Bytes(a + b), + (Content(a), Content(b)) => Content(a + b), + (Content(a), Symbol(b)) => Content(a + TextElem::packed(b.get())), + (Content(a), Str(b)) => Content(a + TextElem::packed(b)), + (Str(a), Content(b)) => Content(TextElem::packed(a) + b), + (Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b), + + (Array(a), Array(b)) => Array(a + b), + (Dict(a), Dict(b)) => Dict(a + b), + + (Color(color), Length(thickness)) | (Length(thickness), Color(color)) => { + Stroke::from_pair(color, thickness).into_value() + } + (Gradient(gradient), Length(thickness)) + | (Length(thickness), Gradient(gradient)) => { + Stroke::from_pair(gradient, thickness).into_value() + } + (Pattern(pattern), Length(thickness)) | (Length(thickness), Pattern(pattern)) => { + Stroke::from_pair(pattern, thickness).into_value() + } + + (Duration(a), Duration(b)) => Duration(a + b), + (Datetime(a), Duration(b)) => Datetime(a + b), + (Duration(a), Datetime(b)) => Datetime(b + a), + + // Type compatibility. + (Type(a), Str(b)) => Str(format_str!("{a}{b}")), + (Str(a), Type(b)) => Str(format_str!("{a}{b}")), + + (Dyn(a), Dyn(b)) => { + // Alignments can be summed. + if let (Some(&a), Some(&b)) = + (a.downcast::<Alignment>(), b.downcast::<Alignment>()) + { + return Ok((a + b)?.into_value()); + } + + 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) -> HintedStrResult<Value> { + use Value::*; + Ok(match (lhs, rhs) { + (Int(a), Int(b)) => Int(a.checked_sub(b).ok_or_else(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), + + (Decimal(a), Decimal(b)) => Decimal(a.checked_sub(b).ok_or_else(too_large)?), + (Decimal(a), Int(b)) => Decimal( + a.checked_sub(crate::foundations::Decimal::from(b)) + .ok_or_else(too_large)?, + ), + (Int(a), Decimal(b)) => Decimal( + crate::foundations::Decimal::from(a) + .checked_sub(b) + .ok_or_else(too_large)?, + ), + + (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), + + (Duration(a), Duration(b)) => Duration(a - b), + (Datetime(a), Duration(b)) => Datetime(a - b), + (Datetime(a), Datetime(b)) => Duration((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) -> HintedStrResult<Value> { + use Value::*; + Ok(match (lhs, rhs) { + (Int(a), Int(b)) => Int(a.checked_mul(b).ok_or_else(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), + + (Decimal(a), Decimal(b)) => Decimal(a.checked_mul(b).ok_or_else(too_large)?), + (Decimal(a), Int(b)) => Decimal( + a.checked_mul(crate::foundations::Decimal::from(b)) + .ok_or_else(too_large)?, + ), + (Int(a), Decimal(b)) => Decimal( + crate::foundations::Decimal::from(a) + .checked_mul(b) + .ok_or_else(too_large)?, + ), + + (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(Value::Int(b).cast()?)?), + (Int(a), Str(b)) => Str(b.repeat(Value::Int(a).cast()?)?), + (Array(a), Int(b)) => Array(a.repeat(Value::Int(b).cast()?)?), + (Int(a), Array(b)) => Array(b.repeat(Value::Int(a).cast()?)?), + (Content(a), b @ Int(_)) => Content(a.repeat(b.cast()?)), + (a @ Int(_), Content(b)) => Content(b.repeat(a.cast()?)), + + (Int(a), Duration(b)) => Duration(b * (a as f64)), + (Float(a), Duration(b)) => Duration(b * a), + (Duration(a), Int(b)) => Duration(a * (b as f64)), + (Duration(a), Float(b)) => Duration(a * b), + + (a, b) => mismatch!("cannot multiply {} with {}", a, b), + }) +} + +/// Compute the quotient of two values. +pub fn div(lhs: Value, rhs: Value) -> HintedStrResult<Value> { + use 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), + + (Decimal(a), Decimal(b)) => Decimal(a.checked_div(b).ok_or_else(too_large)?), + (Decimal(a), Int(b)) => Decimal( + a.checked_div(crate::foundations::Decimal::from(b)) + .ok_or_else(too_large)?, + ), + (Int(a), Decimal(b)) => Decimal( + crate::foundations::Decimal::from(a) + .checked_div(b) + .ok_or_else(too_large)?, + ), + + (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), + + (Duration(a), Int(b)) => Duration(a / (b as f64)), + (Duration(a), Float(b)) => Duration(a / b), + (Duration(a), Duration(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 { + use Value::*; + match *v { + Int(v) => v == 0, + Float(v) => v == 0.0, + Decimal(v) => v.is_zero(), + 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(), + Duration(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) -> HintedStrResult<Value> { + match value { + Value::Bool(b) => Ok(Value::Bool(!b)), + v => mismatch!("cannot apply 'not' to {}", v), + } +} + +/// Compute the logical "and" of two values. +pub fn and(lhs: Value, rhs: Value) -> HintedStrResult<Value> { + match (lhs, rhs) { + (Value::Bool(a), Value::Bool(b)) => Ok(Value::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) -> HintedStrResult<Value> { + match (lhs, rhs) { + (Value::Bool(a), Value::Bool(b)) => Ok(Value::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) -> HintedStrResult<Value> { + Ok(Value::Bool(equal(&lhs, &rhs))) +} + +/// Compute whether two values are unequal. +pub fn neq(lhs: Value, rhs: Value) -> HintedStrResult<Value> { + Ok(Value::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) -> HintedStrResult<Value> { + let ordering = compare(&lhs, &rhs)?; + Ok(Value::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 { + use Value::*; + 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, + (Decimal(a), Decimal(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, + (Version(a), Version(b)) => a == b, + (Str(a), Str(b)) => a == b, + (Bytes(a), Bytes(b)) => a == b, + (Label(a), Label(b)) => a == b, + (Content(a), Content(b)) => a == b, + (Array(a), Array(b)) => a == b, + (Dict(a), Dict(b)) => a == b, + (Func(a), Func(b)) => a == b, + (Args(a), Args(b)) => a == b, + (Type(a), Type(b)) => a == b, + (Module(a), Module(b)) => a == b, + (Plugin(a), Plugin(b)) => a == b, + (Datetime(a), Datetime(b)) => a == b, + (Duration(a), Duration(b)) => a == b, + (Dyn(a), Dyn(b)) => a == b, + + // Some technically different things should compare equal. + (&Int(i), &Float(f)) | (&Float(f), &Int(i)) => i as f64 == f, + (&Int(i), &Decimal(d)) | (&Decimal(d), &Int(i)) => { + crate::foundations::Decimal::from(i) == d + } + (&Length(len), &Relative(rel)) | (&Relative(rel), &Length(len)) => { + len == rel.abs && rel.rel.is_zero() + } + (&Ratio(rat), &Relative(rel)) | (&Relative(rel), &Ratio(rat)) => { + rat == rel.rel && rel.abs.is_zero() + } + + // Type compatibility. + (Type(ty), Str(str)) | (Str(str), Type(ty)) => ty.compat_name() == str.as_str(), + + _ => false, + } +} + +/// Compare two values. +pub fn compare(lhs: &Value, rhs: &Value) -> StrResult<Ordering> { + use Value::*; + 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)?, + (Decimal(a), Decimal(b)) => a.cmp(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), + (Version(a), Version(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))?, + (Int(a), Decimal(b)) => crate::foundations::Decimal::from(*a).cmp(b), + (Decimal(a), Int(b)) => a.cmp(&crate::foundations::Decimal::from(*b)), + (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), + + (Duration(a), Duration(b)) => a.cmp(b), + (Datetime(a), Datetime(b)) => try_cmp_datetimes(a, b)?, + (Array(a), Array(b)) => try_cmp_arrays(a.as_slice(), b.as_slice())?, + + _ => mismatch!("cannot compare {} and {}", lhs, rhs), + }) +} + +/// Try to compare two values. +fn try_cmp_values<T: PartialOrd + Repr>(a: &T, b: &T) -> StrResult<Ordering> { + a.partial_cmp(b) + .ok_or_else(|| eco_format!("cannot compare {} with {}", a.repr(), b.repr())) +} + +/// Try to compare two datetimes. +fn try_cmp_datetimes(a: &Datetime, b: &Datetime) -> StrResult<Ordering> { + a.partial_cmp(b) + .ok_or_else(|| eco_format!("cannot compare {} and {}", a.kind(), b.kind())) +} + +/// Try to compare arrays of values lexicographically. +fn try_cmp_arrays(a: &[Value], b: &[Value]) -> StrResult<Ordering> { + a.iter() + .zip(b.iter()) + .find_map(|(first, second)| { + match compare(first, second) { + // Keep searching for a pair of elements that isn't equal. + Ok(Ordering::Equal) => None, + // Found a pair which either is not equal or not comparable, so + // we stop searching. + result => Some(result), + } + }) + .unwrap_or_else(|| { + // The two arrays are equal up to the shortest array's extent, + // so compare their lengths instead. + Ok(a.len().cmp(&b.len())) + }) +} + +/// Test whether one value is "in" another one. +pub fn in_(lhs: Value, rhs: Value) -> HintedStrResult<Value> { + if let Some(b) = contains(&lhs, &rhs) { + Ok(Value::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) -> HintedStrResult<Value> { + if let Some(b) = contains(&lhs, &rhs) { + Ok(Value::Bool(!b)) + } else { + mismatch!("cannot apply 'not in' to {} and {}", lhs, rhs) + } +} + +/// Test for containment. +pub fn contains(lhs: &Value, rhs: &Value) -> Option<bool> { + use Value::*; + 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.clone())), + + // Type compatibility. + (Type(a), Str(b)) => Some(b.as_str().contains(a.compat_name())), + (Type(a), Dict(b)) => Some(b.contains(a.compat_name())), + + _ => Option::None, + } +} + +#[cold] +fn too_large() -> &'static str { + "value is too large" +} diff --git a/crates/typst-library/src/foundations/plugin.rs b/crates/typst-library/src/foundations/plugin.rs new file mode 100644 index 00000000..31107dc3 --- /dev/null +++ b/crates/typst-library/src/foundations/plugin.rs @@ -0,0 +1,365 @@ +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::sync::{Arc, Mutex}; + +use ecow::{eco_format, EcoString}; +use typst_syntax::Spanned; +use wasmi::{AsContext, AsContextMut}; + +use crate::diag::{bail, At, SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{func, repr, scope, ty, Bytes}; +use crate::World; + +/// A WebAssembly plugin. +/// +/// Typst is capable of interfacing with plugins compiled to WebAssembly. Plugin +/// functions may accept multiple [byte buffers]($bytes) as arguments and return +/// a single byte buffer. They should typically be wrapped in idiomatic Typst +/// functions that perform the necessary conversions between native Typst types +/// and bytes. +/// +/// Plugins run in isolation from your system, which means that printing, +/// reading files, or anything like that will not be supported for security +/// reasons. To run as a plugin, a program needs to be compiled to a 32-bit +/// shared WebAssembly library. Many compilers will use the +/// [WASI ABI](https://wasi.dev/) by default or as their only option (e.g. +/// emscripten), which allows printing, reading files, etc. This ABI will not +/// directly work with Typst. You will either need to compile to a different +/// target or [stub all functions](https://github.com/astrale-sharp/wasm-minimal-protocol/blob/master/wasi-stub). +/// +/// # Plugins and Packages +/// Plugins are distributed as packages. A package can make use of a plugin +/// simply by including a WebAssembly file and loading it. Because the +/// byte-based plugin interface is quite low-level, plugins are typically +/// exposed through wrapper functions, that also live in the same package. +/// +/// # Purity +/// Plugin functions must be pure: Given the same arguments, they must always +/// return the same value. The reason for this is that Typst functions must be +/// pure (which is quite fundamental to the language design) and, since Typst +/// function can call plugin functions, this requirement is inherited. In +/// particular, if a plugin function is called twice with the same arguments, +/// Typst might cache the results and call your function only once. +/// +/// # Example +/// ```example +/// #let myplugin = plugin("hello.wasm") +/// #let concat(a, b) = str( +/// myplugin.concatenate( +/// bytes(a), +/// bytes(b), +/// ) +/// ) +/// +/// #concat("hello", "world") +/// ``` +/// +/// # Protocol +/// To be used as a plugin, a WebAssembly module must conform to the following +/// protocol: +/// +/// ## Exports +/// A plugin module can export functions to make them callable from Typst. To +/// conform to the protocol, an exported function should: +/// +/// - Take `n` 32-bit integer arguments `a_1`, `a_2`, ..., `a_n` (interpreted as +/// lengths, so `usize/size_t` may be preferable), and return one 32-bit +/// integer. +/// +/// - The function should first allocate a buffer `buf` of length +/// `a_1 + a_2 + ... + a_n`, and then call +/// `wasm_minimal_protocol_write_args_to_buffer(buf.ptr)`. +/// +/// - The `a_1` first bytes of the buffer now constitute the first argument, the +/// `a_2` next bytes the second argument, and so on. +/// +/// - The function can now do its job with the arguments and produce an output +/// buffer. Before returning, it should call +/// `wasm_minimal_protocol_send_result_to_host` to send its result back to the +/// host. +/// +/// - To signal success, the function should return `0`. +/// +/// - To signal an error, the function should return `1`. The written buffer is +/// then interpreted as an UTF-8 encoded error message. +/// +/// ## Imports +/// Plugin modules need to import two functions that are provided by the runtime. +/// (Types and functions are described using WAT syntax.) +/// +/// - `(import "typst_env" "wasm_minimal_protocol_write_args_to_buffer" (func (param i32)))` +/// +/// Writes the arguments for the current function into a plugin-allocated +/// buffer. When a plugin function is called, it +/// [receives the lengths](#exports) of its input buffers as arguments. It +/// should then allocate a buffer whose capacity is at least the sum of these +/// lengths. It should then call this function with a `ptr` to the buffer to +/// fill it with the arguments, one after another. +/// +/// - `(import "typst_env" "wasm_minimal_protocol_send_result_to_host" (func (param i32 i32)))` +/// +/// Sends the output of the current function to the host (Typst). The first +/// parameter shall be a pointer to a buffer (`ptr`), while the second is the +/// length of that buffer (`len`). The memory pointed at by `ptr` can be freed +/// immediately after this function returns. If the message should be +/// interpreted as an error message, it should be encoded as UTF-8. +/// +/// # Resources +/// For more resources, check out the +/// [wasm-minimal-protocol repository](https://github.com/astrale-sharp/wasm-minimal-protocol). +/// It contains: +/// +/// - A list of example plugin implementations and a test runner for these +/// examples +/// - Wrappers to help you write your plugin in Rust (Zig wrapper in +/// development) +/// - A stubber for WASI +#[ty(scope, cast)] +#[derive(Clone)] +pub struct Plugin(Arc<Repr>); + +/// The internal representation of a plugin. +struct Repr { + /// The raw WebAssembly bytes. + bytes: Bytes, + /// The function defined by the WebAssembly module. + functions: Vec<(EcoString, wasmi::Func)>, + /// Owns all data associated with the WebAssembly module. + store: Mutex<Store>, +} + +/// Owns all data associated with the WebAssembly module. +type Store = wasmi::Store<StoreData>; + +/// If there was an error reading/writing memory, keep the offset + length to +/// display an error message. +struct MemoryError { + offset: u32, + length: u32, + write: bool, +} +/// The persistent store data used for communication between store and host. +#[derive(Default)] +struct StoreData { + args: Vec<Bytes>, + output: Vec<u8>, + memory_error: Option<MemoryError>, +} + +#[scope] +impl Plugin { + /// Creates a new plugin from a WebAssembly file. + #[func(constructor)] + pub fn construct( + /// The engine. + engine: &mut Engine, + /// Path to a WebAssembly file. + /// + /// For more details, see the [Paths section]($syntax/#paths). + path: Spanned<EcoString>, + ) -> SourceResult<Plugin> { + let Spanned { v: path, span } = path; + let id = span.resolve_path(&path).at(span)?; + let data = engine.world.file(id).at(span)?; + Plugin::new(data).at(span) + } +} + +impl Plugin { + /// Create a new plugin from raw WebAssembly bytes. + #[comemo::memoize] + #[typst_macros::time(name = "load plugin")] + pub fn new(bytes: Bytes) -> StrResult<Plugin> { + let engine = wasmi::Engine::default(); + let module = wasmi::Module::new(&engine, bytes.as_slice()) + .map_err(|err| format!("failed to load WebAssembly module ({err})"))?; + + let mut linker = wasmi::Linker::new(&engine); + linker + .func_wrap( + "typst_env", + "wasm_minimal_protocol_send_result_to_host", + wasm_minimal_protocol_send_result_to_host, + ) + .unwrap(); + linker + .func_wrap( + "typst_env", + "wasm_minimal_protocol_write_args_to_buffer", + wasm_minimal_protocol_write_args_to_buffer, + ) + .unwrap(); + + let mut store = Store::new(&engine, StoreData::default()); + let instance = linker + .instantiate(&mut store, &module) + .and_then(|pre_instance| pre_instance.start(&mut store)) + .map_err(|e| eco_format!("{e}"))?; + + // Ensure that the plugin exports its memory. + if !matches!( + instance.get_export(&store, "memory"), + Some(wasmi::Extern::Memory(_)) + ) { + bail!("plugin does not export its memory"); + } + + // Collect exported functions. + let functions = instance + .exports(&store) + .filter_map(|export| { + let name = export.name().into(); + export.into_func().map(|func| (name, func)) + }) + .collect(); + + Ok(Plugin(Arc::new(Repr { bytes, functions, store: Mutex::new(store) }))) + } + + /// Call the plugin function with the given `name`. + #[comemo::memoize] + #[typst_macros::time(name = "call plugin")] + pub fn call(&self, name: &str, args: Vec<Bytes>) -> StrResult<Bytes> { + // Find the function with the given name. + let func = self + .0 + .functions + .iter() + .find(|(v, _)| v == name) + .map(|&(_, func)| func) + .ok_or_else(|| { + eco_format!("plugin does not contain a function called {name}") + })?; + + let mut store = self.0.store.lock().unwrap(); + let ty = func.ty(store.as_context()); + + // Check function signature. + if ty.params().iter().any(|&v| v != wasmi::core::ValType::I32) { + bail!( + "plugin function `{name}` has a parameter that is not a 32-bit integer" + ); + } + if ty.results() != [wasmi::core::ValType::I32] { + bail!("plugin function `{name}` does not return exactly one 32-bit integer"); + } + + // Check inputs. + let expected = ty.params().len(); + let given = args.len(); + if expected != given { + bail!( + "plugin function takes {expected} argument{}, but {given} {} given", + if expected == 1 { "" } else { "s" }, + if given == 1 { "was" } else { "were" }, + ); + } + + // Collect the lengths of the argument buffers. + let lengths = args + .iter() + .map(|a| wasmi::Val::I32(a.len() as i32)) + .collect::<Vec<_>>(); + + // Store the input data. + store.data_mut().args = args; + + // Call the function. + let mut code = wasmi::Val::I32(-1); + func.call(store.as_context_mut(), &lengths, std::slice::from_mut(&mut code)) + .map_err(|err| eco_format!("plugin panicked: {err}"))?; + if let Some(MemoryError { offset, length, write }) = + store.data_mut().memory_error.take() + { + return Err(eco_format!( + "plugin tried to {kind} out of bounds: pointer {offset:#x} is out of bounds for {kind} of length {length}", + kind = if write { "write" } else { "read" } + )); + } + + // Extract the returned data. + let output = std::mem::take(&mut store.data_mut().output); + + // Parse the functions return value. + match code { + wasmi::Val::I32(0) => {} + wasmi::Val::I32(1) => match std::str::from_utf8(&output) { + Ok(message) => bail!("plugin errored with: {message}"), + Err(_) => { + bail!("plugin errored, but did not return a valid error message") + } + }, + _ => bail!("plugin did not respect the protocol"), + }; + + Ok(output.into()) + } + + /// An iterator over all the function names defined by the plugin. + pub fn iter(&self) -> impl Iterator<Item = &EcoString> { + self.0.functions.as_slice().iter().map(|(func_name, _)| func_name) + } +} + +impl Debug for Plugin { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("Plugin(..)") + } +} + +impl repr::Repr for Plugin { + fn repr(&self) -> EcoString { + "plugin(..)".into() + } +} + +impl PartialEq for Plugin { + fn eq(&self, other: &Self) -> bool { + self.0.bytes == other.0.bytes + } +} + +impl Hash for Plugin { + fn hash<H: Hasher>(&self, state: &mut H) { + self.0.bytes.hash(state); + } +} + +/// Write the arguments to the plugin function into the plugin's memory. +fn wasm_minimal_protocol_write_args_to_buffer( + mut caller: wasmi::Caller<StoreData>, + ptr: u32, +) { + let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); + let arguments = std::mem::take(&mut caller.data_mut().args); + let mut offset = ptr as usize; + for arg in arguments { + if memory.write(&mut caller, offset, arg.as_slice()).is_err() { + caller.data_mut().memory_error = Some(MemoryError { + offset: offset as u32, + length: arg.len() as u32, + write: true, + }); + return; + } + offset += arg.len(); + } +} + +/// Extracts the output of the plugin function from the plugin's memory. +fn wasm_minimal_protocol_send_result_to_host( + mut caller: wasmi::Caller<StoreData>, + ptr: u32, + len: u32, +) { + let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); + let mut buffer = std::mem::take(&mut caller.data_mut().output); + buffer.resize(len as usize, 0); + if memory.read(&caller, ptr as _, &mut buffer).is_err() { + caller.data_mut().memory_error = + Some(MemoryError { offset: ptr, length: len, write: false }); + return; + } + caller.data_mut().output = buffer; +} diff --git a/crates/typst-library/src/foundations/repr.rs b/crates/typst-library/src/foundations/repr.rs new file mode 100644 index 00000000..e219b0f1 --- /dev/null +++ b/crates/typst-library/src/foundations/repr.rs @@ -0,0 +1,235 @@ +//! Debug representation of values. + +use ecow::{eco_format, EcoString}; +use typst_utils::round_with_precision; + +use crate::foundations::{func, Str, Value}; + +/// The Unicode minus sign. +pub const MINUS_SIGN: &str = "\u{2212}"; + +/// Returns the string representation of a value. +/// +/// When inserted into content, most values are displayed as this representation +/// in monospace with syntax-highlighting. The exceptions are `{none}`, +/// integers, floats, strings, content, and functions. +/// +/// **Note:** This function is for debugging purposes. Its output should not be +/// considered stable and may change at any time! +/// +/// # Example +/// ```example +/// #none vs #repr(none) \ +/// #"hello" vs #repr("hello") \ +/// #(1, 2) vs #repr((1, 2)) \ +/// #[*Hi*] vs #repr([*Hi*]) +/// ``` +#[func(title = "Representation")] +pub fn repr( + /// The value whose string representation to produce. + value: Value, +) -> Str { + value.repr().into() +} + +/// A trait that defines the `repr` of a Typst value. +pub trait Repr { + /// Return the debug representation of the value. + fn repr(&self) -> EcoString; +} + +/// Format an integer in a base. +pub fn format_int_with_base(mut n: i64, base: i64) -> EcoString { + if n == 0 { + return "0".into(); + } + + // The largest output is `to_base(i64::MIN, 2)`, which is 64 bytes long, + // plus the length of the minus sign. + const SIZE: usize = 64 + MINUS_SIGN.len(); + let mut digits = [b'\0'; SIZE]; + let mut i = SIZE; + + // It's tempting to take the absolute value, but this will fail for i64::MIN. + // Instead, we turn n negative, as -i64::MAX is perfectly representable. + let negative = n < 0; + if n > 0 { + n = -n; + } + + while n != 0 { + let digit = char::from_digit(-(n % base) as u32, base as u32); + i -= 1; + digits[i] = digit.unwrap_or('?') as u8; + n /= base; + } + + if negative { + let prev = i; + i -= MINUS_SIGN.len(); + digits[i..prev].copy_from_slice(MINUS_SIGN.as_bytes()); + } + + std::str::from_utf8(&digits[i..]).unwrap_or_default().into() +} + +/// Converts a float to a string representation with a specific precision and a +/// unit, all with a single allocation. +/// +/// The returned string is always valid Typst code. As such, it might not be a +/// float literal. For example, it may return `"float.inf"`. +pub fn format_float( + mut value: f64, + precision: Option<u8>, + force_separator: bool, + unit: &str, +) -> EcoString { + if let Some(p) = precision { + value = round_with_precision(value, p as i16); + } + // Debug for f64 always prints a decimal separator, while Display only does + // when necessary. + let unit_multiplication = if unit.is_empty() { "" } else { " * 1" }; + if value.is_nan() { + eco_format!("float.nan{unit_multiplication}{unit}") + } else if value.is_infinite() { + let sign = if value < 0.0 { "-" } else { "" }; + eco_format!("{sign}float.inf{unit_multiplication}{unit}") + } else if force_separator { + eco_format!("{value:?}{unit}") + } else { + eco_format!("{value}{unit}") + } +} + +/// Converts a float to a string representation with a precision of three +/// decimal places. This is intended to be used as part of a larger structure +/// containing multiple float components, such as colors. +pub fn format_float_component(value: f64) -> EcoString { + format_float(value, Some(3), false, "") +} + +/// Converts a float to a string representation with a precision of two decimal +/// places, followed by a unit. +pub fn format_float_with_unit(value: f64, unit: &str) -> EcoString { + format_float(value, Some(2), false, unit) +} + +/// Converts a float to a string that can be used to display the float as text. +pub fn display_float(value: f64) -> EcoString { + if value.is_nan() { + "NaN".into() + } else if value.is_infinite() { + let sign = if value < 0.0 { MINUS_SIGN } else { "" }; + eco_format!("{sign}∞") + } else if value < 0.0 { + eco_format!("{}{}", MINUS_SIGN, value.abs()) + } else { + eco_format!("{}", value.abs()) + } +} + +/// Formats 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 +} + +/// Formats 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 +} + +/// Formats 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 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_to_base() { + assert_eq!(&format_int_with_base(0, 10), "0"); + assert_eq!(&format_int_with_base(0, 16), "0"); + assert_eq!(&format_int_with_base(0, 36), "0"); + assert_eq!( + &format_int_with_base(i64::MAX, 2), + "111111111111111111111111111111111111111111111111111111111111111" + ); + assert_eq!( + &format_int_with_base(i64::MIN, 2), + "\u{2212}1000000000000000000000000000000000000000000000000000000000000000" + ); + assert_eq!(&format_int_with_base(i64::MAX, 10), "9223372036854775807"); + assert_eq!(&format_int_with_base(i64::MIN, 10), "\u{2212}9223372036854775808"); + assert_eq!(&format_int_with_base(i64::MAX, 16), "7fffffffffffffff"); + assert_eq!(&format_int_with_base(i64::MIN, 16), "\u{2212}8000000000000000"); + assert_eq!(&format_int_with_base(i64::MAX, 36), "1y2p0ij32e8e7"); + assert_eq!(&format_int_with_base(i64::MIN, 36), "\u{2212}1y2p0ij32e8e8"); + } +} diff --git a/crates/typst-library/src/foundations/scope.rs b/crates/typst-library/src/foundations/scope.rs new file mode 100644 index 00000000..b51f8caa --- /dev/null +++ b/crates/typst-library/src/foundations/scope.rs @@ -0,0 +1,416 @@ +#[doc(inline)] +pub use typst_macros::category; + +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; + +use ecow::{eco_format, EcoString}; +use indexmap::IndexMap; +use typst_syntax::ast::{self, AstNode}; +use typst_syntax::Span; +use typst_utils::Static; + +use crate::diag::{bail, HintedStrResult, HintedString, StrResult}; +use crate::foundations::{ + Element, Func, IntoValue, Module, NativeElement, NativeFunc, NativeFuncData, + NativeType, Type, Value, +}; +use crate::Library; + +/// 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) -> HintedStrResult<&Value> { + std::iter::once(&self.top) + .chain(self.scopes.iter().rev()) + .find_map(|scope| scope.get(var)) + .or_else(|| { + self.base.and_then(|base| match base.global.scope().get(var) { + Some(value) => Some(value), + None if var == "std" => Some(&base.std), + None => None, + }) + }) + .ok_or_else(|| unknown_variable(var)) + } + + /// Try to access a variable immutably in math. + pub fn get_in_math(&self, var: &str) -> HintedStrResult<&Value> { + std::iter::once(&self.top) + .chain(self.scopes.iter().rev()) + .find_map(|scope| scope.get(var)) + .or_else(|| { + self.base.and_then(|base| match base.math.scope().get(var) { + Some(value) => Some(value), + None if var == "std" => Some(&base.std), + None => None, + }) + }) + .ok_or_else(|| { + unknown_variable_math( + var, + self.base.is_some_and(|base| base.global.scope().get(var).is_some()), + ) + }) + } + + /// Try to access a variable mutably. + pub fn get_mut(&mut self, var: &str) -> HintedStrResult<&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(_) => cannot_mutate_constant(var), + _ if var == "std" => cannot_mutate_constant(var), + _ => unknown_variable(var), + } + })? + } + + /// Check if an std variable is shadowed. + pub fn check_std_shadowed(&self, var: &str) -> bool { + self.base.is_some_and(|base| base.global.scope().get(var).is_some()) + && std::iter::once(&self.top) + .chain(self.scopes.iter().rev()) + .any(|scope| scope.get(var).is_some()) + } +} + +#[cold] +fn cannot_mutate_constant(var: &str) -> HintedString { + eco_format!("cannot mutate a constant: {}", var).into() +} + +/// The error message when a variable is not found. +#[cold] +fn unknown_variable(var: &str) -> HintedString { + let mut res = HintedString::new(eco_format!("unknown variable: {}", var)); + + if var.contains('-') { + res.hint(eco_format!( + "if you meant to use subtraction, try adding spaces around the minus sign{}: `{}`", + if var.matches('-').count() > 1 { "s" } else { "" }, + var.replace('-', " - ") + )); + } + + res +} + +#[cold] +fn unknown_variable_math(var: &str, in_global: bool) -> HintedString { + let mut res = HintedString::new(eco_format!("unknown variable: {}", var)); + + if matches!(var, "none" | "auto" | "false" | "true") { + res.hint(eco_format!( + "if you meant to use a literal, try adding a hash before it: `#{var}`", + )); + } else if in_global { + res.hint(eco_format!( + "`{var}` is not available directly in math, try adding a hash before it: `#{var}`", + )); + } else { + res.hint(eco_format!( + "if you meant to display multiple letters as is, try adding spaces between each letter: `{}`", + var.chars() + .flat_map(|c| [' ', c]) + .skip(1) + .collect::<EcoString>() + )); + res.hint(eco_format!( + "or if you meant to display this as text, try placing it in quotes: `\"{var}\"`" + )); + } + + res +} + +/// A map from binding names to values. +#[derive(Default, Clone)] +pub struct Scope { + map: IndexMap<EcoString, Slot>, + deduplicate: bool, + category: Option<Category>, +} + +impl Scope { + /// Create a new empty scope. + pub fn new() -> Self { + Default::default() + } + + /// Create a new scope with duplication prevention. + pub fn deduplicating() -> Self { + Self { deduplicate: true, ..Default::default() } + } + + /// Enter a new category. + pub fn category(&mut self, category: Category) { + self.category = Some(category); + } + + /// Reset the category. + pub fn reset_category(&mut self) { + self.category = None; + } + + /// Bind a value to a name. + #[track_caller] + pub fn define(&mut self, name: impl Into<EcoString>, value: impl IntoValue) { + self.define_spanned(name, value, Span::detached()) + } + + /// Bind a value to a name defined by an identifier. + #[track_caller] + pub fn define_ident(&mut self, ident: ast::Ident, value: impl IntoValue) { + self.define_spanned(ident.get().clone(), value, ident.span()) + } + + /// Bind a value to a name. + #[track_caller] + pub fn define_spanned( + &mut self, + name: impl Into<EcoString>, + value: impl IntoValue, + span: Span, + ) { + let name = name.into(); + + #[cfg(debug_assertions)] + if self.deduplicate && self.map.contains_key(&name) { + panic!("duplicate definition: {name}"); + } + + self.map.insert( + name, + Slot::new(value.into_value(), span, Kind::Normal, self.category), + ); + } + + /// Define a captured, immutable binding. + pub fn define_captured( + &mut self, + name: EcoString, + value: Value, + capturer: Capturer, + span: Span, + ) { + self.map.insert( + name, + Slot::new(value.into_value(), span, Kind::Captured(capturer), self.category), + ); + } + + /// Define a native function through a Rust type that shadows the function. + pub fn define_func<T: NativeFunc>(&mut self) { + let data = T::data(); + self.define(data.name, Func::from(data)); + } + + /// Define a native function with raw function data. + pub fn define_func_with_data(&mut self, data: &'static NativeFuncData) { + self.define(data.name, Func::from(data)); + } + + /// Define a native type. + pub fn define_type<T: NativeType>(&mut self) { + let data = T::data(); + self.define(data.name, Type::from(data)); + } + + /// Define a native element. + pub fn define_elem<T: NativeElement>(&mut self) { + let data = T::data(); + self.define(data.name, Element::from(data)); + } + + /// Define a module. + pub fn define_module(&mut self, module: Module) { + self.define(module.name().clone(), module); + } + + /// Try to access a variable immutably. + pub fn get(&self, var: &str) -> Option<&Value> { + self.map.get(var).map(Slot::read) + } + + /// Try to access a variable mutably. + pub fn get_mut(&mut self, var: &str) -> Option<HintedStrResult<&mut Value>> { + self.map + .get_mut(var) + .map(Slot::write) + .map(|res| res.map_err(HintedString::from)) + } + + /// Get the span of a definition. + pub fn get_span(&self, var: &str) -> Option<Span> { + Some(self.map.get(var)?.span) + } + + /// Get the category of a definition. + pub fn get_category(&self, var: &str) -> Option<Category> { + self.map.get(var)?.category + } + + /// Iterate over all definitions. + pub fn iter(&self) -> impl Iterator<Item = (&EcoString, &Value, Span)> { + self.map.iter().map(|(k, v)| (k, v.read(), v.span)) + } +} + +impl Debug for Scope { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Scope ")?; + f.debug_map() + .entries(self.map.iter().map(|(k, v)| (k, v.read()))) + .finish() + } +} + +impl Hash for Scope { + fn hash<H: Hasher>(&self, state: &mut H) { + state.write_usize(self.map.len()); + for item in &self.map { + item.hash(state); + } + self.deduplicate.hash(state); + self.category.hash(state); + } +} + +/// Defines the associated scope of a Rust type. +pub trait NativeScope { + /// The constructor function for the type, if any. + fn constructor() -> Option<&'static NativeFuncData>; + + /// Get the associated scope for the type. + fn scope() -> Scope; +} + +/// 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, + /// A span associated with the stored value. + span: Span, + /// The category of the slot. + category: Option<Category>, +} + +/// 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(Capturer), +} + +/// What the variable was captured by. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Capturer { + /// Captured by a function / closure. + Function, + /// Captured by a context expression. + Context, +} + +impl Slot { + /// Create a new slot. + fn new(value: Value, span: Span, kind: Kind, category: Option<Category>) -> Self { + Self { value, span, kind, category } + } + + /// 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(capturer) => { + bail!( + "variables from outside the {} are \ + read-only and cannot be modified", + match capturer { + Capturer::Function => "function", + Capturer::Context => "context expression", + } + ) + } + } + } +} + +/// A group of related definitions. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct Category(Static<CategoryData>); + +impl Category { + /// Create a new category from raw data. + pub const fn from_data(data: &'static CategoryData) -> Self { + Self(Static(data)) + } + + /// The category's name. + pub fn name(&self) -> &'static str { + self.0.name + } + + /// The type's title case name, for use in documentation (e.g. `String`). + pub fn title(&self) -> &'static str { + self.0.title + } + + /// Documentation for the category. + pub fn docs(&self) -> &'static str { + self.0.docs + } +} + +impl Debug for Category { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Category({})", self.name()) + } +} + +/// Defines a category. +#[derive(Debug)] +pub struct CategoryData { + pub name: &'static str, + pub title: &'static str, + pub docs: &'static str, +} diff --git a/crates/typst-library/src/foundations/selector.rs b/crates/typst-library/src/foundations/selector.rs new file mode 100644 index 00000000..bf5449d9 --- /dev/null +++ b/crates/typst-library/src/foundations/selector.rs @@ -0,0 +1,449 @@ +use std::any::{Any, TypeId}; +use std::sync::Arc; + +use comemo::Tracked; +use ecow::{eco_format, EcoString, EcoVec}; +use smallvec::SmallVec; + +use crate::diag::{bail, HintedStrResult, StrResult}; +use crate::foundations::{ + cast, func, repr, scope, ty, CastInfo, Content, Context, Dict, Element, FromValue, + Func, Label, Reflect, Regex, Repr, Str, StyleChain, Symbol, Type, Value, +}; +use crate::introspection::{Introspector, Locatable, Location, Unqueriable}; + +/// A helper macro to create a field selector used in [`Selector::Elem`] +#[macro_export] +#[doc(hidden)] +macro_rules! __select_where { + ($ty:ty $(, $field:ident => $value:expr)* $(,)?) => {{ + #[allow(unused_mut)] + let mut fields = ::smallvec::SmallVec::new(); + $( + fields.push(( + <$ty as $crate::foundations::Fields>::Enum::$field as u8, + $crate::foundations::IntoValue::into_value($value), + )); + )* + $crate::foundations::Selector::Elem( + <$ty as $crate::foundations::NativeElement>::elem(), + Some(fields), + ) + }}; +} + +#[doc(inline)] +pub use crate::__select_where as select_where; + +/// A filter for selecting elements within the document. +/// +/// You can construct a selector in the following ways: +/// - you can use an element [function] +/// - you can filter for an element function with +/// [specific fields]($function.where) +/// - you can use a [string]($str) or [regular expression]($regex) +/// - you can use a [`{<label>}`]($label) +/// - you can use a [`location`] +/// - call the [`selector`] constructor to convert any of the above types into a +/// selector value and use the methods below to refine it +/// +/// Selectors are used to [apply styling rules]($styling/#show-rules) to +/// elements. You can also use selectors to [query] the document for certain +/// types of elements. +/// +/// Furthermore, you can pass a selector to several of Typst's built-in +/// functions to configure their behaviour. One such example is the [outline] +/// where it can be used to change which elements are listed within the outline. +/// +/// Multiple selectors can be combined using the methods shown below. However, +/// not all kinds of selectors are supported in all places, at the moment. +/// +/// # Example +/// ```example +/// #context query( +/// heading.where(level: 1) +/// .or(heading.where(level: 2)) +/// ) +/// +/// = This will be found +/// == So will this +/// === But this will not. +/// ``` +#[ty(scope, cast)] +#[derive(Debug, 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(Element, Option<SmallVec<[(u8, Value); 1]>>), + /// 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) -> StrResult<Self> { + if text.is_empty() { + bail!("text selector is empty"); + } + Ok(Self::Regex(Regex::new(®ex::escape(text)).unwrap())) + } + + /// Define a regex selector. + pub fn regex(regex: Regex) -> StrResult<Self> { + if regex.as_str().is_empty() { + bail!("regex selector is empty"); + } + if regex.is_match("") { + bail!("regex matches empty text"); + } + Ok(Self::Regex(regex)) + } + + /// Define a simple [`Selector::Can`] selector. + pub fn can<T: ?Sized + Any>() -> Self { + Self::Can(TypeId::of::<T>()) + } + + /// Whether the selector matches for the target. + pub fn matches(&self, target: &Content, styles: Option<StyleChain>) -> bool { + match self { + Self::Elem(element, dict) => { + target.elem() == *element + && dict.iter().flat_map(|dict| dict.iter()).all(|(id, value)| { + target.get(*id, styles).as_ref().ok() == Some(value) + }) + } + Self::Label(label) => target.label() == Some(*label), + Self::Can(cap) => target.func().can_type_id(*cap), + Self::Or(selectors) => { + selectors.iter().any(move |sel| sel.matches(target, styles)) + } + Self::And(selectors) => { + selectors.iter().all(move |sel| sel.matches(target, styles)) + } + Self::Location(location) => target.location() == Some(*location), + // Not supported here. + Self::Regex(_) | Self::Before { .. } | Self::After { .. } => false, + } + } +} + +#[scope] +impl Selector { + /// Turns a value into a selector. The following values are accepted: + /// - An element function like a `heading` or `figure`. + /// - A `{<label>}`. + /// - A more complex selector like `{heading.where(level: 1)}`. + #[func(constructor)] + pub fn construct( + /// Can be an element function like a `heading` or `figure`, a `{<label>}` + /// or a more complex selector like `{heading.where(level: 1)}`. + target: Selector, + ) -> Selector { + target + } + + /// Selects all elements that match this or any of the other selectors. + #[func] + pub fn or( + self, + /// The other selectors to match on. + #[variadic] + others: Vec<Selector>, + ) -> Selector { + Self::Or(others.into_iter().chain(Some(self)).collect()) + } + + /// Selects all elements that match this and all of the other selectors. + #[func] + pub fn and( + self, + /// The other selectors to match on. + #[variadic] + others: Vec<Selector>, + ) -> Selector { + Self::And(others.into_iter().chain(Some(self)).collect()) + } + + /// Returns a modified selector that will only match elements that occur + /// before the first match of `end`. + #[func] + pub fn before( + self, + /// The original selection will end at the first match of `end`. + end: LocatableSelector, + /// Whether `end` itself should match or not. This is only relevant if + /// both selectors match the same type of element. Defaults to `{true}`. + #[named] + #[default(true)] + inclusive: bool, + ) -> Selector { + Self::Before { + selector: Arc::new(self), + end: Arc::new(end.0), + inclusive, + } + } + + /// Returns a modified selector that will only match elements that occur + /// after the first match of `start`. + #[func] + pub fn after( + self, + /// The original selection will start at the first match of `start`. + start: LocatableSelector, + /// Whether `start` itself should match or not. This is only relevant + /// if both selectors match the same type of element. Defaults to + /// `{true}`. + #[named] + #[default(true)] + inclusive: bool, + ) -> Selector { + Self::After { + selector: Arc::new(self), + start: Arc::new(start.0), + inclusive, + } + } +} + +impl From<Location> for Selector { + fn from(value: Location) -> Self { + Self::Location(value) + } +} + +impl Repr for Selector { + fn repr(&self) -> EcoString { + match self { + Self::Elem(elem, dict) => { + if let Some(dict) = dict { + let dict = dict + .iter() + .map(|(id, value)| (elem.field_name(*id).unwrap(), value.clone())) + .map(|(name, value)| (EcoString::from(name).into(), value)) + .collect::<Dict>(); + eco_format!("{}.where{}", elem.name(), dict.repr()) + } else { + elem.name().into() + } + } + Self::Label(label) => label.repr(), + Self::Regex(regex) => regex.repr(), + Self::Can(cap) => eco_format!("{cap:?}"), + Self::Or(selectors) | Self::And(selectors) => { + let function = if matches!(self, Self::Or(_)) { "or" } else { "and" }; + let pieces: Vec<_> = selectors.iter().map(Selector::repr).collect(); + eco_format!("{}{}", function, repr::pretty_array_like(&pieces, false)) + } + Self::Location(loc) => loc.repr(), + Self::Before { selector, end: split, inclusive } + | Self::After { selector, start: split, inclusive } => { + let method = + if matches!(self, Self::Before { .. }) { "before" } else { "after" }; + let inclusive_arg = if !*inclusive { ", inclusive: false" } else { "" }; + eco_format!( + "{}.{}({}{})", + selector.repr(), + method, + split.repr(), + inclusive_arg + ) + } + } + } +} + +cast! { + type Selector, + text: EcoString => Self::text(&text)?, + func: Func => func + .element() + .ok_or("only element functions can be used as selectors")? + .select(), + label: Label => Self::Label(label), + 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(Debug, Clone, PartialEq, Hash)] +pub struct LocatableSelector(pub Selector); + +impl LocatableSelector { + /// Resolve this selector into a location that is guaranteed to be unique. + pub fn resolve_unique( + &self, + introspector: Tracked<Introspector>, + context: Tracked<Context>, + ) -> HintedStrResult<Location> { + match &self.0 { + Selector::Location(loc) => Ok(*loc), + other => { + context.introspect()?; + Ok(introspector.query_unique(other).map(|c| c.location().unwrap())?) + } + } + } +} + +impl Reflect for LocatableSelector { + fn input() -> CastInfo { + CastInfo::Union(vec![ + CastInfo::Type(Type::of::<Label>()), + CastInfo::Type(Type::of::<Func>()), + CastInfo::Type(Type::of::<Location>()), + CastInfo::Type(Type::of::<Selector>()), + ]) + } + + fn output() -> CastInfo { + CastInfo::Type(Type::of::<Selector>()) + } + + fn castable(value: &Value) -> bool { + Label::castable(value) + || Func::castable(value) + || Location::castable(value) + || Selector::castable(value) + } +} + +cast! { + LocatableSelector, + self => self.0.into_value(), +} + +impl FromValue for LocatableSelector { + fn from_value(value: Value) -> HintedStrResult<Self> { + fn validate(selector: &Selector) -> StrResult<()> { + match selector { + Selector::Elem(elem, _) => { + if !elem.can::<dyn Locatable>() || elem.can::<dyn Unqueriable>() { + 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)) + } +} + +impl From<Location> for LocatableSelector { + fn from(loc: Location) -> Self { + Self(Selector::Location(loc)) + } +} + +/// 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 input() -> CastInfo { + CastInfo::Union(vec![ + CastInfo::Type(Type::of::<Symbol>()), + CastInfo::Type(Type::of::<Str>()), + CastInfo::Type(Type::of::<Label>()), + CastInfo::Type(Type::of::<Func>()), + CastInfo::Type(Type::of::<Regex>()), + CastInfo::Type(Type::of::<Selector>()), + ]) + } + + fn output() -> CastInfo { + CastInfo::Type(Type::of::<Selector>()) + } + + fn castable(value: &Value) -> bool { + Symbol::castable(value) + || Str::castable(value) + || Label::castable(value) + || Func::castable(value) + || Regex::castable(value) + || Selector::castable(value) + } +} + +cast! { + ShowableSelector, + self => self.0.into_value(), +} + +impl FromValue for ShowableSelector { + fn from_value(value: Value) -> HintedStrResult<Self> { + fn validate(selector: &Selector, nested: bool) -> HintedStrResult<()> { + match selector { + Selector::Elem(_, _) => {} + Selector::Label(_) => {} + Selector::Regex(_) if !nested => {} + Selector::Or(list) | Selector::And(list) => { + for selector in list { + validate(selector, true)?; + } + } + Selector::Regex(_) + | 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, false)?; + Ok(Self(selector)) + } +} diff --git a/crates/typst-library/src/foundations/str.rs b/crates/typst-library/src/foundations/str.rs new file mode 100644 index 00000000..fc221c49 --- /dev/null +++ b/crates/typst-library/src/foundations/str.rs @@ -0,0 +1,969 @@ +use std::borrow::{Borrow, Cow}; +use std::fmt::{self, Debug, Display, Formatter}; +use std::hash::{Hash, Hasher}; +use std::ops::{Add, AddAssign, Deref, Range}; + +use comemo::Tracked; +use ecow::EcoString; +use serde::{Deserialize, Serialize}; +use typst_syntax::{Span, Spanned}; +use typst_utils::PicoStr; +use unicode_segmentation::UnicodeSegmentation; + +use crate::diag::{bail, At, SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, dict, func, repr, scope, ty, Array, Bytes, Context, Decimal, Dict, Func, + IntoValue, Label, Repr, Type, Value, Version, +}; +use crate::layout::Alignment; + +/// Create a new [`Str`] from a format string. +#[macro_export] +#[doc(hidden)] +macro_rules! __format_str { + ($($tts:tt)*) => {{ + $crate::foundations::Str::from($crate::foundations::eco_format!($($tts)*)) + }}; +} + +#[doc(hidden)] +pub use ecow::eco_format; + +#[doc(inline)] +pub use crate::__format_str as format_str; + +/// A sequence of Unicode codepoints. +/// +/// You can iterate over the grapheme clusters of the string using a [for +/// loop]($scripting/#loops). Grapheme clusters are basically characters but +/// keep together things that belong together, e.g. multiple codepoints that +/// together form a flag emoji. Strings can be added with the `+` operator, +/// [joined together]($scripting/#blocks) and multiplied with integers. +/// +/// Typst provides utility methods for string manipulation. Many of these +/// methods (e.g., `split`, `trim` and `replace`) operate on _patterns:_ A +/// pattern can be either a string or a [regular expression]($regex). This makes +/// the methods quite versatile. +/// +/// All lengths and indices are expressed in terms of UTF-8 bytes. Indices are +/// zero-based and negative indices wrap around to the end of the string. +/// +/// You can convert a value to a string with this type's constructor. +/// +/// # Example +/// ```example +/// #"hello world!" \ +/// #"\"hello\n world\"!" \ +/// #"1 2 3".split() \ +/// #"1,2;3".split(regex("[,;]")) \ +/// #(regex("\d+") in "ten euros") \ +/// #(regex("\d+") in "10 euros") +/// ``` +/// +/// # Escape sequences { #escapes } +/// Just like in markup, you can escape a few symbols in strings: +/// - `[\\]` for a backslash +/// - `[\"]` for a quote +/// - `[\n]` for a newline +/// - `[\r]` for a carriage return +/// - `[\t]` for a tab +/// - `[\u{1f600}]` for a hexadecimal Unicode escape sequence +#[ty(scope, cast, title = "String")] +#[derive(Default, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[derive(Serialize, Deserialize)] +#[serde(transparent)] +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.is_empty() + } + + /// Repeat the string a number of times. + pub fn repeat(&self, n: usize) -> StrResult<Self> { + if self.0.len().checked_mul(n).is_none() { + return Err(eco_format!("cannot repeat this string {n} times")); + } + Ok(Self(self.0.repeat(n))) + } + + /// A string slice containing the entire string. + pub fn as_str(&self) -> &str { + self + } + + /// 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())) + } + + /// Resolve an index, if it is within bounds and on a valid char boundary. + /// + /// `index == len` is considered in bounds. + fn locate_opt(&self, index: i64) -> StrResult<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.is_some_and(|i| !self.0.is_char_boundary(i)) { + return Err(not_a_char_boundary(index)); + } + + Ok(resolved) + } +} + +#[scope] +impl Str { + /// Converts a value to a string. + /// + /// - Integers are formatted in base 10. This can be overridden with the + /// optional `base` parameter. + /// - Floats are formatted in base 10 and never in exponential notation. + /// - From labels the name is extracted. + /// - Bytes are decoded as UTF-8. + /// + /// If you wish to convert from and to Unicode code points, see the + /// [`to-unicode`]($str.to-unicode) and [`from-unicode`]($str.from-unicode) + /// functions. + /// + /// ```example + /// #str(10) \ + /// #str(4000, base: 16) \ + /// #str(2.7) \ + /// #str(1e8) \ + /// #str(<intro>) + /// ``` + #[func(constructor)] + pub fn construct( + /// The value that should be converted to a string. + value: ToStr, + /// The base (radix) to display integers in, between 2 and 36. + #[named] + #[default(Spanned::new(10, Span::detached()))] + base: Spanned<i64>, + ) -> SourceResult<Str> { + Ok(match value { + ToStr::Str(s) => { + if base.v != 10 { + bail!(base.span, "base is only supported for integers"); + } + s + } + ToStr::Int(n) => { + if base.v < 2 || base.v > 36 { + bail!(base.span, "base must be between 2 and 36"); + } + repr::format_int_with_base(n, base.v).into() + } + }) + } + + /// The length of the string in UTF-8 encoded bytes. + #[func(title = "Length")] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Extracts the first grapheme cluster of the string. + /// Fails with an error if the string is empty. + #[func] + pub fn first(&self) -> StrResult<Str> { + self.0 + .graphemes(true) + .next() + .map(Into::into) + .ok_or_else(string_is_empty) + } + + /// Extracts the last grapheme cluster of the string. + /// Fails with an error if the string is empty. + #[func] + pub fn last(&self) -> StrResult<Str> { + self.0 + .graphemes(true) + .next_back() + .map(Into::into) + .ok_or_else(string_is_empty) + } + + /// Extracts the first grapheme cluster after the specified index. Returns + /// the default value if the index is out of bounds or fails with an error + /// if no default value was specified. + #[func] + pub fn at( + &self, + /// The byte index. If negative, indexes from the back. + index: i64, + /// A default value to return if the index is out of bounds. + #[named] + default: Option<Value>, + ) -> StrResult<Value> { + let len = self.len(); + self.locate_opt(index)? + .and_then(|i| self.0[i..].graphemes(true).next().map(|s| s.into_value())) + .or(default) + .ok_or_else(|| no_default_and_out_of_bounds(index, len)) + } + + /// Extracts a substring of the string. + /// Fails with an error if the start or end index is out of bounds. + #[func] + pub fn slice( + &self, + /// The start byte index (inclusive). If negative, indexes from the + /// back. + start: i64, + /// The end byte index (exclusive). If omitted, the whole slice until + /// the end of the string is extracted. If negative, indexes from the + /// back. + #[default] + end: Option<i64>, + /// The number of bytes to extract. This is equivalent to passing + /// `start + count` as the `end` position. Mutually exclusive with `end`. + #[named] + count: Option<i64>, + ) -> StrResult<Str> { + let end = end.or(count.map(|c| start + c)).unwrap_or(self.len() as i64); + let start = self.locate(start)?; + let end = self.locate(end)?.max(start); + Ok(self.0[start..end].into()) + } + + /// Returns the grapheme clusters of the string as an array of substrings. + #[func] + pub fn clusters(&self) -> Array { + self.as_str().graphemes(true).map(|s| Value::Str(s.into())).collect() + } + + /// Returns the Unicode codepoints of the string as an array of substrings. + #[func] + pub fn codepoints(&self) -> Array { + self.chars().map(|c| Value::Str(c.into())).collect() + } + + /// Converts a character into its corresponding code point. + /// + /// ```example + /// #"a".to-unicode() \ + /// #("a\u{0300}" + /// .codepoints() + /// .map(str.to-unicode)) + /// ``` + #[func] + pub fn to_unicode( + /// The character that should be converted. + character: char, + ) -> u32 { + character as u32 + } + + /// Converts a unicode code point into its corresponding string. + /// + /// ```example + /// #str.from-unicode(97) + /// ``` + #[func] + pub fn from_unicode( + /// The code point that should be converted. + value: u32, + ) -> StrResult<Str> { + let c: char = value + .try_into() + .map_err(|_| eco_format!("{value:#x} is not a valid codepoint"))?; + Ok(c.into()) + } + + /// Whether the string contains the specified pattern. + /// + /// This method also has dedicated syntax: You can write `{"bc" in "abcd"}` + /// instead of `{"abcd".contains("bc")}`. + #[func] + pub fn contains( + &self, + /// The pattern to search for. + pattern: StrPattern, + ) -> bool { + match pattern { + StrPattern::Str(pat) => self.0.contains(pat.as_str()), + StrPattern::Regex(re) => re.is_match(self), + } + } + + /// Whether the string starts with the specified pattern. + #[func] + pub fn starts_with( + &self, + /// The pattern the string might start with. + pattern: StrPattern, + ) -> bool { + match pattern { + StrPattern::Str(pat) => self.0.starts_with(pat.as_str()), + StrPattern::Regex(re) => re.find(self).is_some_and(|m| m.start() == 0), + } + } + + /// Whether the string ends with the specified pattern. + #[func] + pub fn ends_with( + &self, + /// The pattern the string might end with. + pattern: StrPattern, + ) -> bool { + match pattern { + StrPattern::Str(pat) => self.0.ends_with(pat.as_str()), + StrPattern::Regex(re) => { + let mut start_byte = 0; + while let Some(mat) = re.find_at(self, start_byte) { + if mat.end() == self.0.len() { + return true; + } + + // There might still be a match overlapping this one, so + // restart at the next code point. + let Some(c) = self[mat.start()..].chars().next() else { break }; + start_byte = mat.start() + c.len_utf8(); + } + false + } + } + } + + /// Searches for the specified pattern in the string and returns the first + /// match as a string or `{none}` if there is no match. + #[func] + pub fn find( + &self, + /// The pattern to search for. + pattern: StrPattern, + ) -> Option<Str> { + 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()), + } + } + + /// Searches for the specified pattern in the string and returns the index + /// of the first match as an integer or `{none}` if there is no match. + #[func] + pub fn position( + &self, + /// The pattern to search for. + pattern: StrPattern, + ) -> Option<usize> { + match pattern { + StrPattern::Str(pat) => self.0.find(pat.as_str()), + StrPattern::Regex(re) => re.find(self).map(|m| m.start()), + } + } + + /// Searches for the specified pattern in the string and returns a + /// dictionary with details about the first match or `{none}` if there is no + /// match. + /// + /// The returned dictionary has the following keys: + /// - `start`: The start offset of the match + /// - `end`: The end offset of the match + /// - `text`: The text that matched. + /// - `captures`: An array containing a string for each matched capturing + /// group. The first item of the array contains the first matched + /// capturing, not the whole match! This is empty unless the `pattern` was + /// a regex with capturing groups. + #[func] + pub fn match_( + &self, + /// The pattern to search for. + 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), + } + } + + /// Searches for the specified pattern in the string and returns an array of + /// dictionaries with details about all matches. For details about the + /// returned dictionaries, see above. + #[func] + pub fn matches( + &self, + /// The pattern to search for. + 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(), + } + } + + /// 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. + #[func] + pub fn replace( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The pattern to search for. + pattern: StrPattern, + /// The string to replace the matches with or a function that gets a + /// dictionary for each match and can return individual replacement + /// strings. + replacement: Replacement, + /// If given, only the first `count` matches of the pattern are placed. + #[named] + count: Option<usize>, + ) -> SourceResult<Str> { + // 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 &replacement { + Replacement::Str(s) => output.push_str(s), + Replacement::Func(func) => { + let piece = func + .call(engine, context, [dict])? + .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()) + } + + /// Removes matches of a pattern from one or both sides of the string, once or + /// repeatedly and returns the resulting string. + #[func] + pub fn trim( + &self, + /// The pattern to search for. If `{none}`, trims white spaces. + #[default] + pattern: Option<StrPattern>, + /// Can be `{start}` or `{end}` to only trim the start or end of the + /// string. If omitted, both sides are trimmed. + #[named] + at: Option<StrSide>, + /// Whether to repeatedly removes matches of the pattern or just once. + /// Defaults to `{true}`. + #[named] + #[default(true)] + repeat: bool, + ) -> Str { + 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 = None; + 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 == Some(m.start()); + + // As long as we're at the beginning or in a consecutive run + // of matches, and we're still trimming at the start, trim. + start &= m.start() == 0 || 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 = Some(m.end()); + } + + // Is the last match directly at the end? + if last.is_some_and(|last| last < s.len()) { + range.end = s.len(); + } + + &s[range.start..range.start.max(range.end)] + } + }; + + trimmed.into() + } + + /// Splits a string at matches of a specified pattern and returns an array + /// of the resulting parts. + #[func] + pub fn split( + &self, + /// The pattern to split at. Defaults to whitespace. + #[default] + 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() + } + } + } + + /// Reverse the string. + #[func(title = "Reverse")] + pub fn rev(&self) -> Str { + let mut s = EcoString::with_capacity(self.0.len()); + for grapheme in self.as_str().graphemes(true).rev() { + s.push_str(grapheme); + } + s.into() + } +} + +impl Deref for Str { + type Target = str; + + fn deref(&self) -> &str { + &self.0 + } +} + +impl Debug for Str { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Debug::fmt(self.as_str(), f) + } +} + +impl Display for Str { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Display::fmt(self.as_str(), f) + } +} + +impl Repr for Str { + fn repr(&self) -> EcoString { + self.as_ref().repr() + } +} + +impl Repr for EcoString { + fn repr(&self) -> EcoString { + self.as_ref().repr() + } +} + +impl Repr for str { + fn repr(&self) -> EcoString { + let mut r = EcoString::with_capacity(self.len() + 2); + r.push('"'); + for c in self.chars() { + match c { + '\0' => r.push_str(r"\u{0}"), + '\'' => r.push('\''), + '"' => r.push_str(r#"\""#), + _ => c.escape_debug().for_each(|c| r.push(c)), + } + } + r.push('"'); + r + } +} + +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! { + PicoStr, + self => Value::Str(self.resolve().into()), + v: Str => v.as_str().into(), +} + +cast! { + String, + self => Value::Str(self.into()), + v: Str => v.into(), +} + +/// A value that can be cast to a string. +pub enum ToStr { + /// A string value ready to be used as-is. + Str(Str), + /// An integer about to be formatted in a given base. + Int(i64), +} + +cast! { + ToStr, + v: i64 => Self::Int(v), + v: f64 => Self::Str(repr::display_float(v).into()), + v: Decimal => Self::Str(format_str!("{}", v)), + v: Version => Self::Str(format_str!("{}", v)), + v: Bytes => Self::Str( + std::str::from_utf8(&v) + .map_err(|_| "bytes are not valid utf-8")? + .into() + ), + v: Label => Self::Str(v.as_str().into()), + v: Type => Self::Str(v.long_name().into()), + v: Str => Self::Str(v), +} + +/// 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>(), + } +} + +/// 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() +} + +/// A regular expression. +/// +/// Can be used as a [show rule selector]($styling/#show-rules) and with +/// [string methods]($str) like `find`, `split`, and `replace`. +/// +/// [See here](https://docs.rs/regex/latest/regex/#syntax) for a specification +/// of the supported syntax. +/// +/// # Example +/// ```example +/// // Works with string methods. +/// #"a,b;c".split(regex("[,;]")) +/// +/// // Works with show rules. +/// #show regex("\d+"): set text(red) +/// +/// The numbers 1 to 10. +/// ``` +#[ty(scope)] +#[derive(Debug, 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}")) + } +} + +#[scope] +impl Regex { + /// Create a regular expression from a string. + #[func(constructor)] + pub fn construct( + /// The regular expression as a string. + /// + /// Most regex escape sequences just work because they are not valid Typst + /// escape sequences. To produce regex escape sequences that are also valid in + /// Typst (e.g. `[\\]`), you need to escape twice. Thus, to match a verbatim + /// backslash, you would need to write `{regex("\\\\")}`. + /// + /// If you need many escape sequences, you can also create a raw element + /// and extract its text to use it for your regular expressions: + /// ```{regex(`\d+\.\d+\.\d+`.text)}```. + regex: Spanned<Str>, + ) -> SourceResult<Regex> { + Self::new(®ex.v).at(regex.span) + } +} + +impl Deref for Regex { + type Target = regex::Regex; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Repr for Regex { + fn repr(&self) -> EcoString { + eco_format!("regex({})", self.0.as_str().repr()) + } +} + +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); + } +} + +/// 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, + self => match self { + Self::Str(v) => v.into_value(), + Self::Regex(v) => v.into_value(), + }, + v: Str => Self::Str(v), + v: Regex => Self::Regex(v), +} + +/// 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, + v: Alignment => match v { + Alignment::START => Self::Start, + Alignment::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, + self => match self { + Self::Str(v) => v.into_value(), + Self::Func(v) => v.into_value(), + }, + v: Str => Self::Str(v), + v: Func => Self::Func(v) +} diff --git a/crates/typst-library/src/foundations/styles.rs b/crates/typst-library/src/foundations/styles.rs new file mode 100644 index 00000000..af4909e5 --- /dev/null +++ b/crates/typst-library/src/foundations/styles.rs @@ -0,0 +1,1034 @@ +use std::any::{Any, TypeId}; +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::{mem, ptr}; + +use comemo::{Track, Tracked}; +use ecow::{eco_vec, EcoString, EcoVec}; +use smallvec::SmallVec; +use typst_syntax::Span; +use typst_utils::LazyHash; + +use crate::diag::{warning, SourceResult, Trace, Tracepoint}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, func, ty, Content, Context, Element, Func, NativeElement, Packed, Repr, + Selector, Show, +}; +use crate::introspection::Locatable; +use crate::text::{FontFamily, FontList, TextElem}; + +/// Provides access to active styles. +/// +/// **Deprecation planned.** Use [context] instead. +/// +/// ```example +/// #let thing(body) = style(styles => { +/// let size = measure(body, styles) +/// [Width of "#body" is #size.width] +/// }) +/// +/// #thing[Hey] \ +/// #thing[Welcome] +/// ``` +#[func] +pub fn style( + /// The engine. + engine: &mut Engine, + /// The call site span. + span: Span, + /// A function to call with the styles. Its return value is displayed + /// in the document. + /// + /// This function is called once for each time the content returned by + /// `style` appears in the document. That makes it possible to generate + /// content that depends on the style context it appears in. + func: Func, +) -> Content { + engine.sink.warn(warning!( + span, "`style` is deprecated"; + hint: "use a `context` expression instead" + )); + + StyleElem::new(func).pack().spanned(span) +} + +/// Executes a style access. +#[elem(Locatable, Show)] +struct StyleElem { + /// The function to call with the styles. + #[required] + func: Func, +} + +impl Show for Packed<StyleElem> { + #[typst_macros::time(name = "style", span = self.span())] + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let context = Context::new(self.location(), Some(styles)); + Ok(self + .func() + .call(engine, context.track(), [styles.to_map()])? + .display()) + } +} + +/// A list of style properties. +#[ty(cast)] +#[derive(Default, PartialEq, Clone, Hash)] +pub struct Styles(EcoVec<LazyHash<Style>>); + +impl Styles { + /// Create a new, empty style list. + pub const fn new() -> Self { + Self(EcoVec::new()) + } + + /// Whether this contains no styles. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Iterate over the contained styles. + pub fn iter(&self) -> impl Iterator<Item = &Style> { + self.0.iter().map(|style| &**style) + } + + /// Iterate over the contained styles. + pub fn as_slice(&self) -> &[LazyHash<Style>] { + self.0.as_slice() + } + + /// 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(LazyHash::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); + *self = outer; + } + + /// Apply one outer styles. + pub fn apply_one(&mut self, outer: Style) { + self.0.insert(0, LazyHash::new(outer)); + } + + /// Add an origin span to all contained properties. + pub fn spanned(mut self, span: Span) -> Self { + for entry in self.0.make_mut() { + if let Style::Property(property) = &mut **entry { + property.span = span; + } + } + self + } + + /// Marks the styles as having been applied outside of any show rule. + pub fn outside(mut self) -> Self { + for entry in self.0.make_mut() { + match &mut **entry { + Style::Property(property) => property.outside = true, + Style::Recipe(recipe) => recipe.outside = true, + _ => {} + } + } + self + } + + /// Marks the styles as being allowed to be lifted up to the page level. + pub fn liftable(mut self) -> Self { + for entry in self.0.make_mut() { + if let Style::Property(property) = &mut **entry { + property.liftable = true; + } + } + self + } + + /// Whether there is a style for the given field of the given element. + pub fn has<T: NativeElement>(&self, field: u8) -> bool { + let elem = T::elem(); + self.0 + .iter() + .filter_map(|style| style.property()) + .any(|property| property.is_of(elem) && property.id == field) + } + + /// Set a font family composed of a preferred family and existing families + /// from a style chain. + pub fn set_family(&mut self, preferred: FontFamily, existing: StyleChain) { + self.set(TextElem::set_font(FontList( + std::iter::once(preferred) + .chain(TextElem::font_in(existing).into_iter().cloned()) + .collect(), + ))); + } +} + +impl From<LazyHash<Style>> for Styles { + fn from(style: LazyHash<Style>) -> Self { + Self(eco_vec![style]) + } +} + +impl From<Style> for Styles { + fn from(style: Style) -> Self { + Self(eco_vec![LazyHash::new(style)]) + } +} + +impl IntoIterator for Styles { + type Item = LazyHash<Style>; + type IntoIter = ecow::vec::IntoIter<Self::Item>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl FromIterator<LazyHash<Style>> for Styles { + fn from_iter<T: IntoIterator<Item = LazyHash<Style>>>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + +impl Debug for Styles { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Styles ")?; + f.debug_list().entries(&self.0).finish() + } +} + +impl Repr for Styles { + fn repr(&self) -> EcoString { + "..".into() + } +} + +/// A single style property or recipe. +#[derive(Clone, Hash)] +pub enum Style { + /// A style property originating from a set rule or constructor. + Property(Property), + /// A show rule recipe. + Recipe(Recipe), + /// Disables a specific show rule recipe. + /// + /// Note: This currently only works for regex recipes since it's the only + /// place we need it for the moment. Normal show rules use guards directly + /// on elements instead. + Revocation(RecipeIndex), +} + +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, + } + } + + /// The style's span, if any. + pub fn span(&self) -> Span { + match self { + Self::Property(property) => property.span, + Self::Recipe(recipe) => recipe.span, + Self::Revocation(_) => Span::detached(), + } + } + + /// Returns `Some(_)` with an optional span if this style is for + /// the given element. + pub fn element(&self) -> Option<Element> { + match self { + Style::Property(property) => Some(property.elem), + Style::Recipe(recipe) => match recipe.selector { + Some(Selector::Elem(elem, _)) => Some(elem), + _ => None, + }, + Style::Revocation(_) => None, + } + } + + /// Whether the style is allowed to be lifted up to the page level. Only + /// true for styles originating from set rules. + pub fn liftable(&self) -> bool { + match self { + Self::Property(property) => property.liftable, + Self::Recipe(_) => true, + Self::Revocation(_) => false, + } + } + + /// Whether the style was applied outside of any show rule. This is set + /// during realization. + pub fn outside(&self) -> bool { + match self { + Self::Property(property) => property.outside, + Self::Recipe(recipe) => recipe.outside, + Self::Revocation(_) => false, + } + } + + /// Turn this style into prehashed style. + pub fn wrap(self) -> LazyHash<Style> { + LazyHash::new(self) + } +} + +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), + Self::Revocation(guard) => guard.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, Hash)] +pub struct Property { + /// The element the property belongs to. + elem: Element, + /// The property's ID. + id: u8, + /// The property's value. + value: Block, + /// The span of the set rule the property stems from. + span: Span, + /// Whether the property is allowed to be lifted up to the page level. + liftable: bool, + /// Whether the property was applied outside of any show rule. + outside: bool, +} + +impl Property { + /// Create a new property from a key-value pair. + pub fn new<E, T>(id: u8, value: T) -> Self + where + E: NativeElement, + T: Debug + Clone + Hash + Send + Sync + 'static, + { + Self { + elem: E::elem(), + id, + value: Block::new(value), + span: Span::detached(), + liftable: false, + outside: false, + } + } + + /// Whether this property is the given one. + pub fn is(&self, elem: Element, id: u8) -> bool { + self.elem == elem && self.id == id + } + + /// Whether this property belongs to the given element. + pub fn is_of(&self, elem: Element) -> bool { + self.elem == elem + } + + /// Turn this property into prehashed style. + pub fn wrap(self) -> LazyHash<Style> { + Style::Property(self).wrap() + } +} + +impl Debug for Property { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!( + f, + "Set({}.{}: ", + self.elem.name(), + self.elem.field_name(self.id).unwrap() + )?; + self.value.fmt(f)?; + write!(f, ")") + } +} + +/// A block storage for storing style values. +/// +/// We're using a `Box` since values will either be contained in an `Arc` and +/// therefore already on the heap or they will be small enough that we can just +/// clone them. +#[derive(Hash)] +struct Block(Box<dyn Blockable>); + +impl Block { + /// Creates a new block. + fn new<T: Blockable>(value: T) -> Self { + Self(Box::new(value)) + } + + /// Downcasts the block to the specified type. + fn downcast<T: 'static>(&self) -> Option<&T> { + self.0.as_any().downcast_ref() + } +} + +impl Debug for Block { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Clone for Block { + fn clone(&self) -> Self { + self.0.dyn_clone() + } +} + +/// A value that can be stored in a block. +/// +/// Auto derived for all types that implement [`Any`], [`Clone`], [`Hash`], +/// [`Debug`], [`Send`] and [`Sync`]. +trait Blockable: Debug + Send + Sync + 'static { + /// Equivalent to `downcast_ref` for the block. + fn as_any(&self) -> &dyn Any; + + /// Equivalent to [`Hash`] for the block. + fn dyn_hash(&self, state: &mut dyn Hasher); + + /// Equivalent to [`Clone`] for the block. + fn dyn_clone(&self) -> Block; +} + +impl<T: Debug + Clone + Hash + Send + Sync + 'static> Blockable for T { + fn as_any(&self) -> &dyn Any { + self + } + + fn dyn_hash(&self, mut state: &mut dyn Hasher) { + // Also hash the TypeId since values with different types but + // equal data should be different. + TypeId::of::<Self>().hash(&mut state); + self.hash(&mut state); + } + + fn dyn_clone(&self) -> Block { + Block(Box::new(self.clone())) + } +} + +impl Hash for dyn Blockable { + fn hash<H: Hasher>(&self, state: &mut H) { + self.dyn_hash(state); + } +} + +/// A show rule recipe. +#[derive(Clone, PartialEq, Hash)] +pub struct Recipe { + /// Determines whether the recipe applies to an element. + /// + /// If this is `None`, then this recipe is from a show rule with + /// no selector (`show: rest => ...`), which is [eagerly applied][Content::styled_with_recipe] + /// to the rest of the content in the scope. + selector: Option<Selector>, + /// The transformation to perform on the match. + transform: Transformation, + /// The span that errors are reported with. + span: Span, + /// Relevant properties of the kind of construct the style originated from + /// and where it was applied. + outside: bool, +} + +impl Recipe { + /// Create a new recipe from a key-value pair. + pub fn new( + selector: Option<Selector>, + transform: Transformation, + span: Span, + ) -> Self { + Self { selector, transform, span, outside: false } + } + + /// The recipe's selector. + pub fn selector(&self) -> Option<&Selector> { + self.selector.as_ref() + } + + /// The recipe's transformation. + pub fn transform(&self) -> &Transformation { + &self.transform + } + + /// The recipe's span. + pub fn span(&self) -> Span { + self.span + } + + /// Apply the recipe to the given content. + pub fn apply( + &self, + engine: &mut Engine, + context: Tracked<Context>, + content: Content, + ) -> SourceResult<Content> { + let mut content = match &self.transform { + Transformation::Content(content) => content.clone(), + Transformation::Func(func) => { + let mut result = func.call(engine, context, [content.clone()]); + if self.selector.is_some() { + let point = || Tracepoint::Show(content.func().name().into()); + result = result.trace(engine.world, point, content.span()); + } + result?.display() + } + Transformation::Style(styles) => content.styled_with_map(styles.clone()), + }; + if content.span().is_detached() { + content = content.spanned(self.span); + } + Ok(content) + } +} + +impl Debug for Recipe { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Show(")?; + if let Some(selector) = &self.selector { + selector.fmt(f)?; + f.write_str(", ")?; + } + self.transform.fmt(f) + } +} + +/// Identifies a show rule recipe from the top of the chain. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct RecipeIndex(pub usize); + +/// A show rule transformation that can be applied to a match. +#[derive(Clone, PartialEq, Hash)] +pub enum Transformation { + /// Replacement content. + Content(Content), + /// A function to apply to the match. + Func(Func), + /// Apply styles to the content. + Style(Styles), +} + +impl Debug for Transformation { + 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! { + Transformation, + 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 [LazyHash<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 chainable 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, C>(&'b self, local: &'b C) -> StyleChain<'b> + where + C: Chainable + ?Sized, + { + Chainable::chain(local, self) + } + + /// Cast the first value for the given property in the chain. + pub fn get<T: Clone + 'static>( + self, + func: Element, + id: u8, + inherent: Option<&T>, + default: impl Fn() -> T, + ) -> T { + self.properties::<T>(func, id, inherent) + .next() + .cloned() + .unwrap_or_else(default) + } + + /// Cast the first value for the given property in the chain, + /// returning a borrowed value. + pub fn get_ref<T: 'static>( + self, + func: Element, + id: u8, + inherent: Option<&'a T>, + default: impl Fn() -> &'a T, + ) -> &'a T { + self.properties::<T>(func, id, inherent) + .next() + .unwrap_or_else(default) + } + + /// Cast the first value for the given property in the chain, taking + /// `Fold` implementations into account. + pub fn get_folded<T: Fold + Clone + 'static>( + self, + func: Element, + id: u8, + inherent: Option<&T>, + default: impl Fn() -> T, + ) -> T { + fn next<T: Fold>( + mut values: impl Iterator<Item = T>, + default: &impl Fn() -> T, + ) -> T { + values + .next() + .map(|value| value.fold(next(values, default))) + .unwrap_or_else(default) + } + next(self.properties::<T>(func, id, inherent).cloned(), &default) + } + + /// Iterate over all values for the given property in the chain. + fn properties<T: 'static>( + self, + func: Element, + id: u8, + inherent: Option<&'a T>, + ) -> impl Iterator<Item = &'a T> { + inherent.into_iter().chain( + self.entries() + .filter_map(|style| style.property()) + .filter(move |property| property.is(func, id)) + .map(|property| &property.value) + .map(move |value| { + value.downcast().unwrap_or_else(|| { + panic!( + "attempted to read a value of a different type than was written {}.{}: {:?}", + func.name(), + func.field_name(id).unwrap(), + value + ) + }) + }), + ) + } + + /// Iterate over the entries of the chain. + pub fn entries(self) -> Entries<'a> { + Entries { inner: [].as_slice().iter(), links: self.links() } + } + + /// Iterate over the recipes in the chain. + pub fn recipes(self) -> impl Iterator<Item = &'a Recipe> { + self.entries().filter_map(|style| style.recipe()) + } + + /// Iterate over the links of the chain. + pub fn links(self) -> Links<'a> { + Links(Some(self)) + } + + /// Convert to a style map. + pub fn to_map(self) -> Styles { + let mut styles: EcoVec<_> = self.entries().cloned().collect(); + styles.make_mut().reverse(); + Styles(styles) + } + + /// Build owned styles from the suffix (all links beyond the `len`) of the + /// chain. + pub fn suffix(self, len: usize) -> Styles { + let mut styles = EcoVec::new(); + let take = self.links().count().saturating_sub(len); + for link in self.links().take(take) { + styles.extend(link.iter().cloned().rev()); + } + styles.make_mut().reverse(); + Styles(styles) + } + + /// Remove the last link from the chain. + pub fn pop(&mut self) { + *self = self.tail.copied().unwrap_or_default(); + } + + /// Determine the shared trunk of a collection of style chains. + pub fn trunk(iter: impl IntoIterator<Item = Self>) -> Option<Self> { + // Determine shared style depth and first span. + let mut iter = iter.into_iter(); + let mut trunk = iter.next()?; + let mut depth = trunk.links().count(); + + for mut chain in iter { + let len = chain.links().count(); + if len < depth { + for _ in 0..depth - len { + trunk.pop(); + } + depth = len; + } else if len > depth { + for _ in 0..len - depth { + chain.pop(); + } + } + + while depth > 0 && chain != trunk { + trunk.pop(); + chain.pop(); + depth -= 1; + } + } + + Some(trunk) + } +} + +impl Debug for StyleChain<'_> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("StyleChain ")?; + f.debug_list() + .entries(self.entries().collect::<Vec<_>>().into_iter().rev()) + .finish() + } +} + +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, + } + } +} + +/// Things that can be attached to a style chain. +pub trait Chainable { + /// Attach `self` as the first link of the chain. + fn chain<'a>(&'a self, outer: &'a StyleChain<'_>) -> StyleChain<'a>; +} + +impl Chainable for LazyHash<Style> { + fn chain<'a>(&'a self, outer: &'a StyleChain<'_>) -> StyleChain<'a> { + StyleChain { + head: std::slice::from_ref(self), + tail: Some(outer), + } + } +} + +impl Chainable for [LazyHash<Style>] { + fn chain<'a>(&'a self, outer: &'a StyleChain<'_>) -> StyleChain<'a> { + if self.is_empty() { + *outer + } else { + StyleChain { head: self, tail: Some(outer) } + } + } +} + +impl<const N: usize> Chainable for [LazyHash<Style>; N] { + fn chain<'a>(&'a self, outer: &'a StyleChain<'_>) -> StyleChain<'a> { + Chainable::chain(self.as_slice(), outer) + } +} + +impl Chainable for Styles { + fn chain<'a>(&'a self, outer: &'a StyleChain<'_>) -> StyleChain<'a> { + Chainable::chain(self.0.as_slice(), outer) + } +} + +/// An iterator over the entries in a style chain. +pub struct Entries<'a> { + inner: std::slice::Iter<'a, LazyHash<Style>>, + links: Links<'a>, +} + +impl<'a> Iterator for Entries<'a> { + type Item = &'a LazyHash<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. +pub struct Links<'a>(Option<StyleChain<'a>>); + +impl<'a> Iterator for Links<'a> { + type Item = &'a [LazyHash<Style>]; + + fn next(&mut self) -> Option<Self::Item> { + let StyleChain { head, tail } = self.0?; + self.0 = tail.copied(); + Some(head) + } +} + +/// A sequence of elements with associated styles. +#[derive(Clone, PartialEq, Hash)] +pub struct StyleVec { + /// The elements themselves. + elements: EcoVec<Content>, + /// A run-length encoded list of style lists. + /// + /// Each element is a (styles, count) pair. Any elements whose + /// style falls after the end of this list is considered to + /// have an empty style list. + styles: EcoVec<(Styles, usize)>, +} + +impl StyleVec { + /// Create a style vector from an unstyled vector content. + pub fn wrap(elements: EcoVec<Content>) -> Self { + Self { elements, styles: EcoVec::new() } + } + + /// Create a `StyleVec` from a list of content with style chains. + pub fn create<'a>(buf: &[(&'a Content, StyleChain<'a>)]) -> (Self, StyleChain<'a>) { + let trunk = StyleChain::trunk(buf.iter().map(|&(_, s)| s)).unwrap_or_default(); + let depth = trunk.links().count(); + + let mut elements = EcoVec::with_capacity(buf.len()); + let mut styles = EcoVec::<(Styles, usize)>::new(); + let mut last: Option<(StyleChain<'a>, usize)> = None; + + for &(element, chain) in buf { + elements.push(element.clone()); + + if let Some((prev, run)) = &mut last { + if chain == *prev { + *run += 1; + } else { + styles.push((prev.suffix(depth), *run)); + last = Some((chain, 1)); + } + } else { + last = Some((chain, 1)); + } + } + + if let Some((last, run)) = last { + let skippable = styles.is_empty() && last == trunk; + if !skippable { + styles.push((last.suffix(depth), run)); + } + } + + (StyleVec { elements, styles }, trunk) + } + + /// Whether there are no elements. + pub fn is_empty(&self) -> bool { + self.elements.is_empty() + } + + /// The number of elements. + pub fn len(&self) -> usize { + self.elements.len() + } + + /// Iterate over the contained content and style chains. + pub fn iter<'a>( + &'a self, + outer: &'a StyleChain<'_>, + ) -> impl Iterator<Item = (&'a Content, StyleChain<'a>)> { + static EMPTY: Styles = Styles::new(); + self.elements + .iter() + .zip( + self.styles + .iter() + .flat_map(|(local, count)| std::iter::repeat(local).take(*count)) + .chain(std::iter::repeat(&EMPTY)), + ) + .map(|(element, local)| (element, outer.chain(local))) + } + + /// Get a style property, but only if it is the same for all children of the + /// style vector. + pub fn shared_get<T: PartialEq>( + &self, + styles: StyleChain<'_>, + getter: fn(StyleChain) -> T, + ) -> Option<T> { + let value = getter(styles); + self.styles + .iter() + .all(|(local, _)| getter(styles.chain(local)) == value) + .then_some(value) + } +} + +impl Debug for StyleVec { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + f.debug_list().entries(&self.elements).finish() + } +} + +/// 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 { + /// Fold this inner value with an outer folded value. + fn fold(self, outer: Self) -> Self; +} + +impl Fold for bool { + fn fold(self, _: Self) -> Self { + self + } +} + +impl<T: Fold> Fold for Option<T> { + fn fold(self, outer: Self) -> Self { + match (self, outer) { + (Some(inner), Some(outer)) => Some(inner.fold(outer)), + // An explicit `None` should be respected, thus we don't do + // `inner.or(outer)`. + (inner, _) => inner, + } + } +} + +impl<T> Fold for Vec<T> { + fn fold(self, mut outer: Self) -> Self { + outer.extend(self); + outer + } +} + +impl<T, const N: usize> Fold for SmallVec<[T; N]> { + fn fold(self, mut outer: Self) -> Self { + outer.extend(self); + outer + } +} + +/// A variant of fold for foldable optional (`Option<T>`) values where an inner +/// `None` value isn't respected (contrary to `Option`'s usual `Fold` +/// implementation, with which folding with an inner `None` always returns +/// `None`). Instead, when either of the `Option` objects is `None`, the other +/// one is necessarily returned by `fold_or`. Normal folding still occurs when +/// both values are `Some`, using `T`'s `Fold` implementation. +/// +/// This is useful when `None` in a particular context means "unspecified" +/// rather than "absent", in which case a specified value (`Some`) is chosen +/// over an unspecified one (`None`), while two specified values are folded +/// together. +pub trait AlternativeFold { + /// Attempts to fold this inner value with an outer value. However, if + /// either value is `None`, returns the other one instead of folding. + fn fold_or(self, outer: Self) -> Self; +} + +impl<T: Fold> AlternativeFold for Option<T> { + fn fold_or(self, outer: Self) -> Self { + match (self, outer) { + (Some(inner), Some(outer)) => Some(inner.fold(outer)), + // If one of values is `None`, return the other one instead of + // folding. + (inner, outer) => inner.or(outer), + } + } +} + +/// A type that accumulates depth when folded. +#[derive(Debug, Default, Clone, Copy, PartialEq, Hash)] +pub struct Depth(pub usize); + +impl Fold for Depth { + fn fold(self, outer: Self) -> Self { + Self(outer.0 + self.0) + } +} diff --git a/crates/typst-library/src/foundations/symbol.rs b/crates/typst-library/src/foundations/symbol.rs new file mode 100644 index 00000000..86676fa2 --- /dev/null +++ b/crates/typst-library/src/foundations/symbol.rs @@ -0,0 +1,356 @@ +#[doc(inline)] +pub use typst_macros::symbols; + +use std::cmp::Reverse; +use std::collections::BTreeSet; +use std::fmt::{self, Debug, Display, Formatter, Write}; +use std::sync::Arc; + +use ecow::{eco_format, EcoString}; +use serde::{Serialize, Serializer}; +use typst_syntax::{Span, Spanned}; + +use crate::diag::{bail, SourceResult, StrResult}; +use crate::foundations::{cast, func, scope, ty, Array, Func}; + +/// A Unicode symbol. +/// +/// Typst defines common symbols so that they can easily be written with +/// standard keyboards. The symbols are defined in modules, from which they can +/// be accessed using [field access notation]($scripting/#fields): +/// +/// - General symbols are defined in the [`sym` module]($category/symbols/sym) +/// - Emoji are defined in the [`emoji` module]($category/symbols/emoji) +/// +/// Moreover, you can define custom symbols with this type's constructor +/// function. +/// +/// ```example +/// #sym.arrow.r \ +/// #sym.gt.eq.not \ +/// $gt.eq.not$ \ +/// #emoji.face.halo +/// ``` +/// +/// Many symbols have different variants, which can be selected by appending the +/// modifiers with dot notation. The order of the modifiers is not relevant. +/// Visit the documentation pages of the symbol modules and click on a symbol to +/// see its available variants. +/// +/// ```example +/// $arrow.l$ \ +/// $arrow.r$ \ +/// $arrow.t.quad$ +/// ``` +#[ty(scope, cast)] +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Symbol(Repr); + +/// The character of a symbol, possibly with a function. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct SymChar(char, Option<fn() -> Func>); + +/// The internal representation. +#[derive(Clone, Eq, PartialEq, Hash)] +enum Repr { + Single(SymChar), + Const(&'static [(&'static str, SymChar)]), + Multi(Arc<(List, EcoString)>), +} + +/// A collection of symbols. +#[derive(Clone, Eq, PartialEq, Hash)] +enum List { + Static(&'static [(&'static str, SymChar)]), + Runtime(Box<[(EcoString, SymChar)]>), +} + +impl Symbol { + /// Create a new symbol from a single character. + pub const fn single(c: SymChar) -> Self { + Self(Repr::Single(c)) + } + + /// Create a symbol with a static variant list. + #[track_caller] + pub const fn list(list: &'static [(&'static str, SymChar)]) -> 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, SymChar)]>) -> Self { + debug_assert!(!list.is_empty()); + Self(Repr::Multi(Arc::new((List::Runtime(list), EcoString::new())))) + } + + /// Get the symbol's char. + pub fn get(&self) -> char { + self.sym().char() + } + + /// Resolve the symbol's `SymChar`. + pub fn sym(&self) -> SymChar { + match &self.0 { + Repr::Single(c) => *c, + Repr::Const(_) => find(self.variants(), "").unwrap(), + Repr::Multi(arc) => find(self.variants(), &arc.1).unwrap(), + } + } + + /// Try to get the function associated with the symbol, if any. + pub fn func(&self) -> StrResult<Func> { + self.sym() + .func() + .ok_or_else(|| eco_format!("symbol {self} is not callable")) + } + + /// 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, SymChar)> { + 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() + } +} + +#[scope] +impl Symbol { + /// Create a custom symbol with modifiers. + /// + /// ```example + /// #let envelope = symbol( + /// "🖂", + /// ("stamped", "🖃"), + /// ("stamped.pen", "🖆"), + /// ("lightning", "🖄"), + /// ("fly", "🖅"), + /// ) + /// + /// #envelope + /// #envelope.stamped + /// #envelope.stamped.pen + /// #envelope.lightning + /// #envelope.fly + /// ``` + #[func(constructor)] + pub fn construct( + /// The callsite span. + span: Span, + /// The variants of the symbol. + /// + /// Can be a just a string consisting of a single character for the + /// modifierless variant or an array with two strings specifying the modifiers + /// and the symbol. Individual modifiers should be separated by dots. When + /// displaying a symbol, Typst selects the first from the variants that have + /// all attached modifiers and the minimum number of other modifiers. + #[variadic] + variants: Vec<Spanned<SymbolVariant>>, + ) -> SourceResult<Symbol> { + let mut list = Vec::new(); + if variants.is_empty() { + bail!(span, "expected at least one variant"); + } + for Spanned { v, span } in variants { + if list.iter().any(|(prev, _)| &v.0 == prev) { + bail!(span, "duplicate variant"); + } + list.push((v.0, SymChar::pure(v.1))); + } + Ok(Symbol::runtime(list.into_boxed_slice())) + } +} + +impl Display for Symbol { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_char(self.get()) + } +} + +impl SymChar { + /// Create a symbol character without a function. + pub const fn pure(c: char) -> Self { + Self(c, None) + } + + /// Create a symbol character with a function. + pub const fn with_func(c: char, func: fn() -> Func) -> Self { + Self(c, Some(func)) + } + + /// Get the character of the symbol. + pub const fn char(&self) -> char { + self.0 + } + + /// Get the function associated with the symbol. + pub fn func(&self) -> Option<Func> { + self.1.map(|f| f()) + } +} + +impl From<char> for SymChar { + fn from(c: char) -> Self { + SymChar(c, None) + } +} + +impl Debug for Repr { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Single(c) => Debug::fmt(c, f), + Self::Const(list) => list.fmt(f), + Self::Multi(lists) => lists.fmt(f), + } + } +} + +impl Debug for List { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Static(list) => list.fmt(f), + Self::Runtime(list) => list.fmt(f), + } + } +} + +impl crate::foundations::Repr for Symbol { + fn repr(&self) -> EcoString { + eco_format!("\"{}\"", self.get()) + } +} + +impl Serialize for Symbol { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_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()), + } + } +} + +/// A value that can be cast to a symbol. +pub struct SymbolVariant(EcoString, char); + +cast! { + SymbolVariant, + c: char => Self(EcoString::new(), c), + array: Array => { + let mut iter = array.into_iter(); + match (iter.next(), iter.next(), iter.next()) { + (Some(a), Some(b), None) => Self(a.cast()?, b.cast()?), + _ => Err("point array must contain exactly two entries")?, + } + }, +} + +/// Iterator over variants. +enum Variants<'a> { + Single(std::option::IntoIter<SymChar>), + Static(std::slice::Iter<'static, (&'static str, SymChar)>), + Runtime(std::slice::Iter<'a, (EcoString, SymChar)>), +} + +impl<'a> Iterator for Variants<'a> { + type Item = (&'a str, SymChar); + + 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, SymChar)>, + modifiers: &str, +) -> Option<SymChar> { + 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/crates/typst-library/src/foundations/sys.rs b/crates/typst-library/src/foundations/sys.rs new file mode 100644 index 00000000..7c128104 --- /dev/null +++ b/crates/typst-library/src/foundations/sys.rs @@ -0,0 +1,18 @@ +//! System-related things. + +use crate::foundations::{Dict, Module, Scope, Version}; + +/// A module with system-related things. +pub fn module(inputs: Dict) -> Module { + let mut scope = Scope::deduplicating(); + scope.define( + "version", + Version::from_iter([ + env!("CARGO_PKG_VERSION_MAJOR").parse::<u32>().unwrap(), + env!("CARGO_PKG_VERSION_MINOR").parse::<u32>().unwrap(), + env!("CARGO_PKG_VERSION_PATCH").parse::<u32>().unwrap(), + ]), + ); + scope.define("inputs", inputs); + Module::new("sys", scope) +} diff --git a/crates/typst-library/src/foundations/ty.rs b/crates/typst-library/src/foundations/ty.rs new file mode 100644 index 00000000..70845dd2 --- /dev/null +++ b/crates/typst-library/src/foundations/ty.rs @@ -0,0 +1,223 @@ +#[doc(inline)] +pub use typst_macros::{scope, ty}; + +use std::cmp::Ordering; +use std::fmt::{self, Debug, Display, Formatter}; + +use ecow::{eco_format, EcoString}; +use once_cell::sync::Lazy; +use typst_utils::Static; + +use crate::diag::StrResult; +use crate::foundations::{ + cast, func, AutoValue, Func, NativeFuncData, NoneValue, Repr, Scope, Value, +}; + +/// Describes a kind of value. +/// +/// To style your document, you need to work with values of different kinds: +/// Lengths specifying the size of your elements, colors for your text and +/// shapes, and more. Typst categorizes these into clearly defined _types_ and +/// tells you where it expects which type of value. +/// +/// Apart from basic types for numeric values and [typical]($int) +/// [types]($float) [known]($str) [from]($array) [programming]($dictionary) +/// languages, Typst provides a special type for [_content._]($content) A value +/// of this type can hold anything that you can enter into your document: Text, +/// elements like headings and shapes, and style information. +/// +/// # Example +/// ```example +/// #let x = 10 +/// #if type(x) == int [ +/// #x is an integer! +/// ] else [ +/// #x is another value... +/// ] +/// +/// An image is of type +/// #type(image("glacier.jpg")). +/// ``` +/// +/// The type of `10` is `int`. Now, what is the type of `int` or even `type`? +/// ```example +/// #type(int) \ +/// #type(type) +/// ``` +/// +/// # Compatibility +/// In Typst 0.7 and lower, the `type` function returned a string instead of a +/// type. Compatibility with the old way will remain for a while to give package +/// authors time to upgrade, but it will be removed at some point. +/// +/// - Checks like `{int == "integer"}` evaluate to `{true}` +/// - Adding/joining a type and string will yield a string +/// - The `{in}` operator on a type and a dictionary will evaluate to `{true}` +/// if the dictionary has a string key matching the type's name +#[ty(scope, cast)] +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct Type(Static<NativeTypeData>); + +impl Type { + /// Get the type for `T`. + pub fn of<T: NativeType>() -> Self { + T::ty() + } + + /// The type's short name, how it is used in code (e.g. `str`). + pub fn short_name(&self) -> &'static str { + self.0.name + } + + /// The type's long name, for use in diagnostics (e.g. `string`). + pub fn long_name(&self) -> &'static str { + self.0.long_name + } + + /// The type's title case name, for use in documentation (e.g. `String`). + pub fn title(&self) -> &'static str { + self.0.title + } + + /// Documentation for the type (as Markdown). + pub fn docs(&self) -> &'static str { + self.0.docs + } + + /// Search keywords for the type. + pub fn keywords(&self) -> &'static [&'static str] { + self.0.keywords + } + + /// This type's constructor function. + pub fn constructor(&self) -> StrResult<Func> { + self.0 + .constructor + .as_ref() + .map(|lazy| Func::from(*lazy)) + .ok_or_else(|| eco_format!("type {self} does not have a constructor")) + } + + /// The type's associated scope that holds sub-definitions. + pub fn scope(&self) -> &'static Scope { + &(self.0).0.scope + } + + /// Get a field from this type's scope, if possible. + pub fn field(&self, field: &str) -> StrResult<&'static Value> { + self.scope() + .get(field) + .ok_or_else(|| eco_format!("type {self} does not contain field `{field}`")) + } +} + +// Type compatibility. +impl Type { + /// The type's backward-compatible name. + pub fn compat_name(&self) -> &str { + self.long_name() + } +} + +#[scope] +impl Type { + /// Determines a value's type. + /// + /// ```example + /// #type(12) \ + /// #type(14.7) \ + /// #type("hello") \ + /// #type(<glacier>) \ + /// #type([Hi]) \ + /// #type(x => x + 1) \ + /// #type(type) + /// ``` + #[func(constructor)] + pub fn construct( + /// The value whose type's to determine. + value: Value, + ) -> Type { + value.ty() + } +} + +impl Debug for Type { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Type({})", self.long_name()) + } +} + +impl Repr for Type { + fn repr(&self) -> EcoString { + if *self == Type::of::<AutoValue>() { + "type(auto)" + } else if *self == Type::of::<NoneValue>() { + "type(none)" + } else { + self.long_name() + } + .into() + } +} + +impl Display for Type { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(self.long_name()) + } +} + +impl Ord for Type { + fn cmp(&self, other: &Self) -> Ordering { + self.long_name().cmp(other.long_name()) + } +} + +impl PartialOrd for Type { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +/// A Typst type that is defined by a native Rust type. +pub trait NativeType { + /// The type's name. + /// + /// In contrast to `data()`, this is usable in const contexts. + const NAME: &'static str; + + /// Get the type for the native Rust type. + fn ty() -> Type { + Type::from(Self::data()) + } + + // Get the type data for the native Rust type. + fn data() -> &'static NativeTypeData; +} + +/// Defines a native type. +#[derive(Debug)] +pub struct NativeTypeData { + /// The type's normal name (e.g. `str`), as exposed to Typst. + pub name: &'static str, + pub long_name: &'static str, + /// The function's title case name (e.g. `String`). + pub title: &'static str, + /// The documentation for this type as a string. + pub docs: &'static str, + /// A list of alternate search terms for this type. + pub keywords: &'static [&'static str], + /// The constructor for this type. + pub constructor: Lazy<Option<&'static NativeFuncData>>, + pub scope: Lazy<Scope>, +} + +impl From<&'static NativeTypeData> for Type { + fn from(data: &'static NativeTypeData) -> Self { + Self(Static(data)) + } +} + +cast! { + &'static NativeTypeData, + self => Type::from(self).into_value(), +} diff --git a/crates/typst-library/src/foundations/value.rs b/crates/typst-library/src/foundations/value.rs new file mode 100644 index 00000000..fbfa5a0e --- /dev/null +++ b/crates/typst-library/src/foundations/value.rs @@ -0,0 +1,761 @@ +use std::any::{Any, TypeId}; +use std::cmp::Ordering; +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +use ecow::{eco_format, EcoString}; +use serde::de::value::{MapAccessDeserializer, SeqAccessDeserializer}; +use serde::de::{Error, MapAccess, SeqAccess, Visitor}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use typst_syntax::{ast, Span}; +use typst_utils::ArcExt; + +use crate::diag::{HintedStrResult, HintedString, StrResult}; +use crate::foundations::{ + fields, ops, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime, + Decimal, Dict, Duration, Fold, FromValue, Func, IntoValue, Label, Module, + NativeElement, NativeType, NoneValue, Plugin, Reflect, Repr, Resolve, Scope, Str, + Styles, Symbol, Type, Version, +}; +use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel}; +use crate::text::{RawContent, RawElem, TextElem}; +use crate::visualize::{Color, Gradient, Pattern}; + +/// 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 gradient value: `gradient.linear(...)`. + Gradient(Gradient), + /// A pattern fill: `pattern(...)`. + Pattern(Pattern), + /// A symbol: `arrow.l`. + Symbol(Symbol), + /// A version. + Version(Version), + /// A string: `"string"`. + Str(Str), + /// Raw bytes. + Bytes(Bytes), + /// A label: `<intro>`. + Label(Label), + /// A datetime + Datetime(Datetime), + /// A decimal value: `decimal("123.4500")` + Decimal(Decimal), + /// A duration + Duration(Duration), + /// 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 type. + Type(Type), + /// A module. + Module(Module), + /// A WebAssembly plugin. + Plugin(Plugin), + /// A dynamic value. + Dyn(Dynamic), +} + +impl Value { + /// Create a new dynamic value. + pub fn dynamic<T>(any: T) -> Self + where + T: Debug + Repr + NativeType + 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::Pt => Abs::pt(v).into_value(), + ast::Unit::Mm => Abs::mm(v).into_value(), + ast::Unit::Cm => Abs::cm(v).into_value(), + ast::Unit::In => Abs::inches(v).into_value(), + ast::Unit::Rad => Angle::rad(v).into_value(), + ast::Unit::Deg => Angle::deg(v).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 type of this value. + pub fn ty(&self) -> Type { + match self { + Self::None => Type::of::<NoneValue>(), + Self::Auto => Type::of::<AutoValue>(), + Self::Bool(_) => Type::of::<bool>(), + Self::Int(_) => Type::of::<i64>(), + Self::Float(_) => Type::of::<f64>(), + Self::Length(_) => Type::of::<Length>(), + Self::Angle(_) => Type::of::<Angle>(), + Self::Ratio(_) => Type::of::<Ratio>(), + Self::Relative(_) => Type::of::<Rel<Length>>(), + Self::Fraction(_) => Type::of::<Fr>(), + Self::Color(_) => Type::of::<Color>(), + Self::Gradient(_) => Type::of::<Gradient>(), + Self::Pattern(_) => Type::of::<Pattern>(), + Self::Symbol(_) => Type::of::<Symbol>(), + Self::Version(_) => Type::of::<Version>(), + Self::Str(_) => Type::of::<Str>(), + Self::Bytes(_) => Type::of::<Bytes>(), + Self::Label(_) => Type::of::<Label>(), + Self::Datetime(_) => Type::of::<Datetime>(), + Self::Decimal(_) => Type::of::<Decimal>(), + Self::Duration(_) => Type::of::<Duration>(), + Self::Content(_) => Type::of::<Content>(), + Self::Styles(_) => Type::of::<Styles>(), + Self::Array(_) => Type::of::<Array>(), + Self::Dict(_) => Type::of::<Dict>(), + Self::Func(_) => Type::of::<Func>(), + Self::Args(_) => Type::of::<Args>(), + Self::Type(_) => Type::of::<Type>(), + Self::Module(_) => Type::of::<Module>(), + Self::Plugin(_) => Type::of::<Module>(), + Self::Dyn(v) => v.ty(), + } + } + + /// Try to cast the value into a specific type. + pub fn cast<T: FromValue>(self) -> HintedStrResult<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::Version(version) => version.component(field).map(Self::Int), + Self::Dict(dict) => dict.get(field).cloned(), + Self::Content(content) => content.field_by_name(field), + Self::Type(ty) => ty.field(field).cloned(), + Self::Func(func) => func.field(field).cloned(), + Self::Module(module) => module.field(field).cloned(), + _ => fields::field(self, field), + } + } + + /// The associated scope, if this is a function, type, or module. + pub fn scope(&self) -> Option<&Scope> { + match self { + Self::Func(func) => func.scope(), + Self::Type(ty) => Some(ty.scope()), + Self::Module(module) => Some(module.scope()), + _ => None, + } + } + + /// The name, if this is a function, type, or module. + pub fn name(&self) -> Option<&str> { + match self { + Self::Func(func) => func.name(), + Self::Type(ty) => Some(ty.short_name()), + Self::Module(module) => Some(module.name()), + _ => None, + } + } + + /// Try to extract documentation for the value. + pub fn docs(&self) -> Option<&'static str> { + match self { + Self::Func(func) => func.docs(), + Self::Type(ty) => Some(ty.docs()), + _ => None, + } + } + + /// Return the display representation of the value. + pub fn display(self) -> Content { + match self { + Self::None => Content::empty(), + Self::Int(v) => TextElem::packed(repr::format_int_with_base(v, 10)), + Self::Float(v) => TextElem::packed(repr::display_float(v)), + Self::Decimal(v) => TextElem::packed(eco_format!("{v}")), + Self::Str(v) => TextElem::packed(v), + Self::Version(v) => TextElem::packed(eco_format!("{v}")), + Self::Symbol(v) => TextElem::packed(v.get()), + Self::Content(v) => v, + Self::Module(module) => module.content(), + _ => RawElem::new(RawContent::Text(self.repr())) + .with_lang(Some("typc".into())) + .with_block(false) + .pack(), + } + } + + /// 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, + } + } +} + +impl Debug for Value { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::None => Debug::fmt(&NoneValue, f), + Self::Auto => Debug::fmt(&AutoValue, f), + 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::Gradient(v) => Debug::fmt(v, f), + Self::Pattern(v) => Debug::fmt(v, f), + Self::Symbol(v) => Debug::fmt(v, f), + Self::Version(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::Datetime(v) => Debug::fmt(v, f), + Self::Decimal(v) => Debug::fmt(v, f), + Self::Duration(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::Type(v) => Debug::fmt(v, f), + Self::Module(v) => Debug::fmt(v, f), + Self::Plugin(v) => Debug::fmt(v, f), + Self::Dyn(v) => Debug::fmt(v, f), + } + } +} + +impl Repr for Value { + fn repr(&self) -> EcoString { + match self { + Self::None => NoneValue.repr(), + Self::Auto => AutoValue.repr(), + Self::Bool(v) => v.repr(), + Self::Int(v) => v.repr(), + Self::Float(v) => v.repr(), + Self::Length(v) => v.repr(), + Self::Angle(v) => v.repr(), + Self::Ratio(v) => v.repr(), + Self::Relative(v) => v.repr(), + Self::Fraction(v) => v.repr(), + Self::Color(v) => v.repr(), + Self::Gradient(v) => v.repr(), + Self::Pattern(v) => v.repr(), + Self::Symbol(v) => v.repr(), + Self::Version(v) => v.repr(), + Self::Str(v) => v.repr(), + Self::Bytes(v) => v.repr(), + Self::Label(v) => v.repr(), + Self::Datetime(v) => v.repr(), + Self::Decimal(v) => v.repr(), + Self::Duration(v) => v.repr(), + Self::Content(v) => v.repr(), + Self::Styles(v) => v.repr(), + Self::Array(v) => v.repr(), + Self::Dict(v) => v.repr(), + Self::Func(v) => v.repr(), + Self::Args(v) => v.repr(), + Self::Type(v) => v.repr(), + Self::Module(v) => v.repr(), + Self::Plugin(v) => v.repr(), + Self::Dyn(v) => v.repr(), + } + } +} + +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::Gradient(v) => v.hash(state), + Self::Pattern(v) => v.hash(state), + Self::Symbol(v) => v.hash(state), + Self::Version(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::Datetime(v) => v.hash(state), + Self::Decimal(v) => v.hash(state), + Self::Duration(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::Type(v) => v.hash(state), + Self::Module(v) => v.hash(state), + Self::Plugin(v) => v.hash(state), + Self::Dyn(v) => v.hash(state), + } + } +} + +impl Serialize for Value { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + match self { + Self::None => NoneValue.serialize(serializer), + Self::Bool(v) => v.serialize(serializer), + Self::Int(v) => v.serialize(serializer), + Self::Float(v) => v.serialize(serializer), + Self::Str(v) => v.serialize(serializer), + Self::Bytes(v) => v.serialize(serializer), + Self::Symbol(v) => v.serialize(serializer), + Self::Content(v) => v.serialize(serializer), + Self::Array(v) => v.serialize(serializer), + Self::Dict(v) => v.serialize(serializer), + + // Fall back to repr() for other things. + other => serializer.serialize_str(&other.repr()), + } + } +} + +impl<'de> Deserialize<'de> for Value { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(ValueVisitor) + } +} + +/// Visitor for value deserialization. +struct ValueVisitor; + +impl<'de> Visitor<'de> for ValueVisitor { + type Value = Value; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a typst value") + } + + fn visit_bool<E: Error>(self, v: bool) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_i8<E: Error>(self, v: i8) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_i16<E: Error>(self, v: i16) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_i32<E: Error>(self, v: i32) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_i64<E: Error>(self, v: i64) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_u8<E: Error>(self, v: u8) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_u16<E: Error>(self, v: u16) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_u32<E: Error>(self, v: u32) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_u64<E: Error>(self, v: u64) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_f32<E: Error>(self, v: f32) -> Result<Self::Value, E> { + Ok((v as f64).into_value()) + } + + fn visit_f64<E: Error>(self, v: f64) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_char<E: Error>(self, v: char) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_borrowed_str<E: Error>(self, v: &'de str) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_string<E: Error>(self, v: String) -> Result<Self::Value, E> { + Ok(v.into_value()) + } + + fn visit_bytes<E: Error>(self, v: &[u8]) -> Result<Self::Value, E> { + Ok(Bytes::from(v).into_value()) + } + + fn visit_borrowed_bytes<E: Error>(self, v: &'de [u8]) -> Result<Self::Value, E> { + Ok(Bytes::from(v).into_value()) + } + + fn visit_byte_buf<E: Error>(self, v: Vec<u8>) -> Result<Self::Value, E> { + Ok(Bytes::from(v).into_value()) + } + + fn visit_none<E: Error>(self) -> Result<Self::Value, E> { + Ok(Value::None) + } + + fn visit_some<D: Deserializer<'de>>( + self, + deserializer: D, + ) -> Result<Self::Value, D::Error> { + Value::deserialize(deserializer) + } + + fn visit_unit<E: Error>(self) -> Result<Self::Value, E> { + Ok(Value::None) + } + + fn visit_seq<A: SeqAccess<'de>>(self, seq: A) -> Result<Self::Value, A::Error> { + Ok(Array::deserialize(SeqAccessDeserializer::new(seq))?.into_value()) + } + + fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> { + let dict = Dict::deserialize(MapAccessDeserializer::new(map))?; + Ok(match Datetime::from_toml_dict(&dict) { + None => dict.into_value(), + Some(datetime) => datetime.into_value(), + }) + } +} + +/// A value that is not part of the built-in enum. +#[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: Debug + Repr + NativeType + PartialEq + Hash + Sync + Send + 'static, + { + Self(Arc::new(any)) + } + + /// Whether the wrapped type is `T`. + pub fn is<T: 'static>(&self) -> bool { + (*self.0).as_any().is::<T>() + } + + /// Try to downcast to a reference to a specific type. + pub fn downcast<T: 'static>(&self) -> Option<&T> { + (*self.0).as_any().downcast_ref() + } + + /// The name of the stored value's type. + pub fn ty(&self) -> Type { + self.0.dyn_ty() + } +} + +impl Debug for Dynamic { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Repr for Dynamic { + fn repr(&self) -> EcoString { + self.0.repr() + } +} + +impl PartialEq for Dynamic { + fn eq(&self, other: &Self) -> bool { + self.0.dyn_eq(other) + } +} + +trait Bounds: Debug + Repr + Sync + Send + 'static { + fn as_any(&self) -> &dyn Any; + fn dyn_eq(&self, other: &Dynamic) -> bool; + fn dyn_ty(&self) -> Type; + fn dyn_hash(&self, state: &mut dyn Hasher); +} + +impl<T> Bounds for T +where + T: Debug + Repr + NativeType + 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_ty(&self) -> Type { + Type::of::<T>() + } + + fn dyn_hash(&self, mut state: &mut dyn Hasher) { + // Also hash the TypeId since values with different types but + // equal data should be different. + TypeId::of::<Self>().hash(&mut state); + self.hash(&mut state); + } +} + +impl Hash for dyn Bounds { + fn hash<H: Hasher>(&self, state: &mut H) { + self.dyn_hash(state); + } +} + +/// Implements traits for primitives (Value enum variants). +macro_rules! primitive { + ( + $ty:ty: $name:literal, $variant:ident + $(, $other:ident$(($binding:ident))? => $out:expr)* + ) => { + impl Reflect for $ty { + fn input() -> CastInfo { + CastInfo::Type(Type::of::<Self>()) + } + + fn output() -> CastInfo { + CastInfo::Type(Type::of::<Self>()) + } + + 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) -> HintedStrResult<Self> { + match value { + Value::$variant(v) => Ok(v), + $(Value::$other$(($binding))? => Ok($out),)* + v => Err(<Self as Reflect>::error(&v)), + } + } + } + }; + + (@$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! { Gradient: "gradient", Gradient } +primitive! { Pattern: "pattern", Pattern } +primitive! { Symbol: "symbol", Symbol } +primitive! { Version: "version", Version } +primitive! { + Str: "string", + Str, + Symbol(symbol) => symbol.get().into() +} +primitive! { Bytes: "bytes", Bytes } +primitive! { Label: "label", Label } +primitive! { Datetime: "datetime", Datetime } +primitive! { Decimal: "decimal", Decimal } +primitive! { Duration: "duration", Duration } +primitive! { Content: "content", + Content, + None => Content::empty(), + Symbol(v) => TextElem::packed(v.get()), + Str(v) => TextElem::packed(v) +} +primitive! { Styles: "styles", Styles } +primitive! { Array: "array", Array } +primitive! { Dict: "dictionary", Dict } +primitive! { + Func: "function", + Func, + Type(ty) => ty.constructor()?.clone(), + Symbol(symbol) => symbol.func()? +} +primitive! { Args: "arguments", Args } +primitive! { Type: "type", Type } +primitive! { Module: "module", Module } +primitive! { Plugin: "plugin", Plugin } + +impl<T: Reflect> Reflect for Arc<T> { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } + + fn error(found: &Value) -> HintedString { + T::error(found) + } +} + +impl<T: Clone + IntoValue> IntoValue for Arc<T> { + fn into_value(self) -> Value { + Arc::take(self).into_value() + } +} + +impl<T: FromValue> FromValue for Arc<T> { + fn from_value(value: Value) -> HintedStrResult<Self> { + match value { + v if T::castable(&v) => Ok(Arc::new(T::from_value(v)?)), + _ => Err(Self::error(&value)), + } + } +} + +impl<T: Clone + Resolve> Resolve for Arc<T> { + type Output = Arc<T::Output>; + + fn resolve(self, styles: super::StyleChain) -> Self::Output { + Arc::new(Arc::take(self).resolve(styles)) + } +} + +impl<T: Clone + Fold> Fold for Arc<T> { + fn fold(self, outer: Self) -> Self { + Arc::new(Arc::take(self).fold(Arc::take(outer))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::foundations::{array, dict}; + + #[track_caller] + fn test(value: impl IntoValue, exp: &str) { + assert_eq!(value.into_value().repr(), exp); + } + + #[test] + fn test_value_debug() { + // Primitives. + test(Value::None, "none"); + test(Value::Auto, "auto"); + test(Value::None.ty(), "type(none)"); + test(Value::Auto.ty(), "type(auto)"); + 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"); + + // 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/crates/typst-library/src/foundations/version.rs b/crates/typst-library/src/foundations/version.rs new file mode 100644 index 00000000..62c02917 --- /dev/null +++ b/crates/typst-library/src/foundations/version.rs @@ -0,0 +1,202 @@ +use std::cmp::Ordering; +use std::fmt::{self, Display, Formatter, Write}; +use std::hash::Hash; +use std::iter::repeat; + +use ecow::{eco_format, EcoString, EcoVec}; + +use crate::diag::{bail, StrResult}; +use crate::foundations::{cast, func, repr, scope, ty, Repr}; + +/// A version with an arbitrary number of components. +/// +/// The first three components have names that can be used as fields: `major`, +/// `minor`, `patch`. All following components do not have names. +/// +/// The list of components is semantically extended by an infinite list of +/// zeros. This means that, for example, `0.8` is the same as `0.8.0`. As a +/// special case, the empty version (that has no components at all) is the same +/// as `0`, `0.0`, `0.0.0`, and so on. +/// +/// The current version of the Typst compiler is available as `sys.version`. +/// +/// You can convert a version to an array of explicitly given components using +/// the [`array`] constructor. +#[ty(scope, cast)] +#[derive(Debug, Default, Clone, Hash)] +#[allow(clippy::derived_hash_with_manual_eq)] +pub struct Version(EcoVec<u32>); + +impl Version { + /// The names for the first components of a version. + pub const COMPONENTS: [&'static str; 3] = ["major", "minor", "patch"]; + + /// Create a new (empty) version. + pub fn new() -> Self { + Self::default() + } + + /// Get a named component of a version. + /// + /// Always non-negative. Returns `0` if the version isn't specified to the + /// necessary length. + pub fn component(&self, name: &str) -> StrResult<i64> { + self.0 + .iter() + .zip(Self::COMPONENTS) + .find_map(|(&i, s)| (s == name).then_some(i as i64)) + .ok_or_else(|| "unknown version component".into()) + } + + /// Push a component to the end of this version. + pub fn push(&mut self, component: u32) { + self.0.push(component); + } + + /// The values of the version + pub fn values(&self) -> &[u32] { + &self.0 + } +} + +#[scope] +impl Version { + /// Creates a new version. + /// + /// It can have any number of components (even zero). + /// + /// ```example + /// #version() \ + /// #version(1) \ + /// #version(1, 2, 3, 4) \ + /// #version((1, 2, 3, 4)) \ + /// #version((1, 2), 3) + /// ``` + #[func(constructor)] + pub fn construct( + /// The components of the version (array arguments are flattened) + #[variadic] + components: Vec<VersionComponents>, + ) -> Version { + let mut version = Version::new(); + for c in components { + match c { + VersionComponents::Single(v) => version.push(v), + VersionComponents::Multiple(values) => { + for v in values { + version.push(v); + } + } + } + } + version + } + + /// Retrieves a component of a version. + /// + /// The returned integer is always non-negative. Returns `0` if the version + /// isn't specified to the necessary length. + #[func] + pub fn at( + &self, + /// The index at which to retrieve the component. If negative, indexes + /// from the back of the explicitly given components. + index: i64, + ) -> StrResult<i64> { + let mut index = index; + if index < 0 { + match (self.0.len() as i64).checked_add(index) { + Some(pos_index) if pos_index >= 0 => index = pos_index, + _ => bail!( + "component index out of bounds (index: {index}, len: {})", + self.0.len() + ), + } + } + Ok(usize::try_from(index) + .ok() + .and_then(|i| self.0.get(i).copied()) + .unwrap_or_default() as i64) + } +} + +impl FromIterator<u32> for Version { + fn from_iter<T: IntoIterator<Item = u32>>(iter: T) -> Self { + Self(EcoVec::from_iter(iter)) + } +} + +impl IntoIterator for Version { + type Item = u32; + type IntoIter = ecow::vec::IntoIter<u32>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl Ord for Version { + fn cmp(&self, other: &Self) -> Ordering { + let max_len = self.0.len().max(other.0.len()); + let tail = repeat(&0); + + let self_iter = self.0.iter().chain(tail.clone()); + let other_iter = other.0.iter().chain(tail); + + for (l, r) in self_iter.zip(other_iter).take(max_len) { + match l.cmp(r) { + Ordering::Equal => (), + ord => return ord, + } + } + + Ordering::Equal + } +} + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl Eq for Version {} + +impl PartialEq for Version { + fn eq(&self, other: &Self) -> bool { + matches!(self.cmp(other), Ordering::Equal) + } +} + +impl Display for Version { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let mut first = true; + for &v in &self.0 { + if !first { + f.write_char('.')?; + } + write!(f, "{v}")?; + first = false; + } + Ok(()) + } +} + +impl Repr for Version { + fn repr(&self) -> EcoString { + let parts: Vec<_> = self.0.iter().map(|v| eco_format!("{v}")).collect(); + eco_format!("version{}", &repr::pretty_array_like(&parts, false)) + } +} + +/// One or multiple version components. +pub enum VersionComponents { + Single(u32), + Multiple(Vec<u32>), +} + +cast! { + VersionComponents, + v: u32 => Self::Single(v), + v: Vec<u32> => Self::Multiple(v) +} diff --git a/crates/typst-library/src/introspection/counter.rs b/crates/typst-library/src/introspection/counter.rs new file mode 100644 index 00000000..2e7180c6 --- /dev/null +++ b/crates/typst-library/src/introspection/counter.rs @@ -0,0 +1,856 @@ +use std::num::NonZeroUsize; +use std::str::FromStr; + +use comemo::{Track, Tracked, TrackedMut}; +use ecow::{eco_format, eco_vec, EcoString, EcoVec}; +use smallvec::{smallvec, SmallVec}; +use typst_syntax::Span; +use typst_utils::NonZeroExt; + +use crate::diag::{bail, warning, At, HintedStrResult, SourceResult}; +use crate::engine::{Engine, Route, Sink, Traced}; +use crate::foundations::{ + cast, elem, func, scope, select_where, ty, Args, Array, Construct, Content, Context, + Element, Func, IntoValue, Label, LocatableSelector, NativeElement, Packed, Repr, + Selector, Show, Smart, Str, StyleChain, Value, +}; +use crate::introspection::{Introspector, Locatable, Location, Tag}; +use crate::layout::{Frame, FrameItem, PageElem}; +use crate::math::EquationElem; +use crate::model::{FigureElem, FootnoteElem, HeadingElem, Numbering, NumberingPattern}; +use crate::routines::Routines; +use crate::World; + +/// Counts through pages, elements, and more. +/// +/// With the counter function, you can access and modify counters for pages, +/// headings, figures, and more. Moreover, you can define custom counters for +/// other things you want to count. +/// +/// Since counters change throughout the course of the document, their current +/// value is _contextual._ It is recommended to read the chapter on [context] +/// before continuing here. +/// +/// # Accessing a counter { #accessing } +/// To access the raw value of a counter, we can use the [`get`]($counter.get) +/// function. This function returns an [array]: Counters can have multiple +/// levels (in the case of headings for sections, subsections, and so on), and +/// each item in the array corresponds to one level. +/// +/// ```example +/// #set heading(numbering: "1.") +/// +/// = Introduction +/// Raw value of heading counter is +/// #context counter(heading).get() +/// ``` +/// +/// # Displaying a counter { #displaying } +/// Often, we want to display the value of a counter in a more human-readable +/// way. To do that, we can call the [`display`]($counter.display) function on +/// the counter. This function retrieves the current counter value and formats +/// it either with a provided or with an automatically inferred [numbering]. +/// +/// ```example +/// #set heading(numbering: "1.") +/// +/// = Introduction +/// Some text here. +/// +/// = Background +/// The current value is: #context { +/// counter(heading).display() +/// } +/// +/// Or in roman numerals: #context { +/// counter(heading).display("I") +/// } +/// ``` +/// +/// # Modifying a counter { #modifying } +/// To modify a counter, you can use the `step` and `update` methods: +/// +/// - The `step` method increases the value of the counter by one. Because +/// counters can have multiple levels , it optionally takes a `level` +/// argument. If given, the counter steps at the given depth. +/// +/// - The `update` method allows you to arbitrarily modify the counter. In its +/// basic form, you give it an integer (or an array for multiple levels). For +/// more flexibility, you can instead also give it a function that receives +/// the current value and returns a new value. +/// +/// The heading counter is stepped before the heading is displayed, so +/// `Analysis` gets the number seven even though the counter is at six after the +/// second update. +/// +/// ```example +/// #set heading(numbering: "1.") +/// +/// = Introduction +/// #counter(heading).step() +/// +/// = Background +/// #counter(heading).update(3) +/// #counter(heading).update(n => n * 2) +/// +/// = Analysis +/// Let's skip 7.1. +/// #counter(heading).step(level: 2) +/// +/// == Analysis +/// Still at #context { +/// counter(heading).display() +/// } +/// ``` +/// +/// # Page counter +/// The page counter is special. It is automatically stepped at each pagebreak. +/// But like other counters, you can also step it manually. For example, you +/// could have Roman page numbers for your preface, then switch to Arabic page +/// numbers for your main content and reset the page counter to one. +/// +/// ```example +/// >>> #set page( +/// >>> height: 100pt, +/// >>> margin: (bottom: 24pt, rest: 16pt), +/// >>> ) +/// #set page(numbering: "(i)") +/// +/// = Preface +/// The preface is numbered with +/// roman numerals. +/// +/// #set page(numbering: "1 / 1") +/// #counter(page).update(1) +/// +/// = Main text +/// Here, the counter is reset to one. +/// We also display both the current +/// page and total number of pages in +/// Arabic numbers. +/// ``` +/// +/// # Custom counters +/// To define your own counter, call the `counter` function with a string as a +/// key. This key identifies the counter globally. +/// +/// ```example +/// #let mine = counter("mycounter") +/// #context mine.display() \ +/// #mine.step() +/// #context mine.display() \ +/// #mine.update(c => c * 3) +/// #context mine.display() +/// ``` +/// +/// # How to step +/// When you define and use a custom counter, in general, you should first step +/// the counter and then display it. This way, the stepping behaviour of a +/// counter can depend on the element it is stepped for. If you were writing a +/// counter for, let's say, theorems, your theorem's definition would thus first +/// include the counter step and only then display the counter and the theorem's +/// contents. +/// +/// ```example +/// #let c = counter("theorem") +/// #let theorem(it) = block[ +/// #c.step() +/// *Theorem #context c.display():* +/// #it +/// ] +/// +/// #theorem[$1 = 1$] +/// #theorem[$2 < 3$] +/// ``` +/// +/// The rationale behind this is best explained on the example of the heading +/// counter: An update to the heading counter depends on the heading's level. By +/// stepping directly before the heading, we can correctly step from `1` to +/// `1.1` when encountering a level 2 heading. If we were to step after the +/// heading, we wouldn't know what to step to. +/// +/// Because counters should always be stepped before the elements they count, +/// they always start at zero. This way, they are at one for the first display +/// (which happens after the first step). +/// +/// # Time travel +/// Counters can travel through time! You can find out the final value of the +/// counter before it is reached and even determine what the value was at any +/// particular location in the document. +/// +/// ```example +/// #let mine = counter("mycounter") +/// +/// = Values +/// #context [ +/// Value here: #mine.get() \ +/// At intro: #mine.at(<intro>) \ +/// Final value: #mine.final() +/// ] +/// +/// #mine.update(n => n + 3) +/// +/// = Introduction <intro> +/// #lorem(10) +/// +/// #mine.step() +/// #mine.step() +/// ``` +/// +/// # Other kinds of state { #other-state } +/// The `counter` type is closely related to [state] type. Read its +/// documentation for more details on state management in Typst and why it +/// doesn't just use normal variables for counters. +#[ty(scope)] +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct Counter(CounterKey); + +impl Counter { + /// Create a new counter identified by a key. + pub fn new(key: CounterKey) -> Counter { + Self(key) + } + + /// The counter for the given element. + pub fn of(func: Element) -> Self { + Self::new(CounterKey::Selector(Selector::Elem(func, None))) + } + + /// Gets the current and final value of the state combined in one state. + pub fn both( + &self, + engine: &mut Engine, + location: Location, + ) -> SourceResult<CounterState> { + let sequence = self.sequence(engine)?; + let offset = engine.introspector.query_count_before(&self.selector(), location); + let (mut at_state, at_page) = sequence[offset].clone(); + let (mut final_state, final_page) = sequence.last().unwrap().clone(); + if self.is_page() { + let at_delta = + engine.introspector.page(location).get().saturating_sub(at_page.get()); + at_state.step(NonZeroUsize::ONE, at_delta); + let final_delta = + engine.introspector.pages().get().saturating_sub(final_page.get()); + final_state.step(NonZeroUsize::ONE, final_delta); + } + Ok(CounterState(smallvec![at_state.first(), final_state.first()])) + } + + /// Gets the value of the counter at the given location. Always returns an + /// array of integers, even if the counter has just one number. + pub fn at_loc( + &self, + engine: &mut Engine, + location: Location, + ) -> SourceResult<CounterState> { + let sequence = self.sequence(engine)?; + let offset = engine.introspector.query_count_before(&self.selector(), location); + let (mut state, page) = sequence[offset].clone(); + if self.is_page() { + let delta = + engine.introspector.page(location).get().saturating_sub(page.get()); + state.step(NonZeroUsize::ONE, delta); + } + Ok(state) + } + + /// Displays the value of the counter at the given location. + pub fn display_at_loc( + &self, + engine: &mut Engine, + loc: Location, + styles: StyleChain, + numbering: &Numbering, + ) -> SourceResult<Content> { + let context = Context::new(Some(loc), Some(styles)); + Ok(self + .at_loc(engine, loc)? + .display(engine, context.track(), numbering)? + .display()) + } + + /// Produce the whole sequence of counter states. + /// + /// This has to happen just once for all counters, cutting down the number + /// of counter updates from quadratic to linear. + fn sequence( + &self, + engine: &mut Engine, + ) -> SourceResult<EcoVec<(CounterState, NonZeroUsize)>> { + self.sequence_impl( + engine.routines, + engine.world, + engine.introspector, + engine.traced, + TrackedMut::reborrow_mut(&mut engine.sink), + engine.route.track(), + ) + } + + /// Memoized implementation of `sequence`. + #[comemo::memoize] + fn sequence_impl( + &self, + routines: &Routines, + world: Tracked<dyn World + '_>, + introspector: Tracked<Introspector>, + traced: Tracked<Traced>, + sink: TrackedMut<Sink>, + route: Tracked<Route>, + ) -> SourceResult<EcoVec<(CounterState, NonZeroUsize)>> { + let mut engine = Engine { + routines, + world, + introspector, + traced, + sink, + route: Route::extend(route).unnested(), + }; + + let mut state = CounterState::init(matches!(self.0, CounterKey::Page)); + let mut page = NonZeroUsize::ONE; + let mut stops = eco_vec![(state.clone(), page)]; + + for elem in introspector.query(&self.selector()) { + if self.is_page() { + let prev = page; + page = introspector.page(elem.location().unwrap()); + + let delta = page.get() - prev.get(); + if delta > 0 { + state.step(NonZeroUsize::ONE, delta); + } + } + + if let Some(update) = match elem.with::<dyn Count>() { + Some(countable) => countable.update(), + None => Some(CounterUpdate::Step(NonZeroUsize::ONE)), + } { + state.update(&mut engine, update)?; + } + + stops.push((state.clone(), page)); + } + + Ok(stops) + } + + /// The selector relevant for this counter's updates. + fn selector(&self) -> Selector { + let mut selector = select_where!(CounterUpdateElem, Key => self.0.clone()); + + if let CounterKey::Selector(key) = &self.0 { + selector = Selector::Or(eco_vec![selector, key.clone()]); + } + + selector + } + + /// Whether this is the page counter. + fn is_page(&self) -> bool { + self.0 == CounterKey::Page + } + + /// Shared implementation of displaying between `counter.display` and + /// `DisplayElem`, which will be deprecated. + fn display_impl( + &self, + engine: &mut Engine, + location: Location, + numbering: Smart<Numbering>, + both: bool, + styles: Option<StyleChain>, + ) -> SourceResult<Value> { + let numbering = numbering + .custom() + .or_else(|| { + let styles = styles?; + let CounterKey::Selector(Selector::Elem(func, _)) = self.0 else { + return None; + }; + + if func == HeadingElem::elem() { + HeadingElem::numbering_in(styles).clone() + } else if func == FigureElem::elem() { + FigureElem::numbering_in(styles).clone() + } else if func == EquationElem::elem() { + EquationElem::numbering_in(styles).clone() + } else if func == FootnoteElem::elem() { + Some(FootnoteElem::numbering_in(styles).clone()) + } else { + None + } + }) + .unwrap_or_else(|| NumberingPattern::from_str("1.1").unwrap().into()); + + let state = if both { + self.both(engine, location)? + } else { + self.at_loc(engine, location)? + }; + + let context = Context::new(Some(location), styles); + state.display(engine, context.track(), &numbering) + } +} + +#[scope] +impl Counter { + /// Create a new counter identified by a key. + #[func(constructor)] + pub fn construct( + /// The key that identifies this counter. + /// + /// - If it is a string, creates a custom counter that is only affected + /// by manual updates, + /// - If it is the [`page`] function, counts through pages, + /// - If it is a [selector], counts through elements that matches with the + /// selector. For example, + /// - provide an element function: counts elements of that type, + /// - provide a [`{<label>}`]($label): counts elements with that label. + key: CounterKey, + ) -> Counter { + Self::new(key) + } + + /// Retrieves the value of the counter at the current location. Always + /// returns an array of integers, even if the counter has just one number. + /// + /// This is equivalent to `{counter.at(here())}`. + #[func(contextual)] + pub fn get( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The callsite span. + span: Span, + ) -> SourceResult<CounterState> { + let loc = context.location().at(span)?; + self.at_loc(engine, loc) + } + + /// Displays the current value of the counter with a numbering and returns + /// the formatted output. + /// + /// _Compatibility:_ For compatibility with Typst 0.10 and lower, this + /// function also works without an established context. Then, it will create + /// opaque contextual content rather than directly returning the output of + /// the numbering. This behaviour will be removed in a future release. + #[func(contextual)] + pub fn display( + self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The call span of the display. + span: Span, + /// A [numbering pattern or a function]($numbering), which specifies how + /// to display the counter. If given a function, that function receives + /// each number of the counter as a separate argument. If the amount of + /// numbers varies, e.g. for the heading argument, you can use an + /// [argument sink]($arguments). + /// + /// If this is omitted or set to `{auto}`, displays the counter with the + /// numbering style for the counted element or with the pattern + /// `{"1.1"}` if no such style exists. + #[default] + numbering: Smart<Numbering>, + /// If enabled, displays the current and final top-level count together. + /// Both can be styled through a single numbering pattern. This is used + /// by the page numbering property to display the current and total + /// number of pages when a pattern like `{"1 / 1"}` is given. + #[named] + #[default(false)] + both: bool, + ) -> SourceResult<Value> { + if let Ok(loc) = context.location() { + self.display_impl(engine, loc, numbering, both, context.styles().ok()) + } else { + engine.sink.warn(warning!( + span, "`counter.display` without context is deprecated"; + hint: "use it in a `context` expression instead" + )); + + Ok(CounterDisplayElem::new(self, numbering, both) + .pack() + .spanned(span) + .into_value()) + } + } + + /// Retrieves the value of the counter at the given location. Always returns + /// an array of integers, even if the counter has just one number. + /// + /// The `selector` must match exactly one element in the document. The most + /// useful kinds of selectors for this are [labels]($label) and + /// [locations]($location). + /// + /// _Compatibility:_ For compatibility with Typst 0.10 and lower, this + /// function also works without a known context if the `selector` is a + /// location. This behaviour will be removed in a future release. + #[func(contextual)] + pub fn at( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The callsite span. + span: Span, + /// The place at which the counter's value should be retrieved. + selector: LocatableSelector, + ) -> SourceResult<CounterState> { + let loc = selector.resolve_unique(engine.introspector, context).at(span)?; + self.at_loc(engine, loc) + } + + /// Retrieves the value of the counter at the end of the document. Always + /// returns an array of integers, even if the counter has just one number. + #[func(contextual)] + pub fn final_( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The callsite span. + span: Span, + /// _Compatibility:_ This argument is deprecated. It only exists for + /// compatibility with Typst 0.10 and lower and shouldn't be used + /// anymore. + #[default] + location: Option<Location>, + ) -> SourceResult<CounterState> { + if location.is_none() { + context.location().at(span)?; + } else { + engine.sink.warn(warning!( + span, "calling `counter.final` with a location is deprecated"; + hint: "try removing the location argument" + )); + } + + let sequence = self.sequence(engine)?; + let (mut state, page) = sequence.last().unwrap().clone(); + if self.is_page() { + let delta = engine.introspector.pages().get().saturating_sub(page.get()); + state.step(NonZeroUsize::ONE, delta); + } + Ok(state) + } + + /// Increases the value of the counter by one. + /// + /// The update will be in effect at the position where the returned content + /// is inserted into the document. If you don't put the output into the + /// document, nothing happens! This would be the case, for example, if you + /// write `{let _ = counter(page).step()}`. Counter updates are always + /// applied in layout order and in that case, Typst wouldn't know when to + /// step the counter. + #[func] + pub fn step( + self, + /// The call span of the update. + span: Span, + /// The depth at which to step the counter. Defaults to `{1}`. + #[named] + #[default(NonZeroUsize::ONE)] + level: NonZeroUsize, + ) -> Content { + self.update(span, CounterUpdate::Step(level)) + } + + /// Updates the value of the counter. + /// + /// Just like with `step`, the update only occurs if you put the resulting + /// content into the document. + #[func] + pub fn update( + self, + /// The call span of the update. + span: Span, + /// If given an integer or array of integers, sets the counter to that + /// value. If given a function, that function receives the previous + /// counter value (with each number as a separate argument) and has to + /// return the new value (integer or array). + update: CounterUpdate, + ) -> Content { + CounterUpdateElem::new(self.0, update).pack().spanned(span) + } +} + +impl Repr for Counter { + fn repr(&self) -> EcoString { + eco_format!("counter({})", self.0.repr()) + } +} + +/// Identifies a counter. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum CounterKey { + /// The page counter. + Page, + /// Counts elements matching the given selectors. Only works for + /// [locatable]($location/#locatable) + /// elements or labels. + Selector(Selector), + /// Counts through manual counters with the same key. + Str(Str), +} + +cast! { + CounterKey, + self => match self { + Self::Page => PageElem::elem().into_value(), + Self::Selector(v) => v.into_value(), + Self::Str(v) => v.into_value(), + }, + v: Str => Self::Str(v), + v: Label => Self::Selector(Selector::Label(v)), + v: Element => { + if v == PageElem::elem() { + Self::Page + } else { + Self::Selector(LocatableSelector::from_value(v.into_value())?.0) + } + }, + v: LocatableSelector => Self::Selector(v.0), +} + +impl Repr for CounterKey { + fn repr(&self) -> EcoString { + match self { + Self::Page => "page".into(), + Self::Selector(selector) => selector.repr(), + Self::Str(str) => str.repr(), + } + } +} + +/// An update to perform on a counter. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum CounterUpdate { + /// Set the counter to the specified state. + Set(CounterState), + /// Increase the number for the given level by one. + Step(NonZeroUsize), + /// Apply the given function to the counter's state. + Func(Func), +} + +cast! { + CounterUpdate, + v: CounterState => Self::Set(v), + v: Func => Self::Func(v), +} + +/// Elements that have special counting behaviour. +pub trait Count { + /// Get the counter update for this element. + fn update(&self) -> Option<CounterUpdate>; +} + +/// Counts through elements with different levels. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct CounterState(pub SmallVec<[usize; 3]>); + +impl CounterState { + /// Get the initial counter state for the key. + pub fn init(page: bool) -> Self { + // Special case, because pages always start at one. + Self(smallvec![usize::from(page)]) + } + + /// Advance the counter and return the numbers for the given heading. + pub fn update( + &mut self, + engine: &mut Engine, + update: CounterUpdate, + ) -> SourceResult<()> { + match update { + CounterUpdate::Set(state) => *self = state, + CounterUpdate::Step(level) => self.step(level, 1), + CounterUpdate::Func(func) => { + *self = func + .call(engine, Context::none().track(), self.0.iter().copied())? + .cast() + .at(func.span())? + } + } + Ok(()) + } + + /// Advance the number of the given level by the specified amount. + pub fn step(&mut self, level: NonZeroUsize, by: usize) { + let level = level.get(); + + while self.0.len() < level { + self.0.push(0); + } + + self.0[level - 1] = self.0[level - 1].saturating_add(by); + self.0.truncate(level); + } + + /// Get the first number of the state. + pub fn first(&self) -> usize { + self.0.first().copied().unwrap_or(1) + } + + /// Display the counter state with a numbering. + pub fn display( + &self, + engine: &mut Engine, + context: Tracked<Context>, + numbering: &Numbering, + ) -> SourceResult<Value> { + numbering.apply(engine, context, &self.0) + } +} + +cast! { + CounterState, + self => Value::Array(self.0.into_iter().map(IntoValue::into_value).collect()), + num: usize => Self(smallvec![num]), + array: Array => Self(array + .into_iter() + .map(Value::cast) + .collect::<HintedStrResult<_>>()?), +} + +/// Executes an update of a counter. +#[elem(Construct, Locatable, Show, Count)] +struct CounterUpdateElem { + /// The key that identifies the counter. + #[required] + key: CounterKey, + + /// The update to perform on the counter. + #[required] + #[internal] + update: CounterUpdate, +} + +impl Construct for CounterUpdateElem { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> { + bail!(args.span, "cannot be constructed manually"); + } +} + +impl Show for Packed<CounterUpdateElem> { + fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(Content::empty()) + } +} + +impl Count for Packed<CounterUpdateElem> { + fn update(&self) -> Option<CounterUpdate> { + Some(self.update.clone()) + } +} + +/// Executes a display of a counter. +/// +/// **Deprecation planned.** +#[elem(Construct, Locatable, Show)] +pub struct CounterDisplayElem { + /// The counter. + #[required] + #[internal] + counter: Counter, + + /// The numbering to display the counter with. + #[required] + #[internal] + numbering: Smart<Numbering>, + + /// Whether to display both the current and final value. + #[required] + #[internal] + both: bool, +} + +impl Construct for CounterDisplayElem { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> { + bail!(args.span, "cannot be constructed manually"); + } +} + +impl Show for Packed<CounterDisplayElem> { + #[typst_macros::time(name = "counter.display", span = self.span())] + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + Ok(self + .counter + .display_impl( + engine, + self.location().unwrap(), + self.numbering.clone(), + self.both, + Some(styles), + )? + .display()) + } +} + +/// An specialized handler of the page counter that tracks both the physical +/// and the logical page counter. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct ManualPageCounter { + physical: NonZeroUsize, + logical: usize, +} + +impl ManualPageCounter { + /// Create a new fast page counter, starting at 1. + pub fn new() -> Self { + Self { physical: NonZeroUsize::ONE, logical: 1 } + } + + /// Get the current physical page counter state. + pub fn physical(&self) -> NonZeroUsize { + self.physical + } + + /// Get the current logical page counter state. + pub fn logical(&self) -> usize { + self.logical + } + + /// Advance past a page. + pub fn visit(&mut self, engine: &mut Engine, page: &Frame) -> SourceResult<()> { + for (_, item) in page.items() { + match item { + FrameItem::Group(group) => self.visit(engine, &group.frame)?, + FrameItem::Tag(Tag::Start(elem)) => { + let Some(elem) = elem.to_packed::<CounterUpdateElem>() else { + continue; + }; + if *elem.key() == CounterKey::Page { + let mut state = CounterState(smallvec![self.logical]); + state.update(engine, elem.update.clone())?; + self.logical = state.first(); + } + } + _ => {} + } + } + + Ok(()) + } + + /// Step past a page _boundary._ + pub fn step(&mut self) { + self.physical = self.physical.saturating_add(1); + self.logical += 1; + } +} + +impl Default for ManualPageCounter { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/typst-library/src/introspection/here.rs b/crates/typst-library/src/introspection/here.rs new file mode 100644 index 00000000..9d613381 --- /dev/null +++ b/crates/typst-library/src/introspection/here.rs @@ -0,0 +1,52 @@ +use comemo::Tracked; + +use crate::diag::HintedStrResult; +use crate::foundations::{func, Context}; +use crate::introspection::Location; + +/// Provides the current location in the document. +/// +/// You can think of `here` as a low-level building block that directly extracts +/// the current location from the active [context]. Some other functions use it +/// internally: For instance, `{counter.get()}` is equivalent to +/// `{counter.at(here())}`. +/// +/// Within show rules on [locatable]($location/#locatable) elements, `{here()}` +/// will match the location of the shown element. +/// +/// If you want to display the current page number, refer to the documentation +/// of the [`counter`] type. While `here` can be used to determine the physical +/// page number, typically you want the logical page number that may, for +/// instance, have been reset after a preface. +/// +/// # Examples +/// Determining the current position in the document in combination with the +/// [`position`]($location.position) method: +/// ```example +/// #context [ +/// I am located at +/// #here().position() +/// ] +/// ``` +/// +/// Running a [query] for elements before the current position: +/// ```example +/// = Introduction +/// = Background +/// +/// There are +/// #context query( +/// selector(heading).before(here()) +/// ).len() +/// headings before me. +/// +/// = Conclusion +/// ``` +/// Refer to the [`selector`] type for more details on before/after selectors. +#[func(contextual)] +pub fn here( + /// The callsite context. + context: Tracked<Context>, +) -> HintedStrResult<Location> { + context.location() +} diff --git a/crates/typst-library/src/introspection/introspector.rs b/crates/typst-library/src/introspection/introspector.rs new file mode 100644 index 00000000..f4eaea30 --- /dev/null +++ b/crates/typst-library/src/introspection/introspector.rs @@ -0,0 +1,453 @@ +use std::collections::{BTreeSet, HashMap, HashSet}; +use std::fmt::{self, Debug, Formatter}; +use std::hash::Hash; +use std::num::NonZeroUsize; +use std::sync::RwLock; + +use ecow::EcoVec; +use smallvec::SmallVec; +use typst_utils::NonZeroExt; + +use crate::diag::{bail, StrResult}; +use crate::foundations::{Content, Label, Repr, Selector}; +use crate::introspection::{Location, Tag}; +use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform}; +use crate::model::Numbering; + +/// Can be queried for elements and their positions. +#[derive(Default, Clone)] +pub struct Introspector { + /// The number of pages in the document. + pages: usize, + /// The page numberings, indexed by page number minus 1. + page_numberings: Vec<Option<Numbering>>, + + /// All introspectable elements. + elems: Vec<Pair>, + /// Lists all elements with a specific hash key. This is used for + /// introspector-assisted location assignment during measurement. + keys: MultiMap<u128, Location>, + + /// Accelerates lookup of elements by location. + locations: HashMap<Location, usize>, + /// Accelerates lookup of elements by label. + labels: MultiMap<Label, usize>, + + /// 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: QueryCache, +} + +/// A pair of content and its position. +type Pair = (Content, Position); + +impl Introspector { + /// Creates an introspector for a page list. + #[typst_macros::time(name = "introspect")] + pub fn new(pages: &[Page]) -> Self { + IntrospectorBuilder::new().build(pages) + } + + /// Iterates over all locatable elements. + pub fn all(&self) -> impl Iterator<Item = &Content> + '_ { + self.elems.iter().map(|(c, _)| c) + } + + /// Retrieves the element with the given index. + #[track_caller] + fn get_by_idx(&self, idx: usize) -> &Content { + &self.elems[idx].0 + } + + /// Retrieves the position of the element with the given index. + #[track_caller] + fn get_pos_by_idx(&self, idx: usize) -> Position { + self.elems[idx].1 + } + + /// Retrieves an element by its location. + fn get_by_loc(&self, location: &Location) -> Option<&Content> { + self.locations.get(location).map(|&idx| self.get_by_idx(idx)) + } + + /// Retrieves the position of the element with the given index. + fn get_pos_by_loc(&self, location: &Location) -> Option<Position> { + self.locations.get(location).map(|&idx| self.get_pos_by_idx(idx)) + } + + /// Performs a binary search for `elem` among the `list`. + fn binary_search(&self, list: &[Content], elem: &Content) -> Result<usize, usize> { + list.binary_search_by_key(&self.elem_index(elem), |elem| self.elem_index(elem)) + } + + /// Gets the index of this element. + fn elem_index(&self, elem: &Content) -> usize { + self.loc_index(&elem.location().unwrap()) + } + + /// Gets the index of the element with this location among all. + fn loc_index(&self, location: &Location) -> usize { + self.locations.get(location).copied().unwrap_or(usize::MAX) + } +} + +#[comemo::track] +impl Introspector { + /// Query for all matching elements. + pub fn query(&self, selector: &Selector) -> EcoVec<Content> { + let hash = typst_utils::hash128(selector); + if let Some(output) = self.queries.get(hash) { + return output; + } + + let output = match selector { + Selector::Elem(..) => self + .all() + .filter(|elem| selector.matches(elem, None)) + .cloned() + .collect(), + Selector::Location(location) => { + self.get_by_loc(location).cloned().into_iter().collect() + } + Selector::Label(label) => self + .labels + .get(label) + .iter() + .map(|&idx| self.get_by_idx(idx).clone()) + .collect(), + Selector::Or(selectors) => selectors + .iter() + .flat_map(|sel| self.query(sel)) + .map(|elem| self.elem_index(&elem)) + .collect::<BTreeSet<usize>>() + .into_iter() + .map(|idx| self.get_by_idx(idx).clone()) + .collect(), + Selector::And(selectors) => { + let mut results: Vec<_> = + selectors.iter().map(|sel| self.query(sel)).collect(); + + // Extract the smallest result list and then keep only those + // elements in the smallest list that are also in all other + // lists. + results + .iter() + .enumerate() + .min_by_key(|(_, vec)| vec.len()) + .map(|(i, _)| i) + .map(|i| results.swap_remove(i)) + .iter() + .flatten() + .filter(|candidate| { + results + .iter() + .all(|other| self.binary_search(other, candidate).is_ok()) + }) + .cloned() + .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 self.binary_search(&list, &end) { + // 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 self.binary_search(&list, &start) { + // Element itself is contained. + Ok(i) => i + !*inclusive as usize, + // Element itself is not contained. + Err(i) => i, + }; + list = list[split..].into(); + } + list + } + // Not supported here. + Selector::Can(_) | Selector::Regex(_) => EcoVec::new(), + }; + + self.queries.insert(hash, output.clone()); + output + } + + /// Query for the first element that matches the selector. + pub fn query_first(&self, selector: &Selector) -> Option<Content> { + match selector { + Selector::Location(location) => self.get_by_loc(location).cloned(), + Selector::Label(label) => self + .labels + .get(label) + .first() + .map(|&idx| self.get_by_idx(idx).clone()), + _ => self.query(selector).first().cloned(), + } + } + + /// Query for the first element that matches the selector. + pub fn query_unique(&self, selector: &Selector) -> StrResult<Content> { + match selector { + Selector::Location(location) => self + .get_by_loc(location) + .cloned() + .ok_or_else(|| "element does not exist in the document".into()), + Selector::Label(label) => self.query_label(*label).cloned(), + _ => { + let elems = self.query(selector); + if elems.len() > 1 { + bail!("selector matches multiple elements",); + } + elems + .into_iter() + .next() + .ok_or_else(|| "selector does not match any element".into()) + } + } + } + + /// Query for a unique element with the label. + pub fn query_label(&self, label: Label) -> StrResult<&Content> { + match *self.labels.get(&label) { + [idx] => Ok(self.get_by_idx(idx)), + [] => bail!("label `{}` does not exist in the document", label.repr()), + _ => bail!("label `{}` occurs multiple times in the document", label.repr()), + } + } + + /// This is an optimized version of + /// `query(selector.before(end, true).len()` used by counters and state. + pub fn query_count_before(&self, selector: &Selector, end: Location) -> usize { + // See `query()` for details. + let list = self.query(selector); + if let Some(end) = self.get_by_loc(&end) { + match self.binary_search(&list, end) { + Ok(i) => i + 1, + Err(i) => i, + } + } else { + list.len() + } + } + + /// The total number pages. + pub fn pages(&self) -> NonZeroUsize { + NonZeroUsize::new(self.pages).unwrap_or(NonZeroUsize::ONE) + } + + /// 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.get_pos_by_loc(&location) + .unwrap_or(Position { page: NonZeroUsize::ONE, point: Point::zero() }) + } + + /// Gets the page numbering for the given location, if any. + pub fn page_numbering(&self, location: Location) -> Option<&Numbering> { + let page = self.page(location); + self.page_numberings + .get(page.get() - 1) + .and_then(|slot| slot.as_ref()) + } + + /// Try to find a location for an element with the given `key` hash + /// that is closest after the `anchor`. + /// + /// This is used for introspector-assisted location assignment during + /// measurement. See the "Dealing with Measurement" section of the + /// [`Locator`](crate::introspection::Locator) docs for more details. + pub fn locator(&self, key: u128, anchor: Location) -> Option<Location> { + let anchor = self.loc_index(&anchor); + self.keys + .get(&key) + .iter() + .copied() + .min_by_key(|loc| self.loc_index(loc).wrapping_sub(anchor)) + } +} + +impl Debug for Introspector { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("Introspector(..)") + } +} + +/// A map from one keys to multiple elements. +#[derive(Clone)] +struct MultiMap<K, V>(HashMap<K, SmallVec<[V; 1]>>); + +impl<K, V> MultiMap<K, V> +where + K: Hash + Eq, +{ + fn get(&self, key: &K) -> &[V] { + self.0.get(key).map_or(&[], |vec| vec.as_slice()) + } + + fn insert(&mut self, key: K, value: V) { + self.0.entry(key).or_default().push(value); + } + + fn take(&mut self, key: &K) -> Option<impl Iterator<Item = V>> { + self.0.remove(key).map(|vec| vec.into_iter()) + } +} + +impl<K, V> Default for MultiMap<K, V> { + fn default() -> Self { + Self(HashMap::new()) + } +} + +/// Caches queries. +#[derive(Default)] +struct QueryCache(RwLock<HashMap<u128, EcoVec<Content>>>); + +impl QueryCache { + fn get(&self, hash: u128) -> Option<EcoVec<Content>> { + self.0.read().unwrap().get(&hash).cloned() + } + + fn insert(&self, hash: u128, output: EcoVec<Content>) { + self.0.write().unwrap().insert(hash, output); + } +} + +impl Clone for QueryCache { + fn clone(&self) -> Self { + Self(RwLock::new(self.0.read().unwrap().clone())) + } +} + +/// Builds the introspector. +#[derive(Default)] +struct IntrospectorBuilder { + page_numberings: Vec<Option<Numbering>>, + seen: HashSet<Location>, + insertions: MultiMap<Location, Vec<Pair>>, + keys: MultiMap<u128, Location>, + locations: HashMap<Location, usize>, + labels: MultiMap<Label, usize>, +} + +impl IntrospectorBuilder { + /// Create an empty builder. + fn new() -> Self { + Self::default() + } + + /// Build the introspector. + fn build(mut self, pages: &[Page]) -> Introspector { + self.page_numberings.reserve(pages.len()); + + // Discover all elements. + let mut root = Vec::new(); + for (i, page) in pages.iter().enumerate() { + self.page_numberings.push(page.numbering.clone()); + self.discover( + &mut root, + &page.frame, + NonZeroUsize::new(1 + i).unwrap(), + Transform::identity(), + ); + } + + self.locations.reserve(self.seen.len()); + + // Save all pairs and their descendants in the correct order. + let mut elems = Vec::with_capacity(self.seen.len()); + for pair in root { + self.visit(&mut elems, pair); + } + + Introspector { + pages: pages.len(), + page_numberings: self.page_numberings, + elems, + keys: self.keys, + locations: self.locations, + labels: self.labels, + queries: QueryCache::default(), + } + } + + /// Processes the tags in the frame. + fn discover( + &mut self, + sink: &mut Vec<Pair>, + 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); + + if let Some(parent) = group.parent { + let mut nested = vec![]; + self.discover(&mut nested, &group.frame, page, ts); + self.insertions.insert(parent, nested); + } else { + self.discover(sink, &group.frame, page, ts); + } + } + FrameItem::Tag(Tag::Start(elem)) => { + let loc = elem.location().unwrap(); + if self.seen.insert(loc) { + let point = pos.transform(ts); + sink.push((elem.clone(), Position { page, point })); + } + } + FrameItem::Tag(Tag::End(loc, key)) => { + self.keys.insert(*key, *loc); + } + _ => {} + } + } + } + + /// Saves a pair and all its descendants into `elems` and populates the + /// acceleration structures. + fn visit(&mut self, elems: &mut Vec<Pair>, pair: Pair) { + let elem = &pair.0; + let loc = elem.location().unwrap(); + let idx = elems.len(); + + // Populate the location acceleration map. + self.locations.insert(loc, idx); + + // Populate the label acceleration map. + if let Some(label) = elem.label() { + self.labels.insert(label, idx); + } + + // Save the element. + elems.push(pair); + + // Process potential descendants. + if let Some(insertions) = self.insertions.take(&loc) { + for pair in insertions.flatten() { + self.visit(elems, pair); + } + } + } +} diff --git a/crates/typst-library/src/introspection/locate.rs b/crates/typst-library/src/introspection/locate.rs new file mode 100644 index 00000000..9a0e28e2 --- /dev/null +++ b/crates/typst-library/src/introspection/locate.rs @@ -0,0 +1,119 @@ +use comemo::{Track, Tracked}; +use typst_syntax::Span; + +use crate::diag::{warning, HintedStrResult, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, func, Content, Context, Func, LocatableSelector, NativeElement, Packed, + Show, StyleChain, Value, +}; +use crate::introspection::{Locatable, Location}; + +/// Determines the location of an element in the document. +/// +/// Takes a selector that must match exactly one element and returns that +/// element's [`location`]. This location can, in particular, be used to +/// retrieve the physical [`page`]($location.page) number and +/// [`position`]($location.position) (page, x, y) for that element. +/// +/// # Examples +/// Locating a specific element: +/// ```example +/// #context [ +/// Introduction is at: \ +/// #locate(<intro>).position() +/// ] +/// +/// = Introduction <intro> +/// ``` +/// +/// # Compatibility +/// In Typst 0.10 and lower, the `locate` function took a closure that made the +/// current location in the document available (like [`here`] does now). This +/// usage pattern is deprecated. Compatibility with the old way will remain for +/// a while to give package authors time to upgrade. To that effect, `locate` +/// detects whether it received a selector or a user-defined function and +/// adjusts its semantics accordingly. This behaviour will be removed in the +/// future. +#[func(contextual)] +pub fn locate( + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The span of the `locate` call. + span: Span, + /// A selector that should match exactly one element. This element will be + /// located. + /// + /// Especially useful in combination with + /// - [`here`] to locate the current context, + /// - a [`location`] retrieved from some queried element via the + /// [`location()`]($content.location) method on content. + selector: LocateInput, +) -> HintedStrResult<LocateOutput> { + Ok(match selector { + LocateInput::Selector(selector) => { + LocateOutput::Location(selector.resolve_unique(engine.introspector, context)?) + } + LocateInput::Func(func) => { + engine.sink.warn(warning!( + span, "`locate` with callback function is deprecated"; + hint: "use a `context` expression instead" + )); + + LocateOutput::Content(LocateElem::new(func).pack().spanned(span)) + } + }) +} + +/// Compatible input type. +pub enum LocateInput { + Selector(LocatableSelector), + Func(Func), +} + +cast! { + LocateInput, + v: Func => { + if v.element().is_some() { + Self::Selector(Value::Func(v).cast()?) + } else { + Self::Func(v) + } + }, + v: LocatableSelector => Self::Selector(v), +} + +/// Compatible output type. +pub enum LocateOutput { + Location(Location), + Content(Content), +} + +cast! { + LocateOutput, + self => match self { + Self::Location(v) => v.into_value(), + Self::Content(v) => v.into_value(), + }, + v: Location => Self::Location(v), + v: Content => Self::Content(v), +} + +/// Executes a `locate` call. +#[elem(Locatable, Show)] +struct LocateElem { + /// The function to call with the location. + #[required] + func: Func, +} + +impl Show for Packed<LocateElem> { + #[typst_macros::time(name = "locate", span = self.span())] + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let location = self.location().unwrap(); + let context = Context::new(Some(location), Some(styles)); + Ok(self.func().call(engine, context.track(), [location])?.display()) + } +} diff --git a/crates/typst-library/src/introspection/location.rs b/crates/typst-library/src/introspection/location.rs new file mode 100644 index 00000000..8f4caaec --- /dev/null +++ b/crates/typst-library/src/introspection/location.rs @@ -0,0 +1,113 @@ +use std::fmt::{self, Debug, Formatter}; +use std::num::NonZeroUsize; + +use ecow::EcoString; + +use crate::engine::Engine; +use crate::foundations::{func, scope, ty, Repr}; +use crate::layout::Position; +use crate::model::Numbering; + +/// Identifies an element in the document. +/// +/// A location uniquely identifies an element in the document and lets you +/// access its absolute position on the pages. You can retrieve the current +/// location with the [`here`] function and the location of a queried or shown +/// element with the [`location()`]($content.location) method on content. +/// +/// # Locatable elements { #locatable } +/// Currently, only a subset of element functions is locatable. Aside from +/// headings and figures, this includes equations, references, quotes and all +/// elements with an explicit label. As a result, you _can_ query for e.g. +/// [`strong`] elements, but you will find only those that have an explicit +/// label attached to them. This limitation will be resolved in the future. +#[ty(scope)] +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct Location(u128); + +impl Location { + /// Create a new location from a unique hash. + pub fn new(hash: u128) -> Self { + Self(hash) + } + + /// Extract the raw hash. + pub fn hash(self) -> u128 { + self.0 + } + + /// Produces a well-known variant of this location. + /// + /// This is a synthetic location created from another one and is used, for + /// example, in bibliography management to create individual linkable + /// locations for reference entries from the bibliography's location. + pub fn variant(self, n: usize) -> Self { + Self(typst_utils::hash128(&(self.0, n))) + } +} + +#[scope] +impl Location { + /// Returns the page number for this location. + /// + /// Note that this does not return the value of the [page counter]($counter) + /// at this location, but the true page number (starting from one). + /// + /// If you want to know the value of the page counter, use + /// `{counter(page).at(loc)}` instead. + /// + /// Can be used with [`here`] to retrieve the physical page position + /// of the current context: + /// ```example + /// #context [ + /// I am located on + /// page #here().page() + /// ] + /// ``` + #[func] + pub fn page(self, engine: &mut Engine) -> NonZeroUsize { + engine.introspector.page(self) + } + + /// Returns a dictionary with the page number and the x, y position for this + /// location. The page number starts at one and the coordinates are measured + /// from the top-left of the page. + /// + /// If you only need the page number, use `page()` instead as it allows + /// Typst to skip unnecessary work. + #[func] + pub fn position(self, engine: &mut Engine) -> Position { + engine.introspector.position(self) + } + + /// Returns the page numbering pattern of the page at this location. This + /// can be used when displaying the page counter in order to obtain the + /// local numbering. This is useful if you are building custom indices or + /// outlines. + /// + /// If the page numbering is set to `{none}` at that location, this function + /// returns `{none}`. + #[func] + pub fn page_numbering(self, engine: &mut Engine) -> Option<Numbering> { + engine.introspector.page_numbering(self).cloned() + } +} + +impl Debug for Location { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Location({})", self.0) + } +} + +impl Repr for Location { + fn repr(&self) -> EcoString { + "..".into() + } +} + +/// Makes this element as locatable through the introspector. +pub trait Locatable {} + +/// Marks this element as not being queryable even though it is locatable for +/// internal reasons. +pub trait Unqueriable {} diff --git a/crates/typst-library/src/introspection/locator.rs b/crates/typst-library/src/introspection/locator.rs new file mode 100644 index 00000000..4045aa9c --- /dev/null +++ b/crates/typst-library/src/introspection/locator.rs @@ -0,0 +1,350 @@ +use std::collections::HashMap; +use std::fmt::{self, Debug, Formatter}; +use std::hash::Hash; +use std::sync::OnceLock; + +use comemo::{Tracked, Validate}; + +use crate::introspection::{Introspector, Location}; + +/// Provides locations for elements in the document. +/// +/// A [`Location`] is a unique ID for an element generated during realization. +/// +/// # How to use this +/// The same content may yield different results when laid out in different +/// parts of the document. To reflect this, every layout operation receives a +/// locator and every layout operation requires a locator. In code: +/// +/// - all layouters receive an owned `Locator` +/// - all layout functions take an owned `Locator` +/// +/// When a layouter only requires a single sublayout call, it can simply pass on +/// its locator. When a layouter needs to call multiple sublayouters, we need to +/// make an explicit decision: +/// +/// - Split: When we're layouting multiple distinct children (or other pieces of +/// content), we need to split up the locator with [`Locator::split`]. This +/// allows us to produce multiple new `Locator`s for the sublayouts. When we +/// split the locator, each sublocator will be a distinct entity and using it +/// to e.g. layout the same piece of figure content will yield distinctly +/// numbered figures. +/// +/// - Relayout: When we're layouting the same content multiple times (e.g. when +/// measuring something), we can call [`Locator::relayout`] to use the same +/// locator multiple times. This indicates to the compiler that it's actually +/// the same content. Using it to e.g. layout the same piece of figure content +/// will yield the same figure number both times. Typically, when we layout +/// something multiple times using `relayout`, only one of the outputs +/// actually ends up in the document, while the other outputs are only used +/// for measurement and then discarded. +/// +/// The `Locator` intentionally does not implement `Copy` and `Clone` so that it +/// can only be used once. This ensures that whenever we are layouting multiple +/// things, we make an explicit decision whether we want to split or relayout. +/// +/// # How it works +/// There are two primary considerations for the assignment of locations: +/// +/// 1. Locations should match up over multiple layout iterations, so that +/// elements can be identified as being the same: That's the whole point of +/// them. +/// +/// 2. Locations should be as stable as possible across document edits, so that +/// incremental compilation is effective. +/// +/// 3. We want to assign them with as little long-lived state as possible to +/// enable parallelization of the layout process. +/// +/// Let's look at a few different assignment strategies to get a feeling for +/// these requirements: +/// +/// - A very simple way to generate unique IDs would be to just increase a +/// counter for each element. In this setup, (1) is somewhat satisfied: In +/// principle, the counter will line up across iterations, but things start to +/// break down once we generate content dependent on introspection since the +/// IDs generated for that new content will shift the IDs for all following +/// elements in the document. (2) is not satisfied since an edit in the middle +/// of the document shifts all later IDs. (3) is obviously not satisfied. +/// Conclusion: Not great. +/// +/// - To make things more robust, we can incorporate some stable knowledge about +/// the element into the ID. For this, we can use the element's span since it +/// is already mostly unique: Elements resulting from different source code +/// locations are guaranteed to have different spans. However, we can also +/// have multiple distinct elements generated from the same source location: +/// e.g. `#for _ in range(5) { figure(..) }`. To handle this case, we can then +/// disambiguate elements with the same span with an increasing counter. In +/// this setup, (1) is mostly satisfied: Unless we do stuff like generating +/// colliding counter updates dependent on introspection, things will line up. +/// (2) is also reasonably well satisfied, as typical edits will only affect +/// the single element at the currently edited span. Only if we edit inside of +/// a function, loop, or similar construct, we will affect multiple elements. +/// (3) is still a problem though, since we count up. +/// +/// - What's left is to get rid of the mutable state. Note that layout is a +/// recursive process and has a tree-shaped execution graph. Thus, we can try +/// to determine an element's ID based on the path of execution taken in this +/// graph. Something like "3rd element in layer 1, 7th element in layer 2, +/// ..". This is basically the first approach, but on a per-layer basis. Thus, +/// we can again apply our trick from the second approach, and use the span + +/// disambiguation strategy on a per-layer basis: "1st element with span X in +/// layer 1, 3rd element with span Y in layer 2". The chance for a collision +/// is now pretty low and our state is wholly local to each level. So, if we +/// want to parallelize layout within a layer, we can generate the IDs for +/// that layer upfront and then start forking out. The final remaining +/// question is how we can compactly encode this information: For this, as +/// always, we use hashing! We incorporate the ID information from each layer +/// into a single hash and thanks to the collision resistance of 128-bit +/// SipHash, we get almost guaranteed unique locations. We don't even store +/// the full layer information at all, but rather hash _hierarchically:_ Let +/// `k_x` be our local per-layer ID for layer `x` and `h_x` be the full +/// combined hash for layer `x`. We compute `h_n = hash(h_(n-1), k_n)`. +/// +/// So that's what's going on conceptually in this type. For efficient +/// memoization, we do all of this in a tracked fashion, such that we only +/// observe the hash for all the layers above us, if we actually need to +/// generate a [`Location`]. Thus, if we have a piece of content that does not +/// contain any locatable elements, we can cache its layout even if it occurs in +/// different places. +/// +/// # Dealing with measurement +/// As explained above, any kind of measurement the compiler performs requires a +/// locator that matches the one used during real layout. This ensures that the +/// locations assigned during measurement match up exactly with the locations of +/// real document elements. Without this guarantee, many introspection-driven +/// features (like counters, state, and citations) don't work correctly (since +/// they perform queries dependent on concrete locations). +/// +/// This is all fine and good, but things get really tricky when the _user_ +/// measures such introspecting content since the user isn't kindly managing +/// locators for us. Our standard `Locator` workflow assigns locations that +/// depend a lot on the exact placement in the hierarchy of elements. For this +/// reason, something that is measured, but then placed into something like a +/// grid will get a location influenced by the grid. Without a locator, we can't +/// make the connection between the measured content and the real content, so we +/// can't ensure that the locations match up. +/// +/// One possible way to deal with this is to force the user to uniquely identify +/// content before being measured after all. This would mean that the user needs +/// to come up with an identifier that is unique within the surrounding context +/// block and attach it to the content in some way. However, after careful +/// consideration, I have concluded that this is simply too big of an ask from +/// users: Understanding why this is even necessary is pretty complicated and +/// how to best come up with a unique ID is even more so. +/// +/// For this reason, I chose an alternative best-effort approach: The locator +/// has a custom "measurement mode" (entered through [`LocatorLink::measure`]), +/// in which it does its best to assign locations that match up. Specifically, +/// it uses the key hashes of the individual locatable elements in the measured +/// content (which may not be unique if content is reused) and combines them +/// with the context's location to find the most likely matching real element. +/// This approach works correctly almost all of the time (especially for +/// "normal" hand-written content where the key hashes rarely collide, as +/// opposed to code-heavy things where they do). +/// +/// Support for enhancing this with user-provided uniqueness can still be added +/// in the future. It will most likely anyway be added simply because it's +/// automatically included when we add a way to "freeze" content for things like +/// slidehows. But it will be opt-in because it's just too much complication. +pub struct Locator<'a> { + /// A local hash that incorporates all layers since the last memoization + /// boundary. + local: u128, + /// A pointer to an outer cached locator, which contributes the information + /// for all the layers beyond the memoization boundary on-demand. + outer: Option<&'a LocatorLink<'a>>, +} + +impl<'a> Locator<'a> { + /// Create a new root-level locator. + /// + /// Should typically only be created at the document level, though there + /// are a few places where we use it as well that just don't support + /// introspection (e.g. drawable patterns). + pub fn root() -> Self { + Self { local: 0, outer: None } + } + + /// Creates a new synthetic locator. + /// + /// This can be used to create a new dependent layout based on an element. + /// This is used for layouting footnote entries based on the location + /// of the associated footnote. + pub fn synthesize(location: Location) -> Self { + Self { local: location.hash(), outer: None } + } + + /// Creates a new locator that points to the given link. + pub fn link(link: &'a LocatorLink<'a>) -> Self { + Self { local: 0, outer: Some(link) } + } +} + +impl<'a> Locator<'a> { + /// Returns a type that can be used to generate `Locator`s for multiple + /// child elements. See the type-level docs for more details. + pub fn split(self) -> SplitLocator<'a> { + SplitLocator { + local: self.local, + outer: self.outer, + disambiguators: HashMap::new(), + } + } + + /// Creates a copy of this locator for measurement or relayout of the same + /// content. See the type-level docs for more details. + /// + /// This is effectively just `Clone`, but the `Locator` doesn't implement + /// `Clone` to make this operation explicit. + pub fn relayout(&self) -> Self { + Self { local: self.local, outer: self.outer } + } +} + +#[comemo::track] +impl<'a> Locator<'a> { + /// Resolves the locator based on its local and the outer information. + fn resolve(&self) -> Resolved { + match self.outer { + None => Resolved::Hash(self.local), + Some(outer) => match outer.resolve() { + Resolved::Hash(outer) => { + Resolved::Hash(typst_utils::hash128(&(self.local, outer))) + } + Resolved::Measure(anchor) => Resolved::Measure(anchor), + }, + } + } +} + +impl Debug for Locator<'_> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Locator({:?})", self.resolve()) + } +} + +/// The fully resolved value of a locator. +#[derive(Debug, Copy, Clone, Hash)] +enum Resolved { + /// The full hash, incorporating the local and all outer information. + Hash(u128), + /// Indicates that the locator is in measurement mode, with the given anchor + /// location. + Measure(Location), +} + +/// A type that generates unique sublocators. +pub struct SplitLocator<'a> { + /// A local hash that incorporates all layers since the last memoization + /// boundary. + local: u128, + /// A pointer to an outer cached locator, which contributes the information + /// for all the layers beyond the memoization boundary on-demand. + outer: Option<&'a LocatorLink<'a>>, + /// Simply counts up the number of times we've seen each local hash. + disambiguators: HashMap<u128, usize>, +} + +impl<'a> SplitLocator<'a> { + /// Produces a sublocator for a subtree keyed by `key`. The keys do *not* + /// need to be unique among the `next()` calls on this split locator. (They + /// can even all be `&()`.) + /// + /// However, stable & mostly unique keys lead to more stable locations + /// throughout edits, improving incremental compilation performance. + /// + /// A common choice for a key is the span of the content that will be + /// layouted with this locator. + pub fn next<K: Hash>(&mut self, key: &K) -> Locator<'a> { + self.next_inner(typst_utils::hash128(key)) + } + + /// Produces a sublocator for a subtree. + pub fn next_inner(&mut self, key: u128) -> Locator<'a> { + // Produce a locator disambiguator, for elements with the same key + // within this `SplitLocator`. + let disambiguator = { + let slot = self.disambiguators.entry(key).or_default(); + std::mem::replace(slot, *slot + 1) + }; + + // Combine the key, disambiguator and local hash into a sub-local hash. + // The outer information is not yet merged into this, it is added + // on-demand in `Locator::resolve`. + let local = typst_utils::hash128(&(key, disambiguator, self.local)); + + Locator { outer: self.outer, local } + } + + /// Produces a unique location for an element. + pub fn next_location( + &mut self, + introspector: Tracked<Introspector>, + key: u128, + ) -> Location { + match self.next_inner(key).resolve() { + Resolved::Hash(hash) => Location::new(hash), + Resolved::Measure(anchor) => { + // If we aren't able to find a matching element in the document, + // default to the anchor, so that it's at least remotely in + // the right area (so that counters can be resolved). + introspector.locator(key, anchor).unwrap_or(anchor) + } + } + } +} + +/// A locator can be linked to this type to only access information across the +/// memoization boundary on-demand, improving the cache hit chance. +pub struct LocatorLink<'a> { + /// The link itself. + kind: LinkKind<'a>, + /// The cached resolved link. + resolved: OnceLock<Resolved>, +} + +/// The different kinds of locator links. +enum LinkKind<'a> { + /// An outer `Locator`, which we can resolved if necessary. + /// + /// 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(Tracked<'a, Locator<'a>, <Locator<'static> as Validate>::Constraint>), + /// A link which indicates that we are in measurement mode. + Measure(Location), +} + +impl<'a> LocatorLink<'a> { + /// Create a locator link. + pub fn new(outer: Tracked<'a, Locator<'a>>) -> Self { + LocatorLink { + kind: LinkKind::Outer(outer), + resolved: OnceLock::new(), + } + } + + /// Creates a link that puts any linked downstream locator into measurement + /// mode. + /// + /// Read the "Dealing with measurement" section of the [`Locator`] docs for + /// more details. + pub fn measure(anchor: Location) -> Self { + LocatorLink { + kind: LinkKind::Measure(anchor), + resolved: OnceLock::new(), + } + } + + /// Resolve the link. + /// + /// The result is cached in this link, so that we don't traverse the link + /// chain over and over again. + fn resolve(&self) -> Resolved { + *self.resolved.get_or_init(|| match self.kind { + LinkKind::Outer(outer) => outer.resolve(), + LinkKind::Measure(anchor) => Resolved::Measure(anchor), + }) + } +} diff --git a/crates/typst-library/src/introspection/metadata.rs b/crates/typst-library/src/introspection/metadata.rs new file mode 100644 index 00000000..06000174 --- /dev/null +++ b/crates/typst-library/src/introspection/metadata.rs @@ -0,0 +1,38 @@ +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{elem, Content, Packed, Show, StyleChain, Value}; +use crate::introspection::Locatable; + +/// Exposes a value to the query system without producing visible content. +/// +/// This element can be retrieved with the [`query`] function and from the +/// command line with +/// [`typst query`]($reference/introspection/query/#command-line-queries). Its +/// purpose is to expose an arbitrary value to the introspection system. To +/// identify a metadata value among others, you can attach a [`label`] to it and +/// query for that label. +/// +/// The `metadata` element is especially useful for command line queries because +/// it allows you to expose arbitrary values to the outside world. +/// +/// ```example +/// // Put metadata somewhere. +/// #metadata("This is a note") <note> +/// +/// // And find it from anywhere else. +/// #context { +/// query(<note>).first().value +/// } +/// ``` +#[elem(Show, Locatable)] +pub struct MetadataElem { + /// The value to embed into the document. + #[required] + pub value: Value, +} + +impl Show for Packed<MetadataElem> { + fn show(&self, _: &mut Engine, _styles: StyleChain) -> SourceResult<Content> { + Ok(Content::empty()) + } +} diff --git a/crates/typst-library/src/introspection/mod.rs b/crates/typst-library/src/introspection/mod.rs new file mode 100644 index 00000000..b1ff2e08 --- /dev/null +++ b/crates/typst-library/src/introspection/mod.rs @@ -0,0 +1,53 @@ +//! Interaction between document parts. + +mod counter; +#[path = "here.rs"] +mod here_; +mod introspector; +#[path = "locate.rs"] +mod locate_; +mod location; +mod locator; +mod metadata; +#[path = "query.rs"] +mod query_; +mod state; +mod tag; + +pub use self::counter::*; +pub use self::here_::*; +pub use self::introspector::*; +pub use self::locate_::*; +pub use self::location::*; +pub use self::locator::*; +pub use self::metadata::*; +pub use self::query_::*; +pub use self::state::*; +pub use self::tag::*; + +use crate::foundations::{category, Category, Scope}; + +/// Interactions between document parts. +/// +/// This category is home to Typst's introspection capabilities: With the +/// `counter` function, you can access and manipulate page, section, figure, and +/// equation counters or create custom ones. Meanwhile, the `query` function +/// lets you search for elements in the document to construct things like a list +/// of figures or headers which show the current chapter title. +/// +/// Most of the functions are _contextual._ It is recommended to read the chapter +/// on [context] before continuing here. +#[category] +pub static INTROSPECTION: Category; + +/// Hook up all `introspection` definitions. +pub fn define(global: &mut Scope) { + global.category(INTROSPECTION); + global.define_type::<Location>(); + global.define_type::<Counter>(); + global.define_type::<State>(); + global.define_elem::<MetadataElem>(); + global.define_func::<here>(); + global.define_func::<query>(); + global.define_func::<locate>(); +} diff --git a/crates/typst-library/src/introspection/query.rs b/crates/typst-library/src/introspection/query.rs new file mode 100644 index 00000000..7b106bf0 --- /dev/null +++ b/crates/typst-library/src/introspection/query.rs @@ -0,0 +1,171 @@ +use comemo::Tracked; +use typst_syntax::Span; + +use crate::diag::{warning, HintedStrResult}; +use crate::engine::Engine; +use crate::foundations::{func, Array, Context, LocatableSelector, Value}; +use crate::introspection::Location; + +/// Finds elements in the document. +/// +/// The `query` functions lets you search your document for elements of a +/// particular type or with a particular label. To use it, you first need to +/// ensure that [context] is available. +/// + +/// # Finding elements +/// In the example below, we manually create a table of contents instead of +/// using the [`outline`] function. +/// +/// To do this, we first query for all headings in the document at level 1 and +/// where `outlined` is true. Querying only for headings at level 1 ensures +/// that, for the purpose of this example, sub-headings are not included in the +/// table of contents. The `outlined` field is used to exclude the "Table of +/// Contents" heading itself. +/// +/// Note that we open a `context` to be able to use the `query` function. +/// +/// ```example +/// >>> #set page( +/// >>> width: 240pt, +/// >>> height: 180pt, +/// >>> margin: (top: 20pt, bottom: 35pt) +/// >>> ) +/// #set page(numbering: "1") +/// +/// #heading(outlined: false)[ +/// Table of Contents +/// ] +/// #context { +/// let chapters = query( +/// heading.where( +/// level: 1, +/// outlined: true, +/// ) +/// ) +/// for chapter in chapters { +/// let loc = chapter.location() +/// let nr = numbering( +/// loc.page-numbering(), +/// ..counter(page).at(loc), +/// ) +/// [#chapter.body #h(1fr) #nr \ ] +/// } +/// } +/// +/// = Introduction +/// #lorem(10) +/// #pagebreak() +/// +/// == Sub-Heading +/// #lorem(8) +/// +/// = Discussion +/// #lorem(18) +/// ``` +/// +/// To get the page numbers, we first get the location of the elements returned +/// by `query` with [`location`]($content.location). We then also retrieve the +/// [page numbering]($location.page-numbering) and [page +/// counter]($counter/#page-counter) at that location and apply the numbering to +/// the counter. +/// +/// # A word of caution { #caution } +/// To resolve all your queries, Typst evaluates and layouts parts of the +/// document multiple times. However, there is no guarantee that your queries +/// can actually be completely resolved. If you aren't careful a query can +/// affect itself—leading to a result that never stabilizes. +/// +/// In the example below, we query for all headings in the document. We then +/// generate as many headings. In the beginning, there's just one heading, +/// titled `Real`. Thus, `count` is `1` and one `Fake` heading is generated. +/// Typst sees that the query's result has changed and processes it again. This +/// time, `count` is `2` and two `Fake` headings are generated. This goes on and +/// on. As we can see, the output has a finite amount of headings. This is +/// because Typst simply gives up after a few attempts. +/// +/// In general, you should try not to write queries that affect themselves. The +/// same words of caution also apply to other introspection features like +/// [counters]($counter) and [state]. +/// +/// ```example +/// = Real +/// #context { +/// let elems = query(heading) +/// let count = elems.len() +/// count * [= Fake] +/// } +/// ``` +/// +/// # Command line queries +/// You can also perform queries from the command line with the `typst query` +/// command. This command executes an arbitrary query on the document and +/// returns the resulting elements in serialized form. Consider the following +/// `example.typ` file which contains some invisible [metadata]: +/// +/// ```typ +/// #metadata("This is a note") <note> +/// ``` +/// +/// You can execute a query on it as follows using Typst's CLI: +/// ```sh +/// $ typst query example.typ "<note>" +/// [ +/// { +/// "func": "metadata", +/// "value": "This is a note", +/// "label": "<note>" +/// } +/// ] +/// ``` +/// +/// Frequently, you're interested in only one specific field of the resulting +/// elements. In the case of the `metadata` element, the `value` field is the +/// interesting one. You can extract just this field with the `--field` +/// argument. +/// +/// ```sh +/// $ typst query example.typ "<note>" --field value +/// ["This is a note"] +/// ``` +/// +/// If you are interested in just a single element, you can use the `--one` +/// flag to extract just it. +/// +/// ```sh +/// $ typst query example.typ "<note>" --field value --one +/// "This is a note" +/// ``` +#[func(contextual)] +pub fn query( + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The span of the `query` call. + span: Span, + /// Can be + /// - an element function like a `heading` or `figure`, + /// - a `{<label>}`, + /// - a more complex selector like `{heading.where(level: 1)}`, + /// - or `{selector(heading).before(here())}`. + /// + /// Only [locatable]($location/#locatable) element functions are supported. + target: LocatableSelector, + /// _Compatibility:_ This argument is deprecated. It only exists for + /// compatibility with Typst 0.10 and lower and shouldn't be used anymore. + #[default] + location: Option<Location>, +) -> HintedStrResult<Array> { + if location.is_none() { + context.introspect()?; + } else { + engine.sink.warn(warning!( + span, "calling `query` with a location is deprecated"; + hint: "try removing the location argument" + )); + } + + let vec = engine.introspector.query(&target.0); + Ok(vec.into_iter().map(Value::Content).collect()) +} diff --git a/crates/typst-library/src/introspection/state.rs b/crates/typst-library/src/introspection/state.rs new file mode 100644 index 00000000..13c96b50 --- /dev/null +++ b/crates/typst-library/src/introspection/state.rs @@ -0,0 +1,476 @@ +use comemo::{Track, Tracked, TrackedMut}; +use ecow::{eco_format, eco_vec, EcoString, EcoVec}; +use typst_syntax::Span; + +use crate::diag::{bail, warning, At, SourceResult}; +use crate::engine::{Engine, Route, Sink, Traced}; +use crate::foundations::{ + cast, elem, func, scope, select_where, ty, Args, Construct, Content, Context, Func, + LocatableSelector, NativeElement, Packed, Repr, Selector, Show, Str, StyleChain, + Value, +}; +use crate::introspection::{Introspector, Locatable, Location}; +use crate::routines::Routines; +use crate::World; + +/// Manages stateful parts of your document. +/// +/// Let's say you have some computations in your document and want to remember +/// the result of your last computation to use it in the next one. You might try +/// something similar to the code below and expect it to output 10, 13, 26, and +/// 21. However this **does not work** in Typst. If you test this code, you will +/// see that Typst complains with the following error message: _Variables from +/// outside the function are read-only and cannot be modified._ +/// +/// ```typ +/// // This doesn't work! +/// #let x = 0 +/// #let compute(expr) = { +/// x = eval( +/// expr.replace("x", str(x)) +/// ) +/// [New value is #x. ] +/// } +/// +/// #compute("10") \ +/// #compute("x + 3") \ +/// #compute("x * 2") \ +/// #compute("x - 5") +/// ``` +/// +/// # State and document markup { #state-and-markup } +/// Why does it do that? Because, in general, this kind of computation with side +/// effects is problematic in document markup and Typst is upfront about that. +/// For the results to make sense, the computation must proceed in the same +/// order in which the results will be laid out in the document. In our simple +/// example, that's the case, but in general it might not be. +/// +/// Let's look at a slightly different, but similar kind of state: The heading +/// numbering. We want to increase the heading counter at each heading. Easy +/// enough, right? Just add one. Well, it's not that simple. Consider the +/// following example: +/// +/// ```example +/// #set heading(numbering: "1.") +/// #let template(body) = [ +/// = Outline +/// ... +/// #body +/// ] +/// +/// #show: template +/// +/// = Introduction +/// ... +/// ``` +/// +/// Here, Typst first processes the body of the document after the show rule, +/// sees the `Introduction` heading, then passes the resulting content to the +/// `template` function and only then sees the `Outline`. Just counting up would +/// number the `Introduction` with `1` and the `Outline` with `2`. +/// +/// # Managing state in Typst { #state-in-typst } +/// So what do we do instead? We use Typst's state management system. Calling +/// the `state` function with an identifying string key and an optional initial +/// value gives you a state value which exposes a few functions. The two most +/// important ones are `get` and `update`: +/// +/// - The [`get`]($state.get) function retrieves the current value of the state. +/// Because the value can vary over the course of the document, it is a +/// _contextual_ function that can only be used when [context]($context) is +/// available. +/// +/// - The [`update`]($state.update) function modifies the state. You can give it +/// any value. If given a non-function value, it sets the state to that value. +/// If given a function, that function receives the previous state and has to +/// return the new state. +/// +/// Our initial example would now look like this: +/// +/// ```example +/// #let s = state("x", 0) +/// #let compute(expr) = [ +/// #s.update(x => +/// eval(expr.replace("x", str(x))) +/// ) +/// New value is #context s.get(). +/// ] +/// +/// #compute("10") \ +/// #compute("x + 3") \ +/// #compute("x * 2") \ +/// #compute("x - 5") +/// ``` +/// +/// State managed by Typst is always updated in layout order, not in evaluation +/// order. The `update` method returns content and its effect occurs at the +/// position where the returned content is inserted into the document. +/// +/// As a result, we can now also store some of the computations in variables, +/// but they still show the correct results: +/// +/// ```example +/// >>> #let s = state("x", 0) +/// >>> #let compute(expr) = [ +/// >>> #s.update(x => +/// >>> eval(expr.replace("x", str(x))) +/// >>> ) +/// >>> New value is #context s.get(). +/// >>> ] +/// <<< ... +/// +/// #let more = [ +/// #compute("x * 2") \ +/// #compute("x - 5") +/// ] +/// +/// #compute("10") \ +/// #compute("x + 3") \ +/// #more +/// ``` +/// +/// This example is of course a bit silly, but in practice this is often exactly +/// what you want! A good example are heading counters, which is why Typst's +/// [counting system]($counter) is very similar to its state system. +/// +/// # Time Travel +/// By using Typst's state management system you also get time travel +/// capabilities! We can find out what the value of the state will be at any +/// position in the document from anywhere else. In particular, the `at` method +/// gives us the value of the state at any particular location and the `final` +/// methods gives us the value of the state at the end of the document. +/// +/// ```example +/// >>> #let s = state("x", 0) +/// >>> #let compute(expr) = [ +/// >>> #s.update(x => { +/// >>> eval(expr.replace("x", str(x))) +/// >>> }) +/// >>> New value is #context s.get(). +/// >>> ] +/// <<< ... +/// +/// Value at `<here>` is +/// #context s.at(<here>) +/// +/// #compute("10") \ +/// #compute("x + 3") \ +/// *Here.* <here> \ +/// #compute("x * 2") \ +/// #compute("x - 5") +/// ``` +/// +/// # A word of caution { #caution } +/// To resolve the values of all states, Typst evaluates parts of your code +/// multiple times. However, there is no guarantee that your state manipulation +/// can actually be completely resolved. +/// +/// For instance, if you generate state updates depending on the final value of +/// a state, the results might never converge. The example below illustrates +/// this. We initialize our state with `1` and then update it to its own final +/// value plus 1. So it should be `2`, but then its final value is `2`, so it +/// should be `3`, and so on. This example displays a finite value because Typst +/// simply gives up after a few attempts. +/// +/// ```example +/// // This is bad! +/// #let s = state("x", 1) +/// #context s.update(s.final() + 1) +/// #context s.get() +/// ``` +/// +/// In general, you should try not to generate state updates from within context +/// expressions. If possible, try to express your updates as non-contextual +/// values or functions that compute the new value from the previous value. +/// Sometimes, it cannot be helped, but in those cases it is up to you to ensure +/// that the result converges. +#[ty(scope)] +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct State { + /// The key that identifies the state. + key: Str, + /// The initial value of the state. + init: Value, +} + +impl State { + /// Create a new state identified by a key. + pub fn new(key: Str, init: Value) -> State { + Self { key, init } + } + + /// Get the value of the state at the given location. + pub fn at_loc(&self, engine: &mut Engine, loc: Location) -> SourceResult<Value> { + let sequence = self.sequence(engine)?; + let offset = engine.introspector.query_count_before(&self.selector(), loc); + Ok(sequence[offset].clone()) + } + + /// Produce the whole sequence of states. + /// + /// This has to happen just once for all states, cutting down the number + /// of state updates from quadratic to linear. + fn sequence(&self, engine: &mut Engine) -> SourceResult<EcoVec<Value>> { + self.sequence_impl( + engine.routines, + engine.world, + engine.introspector, + engine.traced, + TrackedMut::reborrow_mut(&mut engine.sink), + engine.route.track(), + ) + } + + /// Memoized implementation of `sequence`. + #[comemo::memoize] + fn sequence_impl( + &self, + routines: &Routines, + world: Tracked<dyn World + '_>, + introspector: Tracked<Introspector>, + traced: Tracked<Traced>, + sink: TrackedMut<Sink>, + route: Tracked<Route>, + ) -> SourceResult<EcoVec<Value>> { + let mut engine = Engine { + routines, + world, + introspector, + traced, + sink, + route: Route::extend(route).unnested(), + }; + let mut state = self.init.clone(); + let mut stops = eco_vec![state.clone()]; + + for elem in introspector.query(&self.selector()) { + let elem = elem.to_packed::<StateUpdateElem>().unwrap(); + match elem.update() { + StateUpdate::Set(value) => state = value.clone(), + StateUpdate::Func(func) => { + state = func.call(&mut engine, Context::none().track(), [state])? + } + } + stops.push(state.clone()); + } + + Ok(stops) + } + + /// The selector for this state's updates. + fn selector(&self) -> Selector { + select_where!(StateUpdateElem, Key => self.key.clone()) + } +} + +#[scope] +impl State { + /// Create a new state identified by a key. + #[func(constructor)] + pub fn construct( + /// The key that identifies this state. + key: Str, + /// The initial value of the state. + #[default] + init: Value, + ) -> State { + Self::new(key, init) + } + + /// Retrieves the value of the state at the current location. + /// + /// This is equivalent to `{state.at(here())}`. + #[func(contextual)] + pub fn get( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The callsite span. + span: Span, + ) -> SourceResult<Value> { + let loc = context.location().at(span)?; + self.at_loc(engine, loc) + } + + /// Retrieves the value of the state at the given selector's unique match. + /// + /// The `selector` must match exactly one element in the document. The most + /// useful kinds of selectors for this are [labels]($label) and + /// [locations]($location). + /// + /// _Compatibility:_ For compatibility with Typst 0.10 and lower, this + /// function also works without a known context if the `selector` is a + /// location. This behaviour will be removed in a future release. + #[func(contextual)] + pub fn at( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The callsite span. + span: Span, + /// The place at which the state's value should be retrieved. + selector: LocatableSelector, + ) -> SourceResult<Value> { + let loc = selector.resolve_unique(engine.introspector, context).at(span)?; + self.at_loc(engine, loc) + } + + /// Retrieves the value of the state at the end of the document. + #[func(contextual)] + pub fn final_( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The callsite span. + span: Span, + /// _Compatibility:_ This argument is deprecated. It only exists for + /// compatibility with Typst 0.10 and lower and shouldn't be used + /// anymore. + #[default] + location: Option<Location>, + ) -> SourceResult<Value> { + if location.is_none() { + context.location().at(span)?; + } else { + engine.sink.warn(warning!( + span, "calling `state.final` with a location is deprecated"; + hint: "try removing the location argument" + )); + } + + let sequence = self.sequence(engine)?; + Ok(sequence.last().unwrap().clone()) + } + + /// Update the value of the state. + /// + /// The update will be in effect at the position where the returned content + /// is inserted into the document. If you don't put the output into the + /// document, nothing happens! This would be the case, for example, if you + /// write `{let _ = state("key").update(7)}`. State updates are always + /// applied in layout order and in that case, Typst wouldn't know when to + /// update the state. + #[func] + pub fn update( + self, + /// The span of the `update` call. + span: Span, + /// If given a non function-value, sets the state to that value. If + /// given a function, that function receives the previous state and has + /// to return the new state. + update: StateUpdate, + ) -> Content { + StateUpdateElem::new(self.key, update).pack().spanned(span) + } + + /// Displays the current value of the state. + /// + /// **Deprecation planned:** Use [`get`]($state.get) instead. + #[func] + pub fn display( + self, + /// The engine. + engine: &mut Engine, + /// The span of the `display` call. + span: Span, + /// A function which receives the value of the state and can return + /// arbitrary content which is then displayed. If this is omitted, the + /// value is directly displayed. + #[default] + func: Option<Func>, + ) -> Content { + engine.sink.warn(warning!( + span, "`state.display` is deprecated"; + hint: "use `state.get` in a `context` expression instead" + )); + + StateDisplayElem::new(self, func).pack().spanned(span) + } +} + +impl Repr for State { + fn repr(&self) -> EcoString { + eco_format!("state({}, {})", self.key.repr(), self.init.repr()) + } +} + +/// An update to perform on a state. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum StateUpdate { + /// Set the state to the specified value. + Set(Value), + /// Apply the given function to the state. + Func(Func), +} + +cast! { + StateUpdate, + v: Func => Self::Func(v), + v: Value => Self::Set(v), +} + +/// Executes a display of a state. +#[elem(Construct, Locatable, Show)] +struct StateUpdateElem { + /// The key that identifies the state. + #[required] + key: Str, + + /// The update to perform on the state. + #[required] + #[internal] + update: StateUpdate, +} + +impl Construct for StateUpdateElem { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> { + bail!(args.span, "cannot be constructed manually"); + } +} + +impl Show for Packed<StateUpdateElem> { + fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(Content::empty()) + } +} + +/// Executes a display of a state. +/// +/// **Deprecation planned.** +#[elem(Construct, Locatable, Show)] +struct StateDisplayElem { + /// The state. + #[required] + #[internal] + state: State, + + /// The function to display the state with. + #[required] + #[internal] + func: Option<Func>, +} + +impl Show for Packed<StateDisplayElem> { + #[typst_macros::time(name = "state.display", span = self.span())] + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let location = self.location().unwrap(); + let context = Context::new(Some(location), Some(styles)); + let value = self.state().at_loc(engine, location)?; + Ok(match self.func() { + Some(func) => func.call(engine, context.track(), [value])?.display(), + None => value.display(), + }) + } +} + +impl Construct for StateDisplayElem { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> { + bail!(args.span, "cannot be constructed manually"); + } +} diff --git a/crates/typst-library/src/introspection/tag.rs b/crates/typst-library/src/introspection/tag.rs new file mode 100644 index 00000000..b2bae28e --- /dev/null +++ b/crates/typst-library/src/introspection/tag.rs @@ -0,0 +1,73 @@ +use std::fmt::{self, Debug, Formatter}; + +use crate::diag::{bail, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + elem, Args, Construct, Content, NativeElement, Packed, Unlabellable, +}; +use crate::introspection::Location; + +/// Marks the start or end of a locatable element. +#[derive(Clone, PartialEq, Hash)] +pub enum Tag { + /// The stored element starts here. + /// + /// Content placed in a tag **must** have a [`Location`] or there will be + /// panics. + Start(Content), + /// The element with the given location and key hash ends here. + /// + /// Note: The key hash is stored here instead of in `Start` simply to make + /// the two enum variants more balanced in size, keeping a `Tag`'s memory + /// size down. There are no semantic reasons for this. + End(Location, u128), +} + +impl Tag { + /// Access the location of the tag. + pub fn location(&self) -> Location { + match self { + Tag::Start(elem) => elem.location().unwrap(), + Tag::End(loc, _) => *loc, + } + } +} + +impl Debug for Tag { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Tag::Start(elem) => write!(f, "Start({:?})", elem.elem().name()), + Tag::End(..) => f.pad("End"), + } + } +} + +/// Holds a tag for a locatable element that was realized. +/// +/// The `TagElem` is handled by all layouters. The held element becomes +/// available for introspection in the next compiler iteration. +#[elem(Construct, Unlabellable)] +pub struct TagElem { + /// The introspectable element. + #[required] + #[internal] + pub tag: Tag, +} + +impl TagElem { + /// Create a packed tag element. + pub fn packed(tag: Tag) -> Content { + let mut content = Self::new(tag).pack(); + // We can skip preparation for the `TagElem`. + content.mark_prepared(); + content + } +} + +impl Construct for TagElem { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> { + bail!(args.span, "cannot be constructed manually") + } +} + +impl Unlabellable for Packed<TagElem> {} diff --git a/crates/typst-library/src/layout/abs.rs b/crates/typst-library/src/layout/abs.rs new file mode 100644 index 00000000..e19b037a --- /dev/null +++ b/crates/typst-library/src/layout/abs.rs @@ -0,0 +1,279 @@ +use std::fmt::{self, Debug, Formatter}; +use std::iter::Sum; +use std::ops::{Add, Div, Mul, Neg, Rem}; + +use ecow::EcoString; +use typst_utils::{Numeric, Scalar}; + +use crate::foundations::{cast, repr, Fold, Repr, Value}; + +/// 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::ZERO) + } + + /// The infinite length. + pub const fn inf() -> Self { + Self(Scalar::INFINITY) + } + + /// Create an absolute length from a number of raw units. + pub const fn raw(raw: f64) -> Self { + Self(Scalar::new(raw)) + } + + /// Create an absolute length from a value in a unit. + pub fn with_unit(val: f64, unit: AbsUnit) -> Self { + Self(Scalar::new(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.get() + } + + /// 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 + AbsUnit::EPS >= 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() < AbsUnit::EPS + } + + /// Whether the size is close to zero or negative. + pub fn approx_empty(self) -> bool { + self.to_raw() <= AbsUnit::EPS + } + + /// Returns a number that represent the sign of this length + pub fn signum(self) -> f64 { + self.0.get().signum() + } +} + +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", self.to_pt()) + } +} + +impl Repr for Abs { + fn repr(&self) -> EcoString { + repr::format_float_with_unit(self.to_pt(), "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) + } +} + +typst_utils::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() + } +} + +typst_utils::assign_impl!(Abs += Abs); +typst_utils::assign_impl!(Abs -= Abs); +typst_utils::assign_impl!(Abs *= f64); +typst_utils::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()) + } +} + +impl Fold for Abs { + fn fold(self, _: Self) -> Self { + self + } +} + +cast! { + Abs, + self => Value::Length(self.into()), +} + +/// Different units of absolute measurement. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum AbsUnit { + /// Points. + Pt, + /// Millimeters. + Mm, + /// Centimeters. + Cm, + /// Inches. + In, +} + +impl AbsUnit { + /// The epsilon for approximate length comparisons. + const EPS: f64 = 1e-4; + + /// How many raw units correspond to a value of `1.0` in this unit. + const fn raw_scale(self) -> f64 { + // We choose a raw scale which has an integer conversion value to all + // four units of interest, so that whole numbers in all units can be + // represented accurately. + match self { + AbsUnit::Pt => 127.0, + AbsUnit::Mm => 360.0, + AbsUnit::Cm => 3600.0, + AbsUnit::In => 9144.0, + } + } +} + +#[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/crates/typst-library/src/layout/align.rs b/crates/typst-library/src/layout/align.rs new file mode 100644 index 00000000..e8ba4d7c --- /dev/null +++ b/crates/typst-library/src/layout/align.rs @@ -0,0 +1,748 @@ +use std::ops::Add; + +use ecow::{eco_format, EcoString}; + +use crate::diag::{bail, HintedStrResult, SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, func, scope, ty, CastInfo, Content, Fold, FromValue, IntoValue, Packed, + Reflect, Repr, Resolve, Show, StyleChain, Value, +}; +use crate::layout::{Abs, Axes, Axis, Dir, Side}; +use crate::text::TextElem; + +/// Aligns content horizontally and vertically. +/// +/// # Example +/// Let's start with centering our content horizontally: +/// ```example +/// #set page(height: 120pt) +/// #set align(center) +/// +/// Centered text, a sight to see \ +/// In perfect balance, visually \ +/// Not left nor right, it stands alone \ +/// A work of art, a visual throne +/// ``` +/// +/// To center something vertically, use _horizon_ alignment: +/// ```example +/// #set page(height: 120pt) +/// #set align(horizon) +/// +/// Vertically centered, \ +/// the stage had entered, \ +/// a new paragraph. +/// ``` +/// +/// # Combining alignments +/// You can combine two alignments with the `+` operator. Let's also only apply +/// this to one piece of content by using the function form instead of a set +/// rule: +/// ```example +/// #set page(height: 120pt) +/// Though left in the beginning ... +/// +/// #align(right + bottom)[ +/// ... they were right in the end, \ +/// and with addition had gotten, \ +/// the paragraph to the bottom! +/// ] +/// ``` +/// +/// # Nested alignment +/// You can use varying alignments for layout containers and the elements within +/// them. This way, you can create intricate layouts: +/// +/// ```example +/// #align(center, block[ +/// #set align(left) +/// Though centered together \ +/// alone \ +/// we \ +/// are \ +/// left. +/// ]) +/// ``` +/// +/// # Alignment within the same line +/// The `align` function performs block-level alignment and thus always +/// interrupts the current paragraph. To have different alignment for parts +/// of the same line, you should use [fractional spacing]($h) instead: +/// +/// ```example +/// Start #h(1fr) End +/// ``` +#[elem(Show)] +pub struct AlignElem { + /// The [alignment] along both axes. + /// + /// ```example + /// #set page(height: 6cm) + /// #set text(lang: "ar") + /// + /// مثال + /// #align( + /// end + horizon, + /// rect(inset: 12pt)[ركن] + /// ) + /// ``` + #[positional] + #[fold] + #[default] + pub alignment: Alignment, + + /// The content to align. + #[required] + pub body: Content, +} + +impl Show for Packed<AlignElem> { + #[typst_macros::time(name = "align", span = self.span())] + fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + Ok(self.body().clone().aligned(self.alignment(styles))) + } +} + +/// Where to [align] something along an axis. +/// +/// Possible values are: +/// - `start`: Aligns at the [start]($direction.start) of the [text +/// direction]($text.dir). +/// - `end`: Aligns at the [end]($direction.end) of the [text +/// direction]($text.dir). +/// - `left`: Align at the left. +/// - `center`: Aligns in the middle, horizontally. +/// - `right`: Aligns at the right. +/// - `top`: Aligns at the top. +/// - `horizon`: Aligns in the middle, vertically. +/// - `bottom`: Align at the bottom. +/// +/// These values are available globally and also in the alignment type's scope, +/// so you can write either of the following two: +/// +/// ```example +/// #align(center)[Hi] +/// #align(alignment.center)[Hi] +/// ``` +/// +/// # 2D alignments +/// To align along both axes at the same time, add the two alignments using the +/// `+` operator. For example, `top + right` aligns the content to the top right +/// corner. +/// +/// ```example +/// #set page(height: 3cm) +/// #align(center + bottom)[Hi] +/// ``` +/// +/// # Fields +/// The `x` and `y` fields hold the alignment's horizontal and vertical +/// components, respectively (as yet another `alignment`). They may be `{none}`. +/// +/// ```example +/// #(top + right).x \ +/// #left.x \ +/// #left.y (none) +/// ``` +#[ty(scope)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Alignment { + H(HAlignment), + V(VAlignment), + Both(HAlignment, VAlignment), +} + +impl Alignment { + /// The horizontal component. + pub const fn x(self) -> Option<HAlignment> { + match self { + Self::H(h) | Self::Both(h, _) => Some(h), + Self::V(_) => None, + } + } + + /// The vertical component. + pub const fn y(self) -> Option<VAlignment> { + match self { + Self::V(v) | Self::Both(_, v) => Some(v), + Self::H(_) => None, + } + } + + /// Normalize the alignment to a LTR-TTB space. + pub fn fix(self, text_dir: Dir) -> Axes<FixedAlignment> { + Axes::new( + self.x().unwrap_or_default().fix(text_dir), + self.y().unwrap_or_default().fix(text_dir), + ) + } +} + +#[scope] +impl Alignment { + pub const START: Self = Alignment::H(HAlignment::Start); + pub const LEFT: Self = Alignment::H(HAlignment::Left); + pub const CENTER: Self = Alignment::H(HAlignment::Center); + pub const RIGHT: Self = Alignment::H(HAlignment::Right); + pub const END: Self = Alignment::H(HAlignment::End); + pub const TOP: Self = Alignment::V(VAlignment::Top); + pub const HORIZON: Self = Alignment::V(VAlignment::Horizon); + pub const BOTTOM: Self = Alignment::V(VAlignment::Bottom); + + /// The axis this alignment belongs to. + /// - `{"horizontal"}` for `start`, `left`, `center`, `right`, and `end` + /// - `{"vertical"}` for `top`, `horizon`, and `bottom` + /// - `{none}` for 2-dimensional alignments + /// + /// ```example + /// #left.axis() \ + /// #bottom.axis() + /// ``` + #[func] + pub const fn axis(self) -> Option<Axis> { + match self { + Self::H(_) => Some(Axis::X), + Self::V(_) => Some(Axis::Y), + Self::Both(..) => None, + } + } + + /// The inverse alignment. + /// + /// ```example + /// #top.inv() \ + /// #left.inv() \ + /// #center.inv() \ + /// #(left + bottom).inv() + /// ``` + #[func(title = "Inverse")] + pub const fn inv(self) -> Alignment { + match self { + Self::H(h) => Self::H(h.inv()), + Self::V(v) => Self::V(v.inv()), + Self::Both(h, v) => Self::Both(h.inv(), v.inv()), + } + } +} + +impl Default for Alignment { + fn default() -> Self { + HAlignment::default() + VAlignment::default() + } +} + +impl Add for Alignment { + type Output = StrResult<Self>; + + fn add(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (Self::H(h), Self::V(v)) | (Self::V(v), Self::H(h)) => Ok(h + v), + (Self::H(_), Self::H(_)) => bail!("cannot add two horizontal alignments"), + (Self::V(_), Self::V(_)) => bail!("cannot add two vertical alignments"), + (Self::H(_), Self::Both(..)) | (Self::Both(..), Self::H(_)) => { + bail!("cannot add a horizontal and a 2D alignment") + } + (Self::V(_), Self::Both(..)) | (Self::Both(..), Self::V(_)) => { + bail!("cannot add a vertical and a 2D alignment") + } + (Self::Both(..), Self::Both(..)) => { + bail!("cannot add two 2D alignments") + } + } + } +} + +impl Repr for Alignment { + fn repr(&self) -> EcoString { + match self { + Self::H(h) => h.repr(), + Self::V(v) => v.repr(), + Self::Both(h, v) => eco_format!("{} + {}", h.repr(), v.repr()), + } + } +} + +impl Fold for Alignment { + fn fold(self, outer: Self) -> Self { + match (self, outer) { + (Self::H(h), Self::V(v) | Self::Both(_, v)) => Self::Both(h, v), + (Self::V(v), Self::H(h) | Self::Both(h, _)) => Self::Both(h, v), + _ => self, + } + } +} + +impl Resolve for Alignment { + type Output = Axes<FixedAlignment>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.fix(TextElem::dir_in(styles)) + } +} + +impl From<Side> for Alignment { + fn from(side: Side) -> Self { + match side { + Side::Left => Self::LEFT, + Side::Top => Self::TOP, + Side::Right => Self::RIGHT, + Side::Bottom => Self::BOTTOM, + } + } +} + +/// Alignment on this axis can be fixed to an absolute direction. +pub trait FixAlignment { + /// Resolve to the absolute alignment. + fn fix(self, dir: Dir) -> FixedAlignment; +} + +/// Where to align something horizontally. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub enum HAlignment { + #[default] + Start, + Left, + Center, + Right, + End, +} + +impl HAlignment { + /// The inverse horizontal alignment. + pub const fn inv(self) -> Self { + match self { + Self::Start => Self::End, + Self::Left => Self::Right, + Self::Center => Self::Center, + Self::Right => Self::Left, + Self::End => Self::Start, + } + } +} + +impl FixAlignment for HAlignment { + fn fix(self, dir: Dir) -> FixedAlignment { + match (self, dir.is_positive()) { + (Self::Start, true) | (Self::End, false) => FixedAlignment::Start, + (Self::Left, _) => FixedAlignment::Start, + (Self::Center, _) => FixedAlignment::Center, + (Self::Right, _) => FixedAlignment::End, + (Self::End, true) | (Self::Start, false) => FixedAlignment::End, + } + } +} + +impl Repr for HAlignment { + fn repr(&self) -> EcoString { + match self { + Self::Start => "start".into(), + Self::Left => "left".into(), + Self::Center => "center".into(), + Self::Right => "right".into(), + Self::End => "end".into(), + } + } +} + +impl Add<VAlignment> for HAlignment { + type Output = Alignment; + + fn add(self, rhs: VAlignment) -> Self::Output { + Alignment::Both(self, rhs) + } +} + +impl From<HAlignment> for Alignment { + fn from(align: HAlignment) -> Self { + Self::H(align) + } +} + +impl TryFrom<Alignment> for HAlignment { + type Error = EcoString; + + fn try_from(value: Alignment) -> StrResult<Self> { + match value { + Alignment::H(h) => Ok(h), + v => bail!( + "expected `start`, `left`, `center`, `right`, or `end`, found {}", + v.repr() + ), + } + } +} + +impl Resolve for HAlignment { + type Output = FixedAlignment; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.fix(TextElem::dir_in(styles)) + } +} + +cast! { + HAlignment, + self => Alignment::H(self).into_value(), + align: Alignment => Self::try_from(align)?, +} + +/// A horizontal alignment which only allows `left`/`right` and `start`/`end`, +/// thus excluding `center`. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub enum OuterHAlignment { + #[default] + Start, + Left, + Right, + End, +} + +impl FixAlignment for OuterHAlignment { + fn fix(self, dir: Dir) -> FixedAlignment { + match (self, dir.is_positive()) { + (Self::Start, true) | (Self::End, false) => FixedAlignment::Start, + (Self::Left, _) => FixedAlignment::Start, + (Self::Right, _) => FixedAlignment::End, + (Self::End, true) | (Self::Start, false) => FixedAlignment::End, + } + } +} + +impl Resolve for OuterHAlignment { + type Output = FixedAlignment; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.fix(TextElem::dir_in(styles)) + } +} + +impl From<OuterHAlignment> for HAlignment { + fn from(value: OuterHAlignment) -> Self { + match value { + OuterHAlignment::Start => Self::Start, + OuterHAlignment::Left => Self::Left, + OuterHAlignment::Right => Self::Right, + OuterHAlignment::End => Self::End, + } + } +} + +impl TryFrom<Alignment> for OuterHAlignment { + type Error = EcoString; + + fn try_from(value: Alignment) -> StrResult<Self> { + match value { + Alignment::H(HAlignment::Start) => Ok(Self::Start), + Alignment::H(HAlignment::Left) => Ok(Self::Left), + Alignment::H(HAlignment::Right) => Ok(Self::Right), + Alignment::H(HAlignment::End) => Ok(Self::End), + v => bail!("expected `start`, `left`, `right`, or `end`, found {}", v.repr()), + } + } +} + +cast! { + OuterHAlignment, + self => HAlignment::from(self).into_value(), + align: Alignment => Self::try_from(align)?, +} + +/// Where to align something vertically. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum VAlignment { + #[default] + Top, + Horizon, + Bottom, +} + +impl VAlignment { + /// The inverse vertical alignment. + pub const fn inv(self) -> Self { + match self { + 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::Top => Abs::zero(), + Self::Horizon => extent / 2.0, + Self::Bottom => extent, + } + } +} + +impl FixAlignment for VAlignment { + fn fix(self, _: Dir) -> FixedAlignment { + // The vertical alignment does not depend on text direction. + match self { + Self::Top => FixedAlignment::Start, + Self::Horizon => FixedAlignment::Center, + Self::Bottom => FixedAlignment::End, + } + } +} + +impl Repr for VAlignment { + fn repr(&self) -> EcoString { + match self { + Self::Top => "top".into(), + Self::Horizon => "horizon".into(), + Self::Bottom => "bottom".into(), + } + } +} + +impl Add<HAlignment> for VAlignment { + type Output = Alignment; + + fn add(self, rhs: HAlignment) -> Self::Output { + Alignment::Both(rhs, self) + } +} + +impl Resolve for VAlignment { + type Output = FixedAlignment; + + fn resolve(self, _: StyleChain) -> Self::Output { + self.fix(Dir::TTB) + } +} + +impl From<VAlignment> for Alignment { + fn from(align: VAlignment) -> Self { + Self::V(align) + } +} + +impl TryFrom<Alignment> for VAlignment { + type Error = EcoString; + + fn try_from(value: Alignment) -> StrResult<Self> { + match value { + Alignment::V(v) => Ok(v), + v => bail!("expected `top`, `horizon`, or `bottom`, found {}", v.repr()), + } + } +} + +cast! { + VAlignment, + self => Alignment::V(self).into_value(), + align: Alignment => Self::try_from(align)?, +} + +/// A vertical alignment which only allows `top` and `bottom`, thus excluding +/// `horizon`. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum OuterVAlignment { + #[default] + Top, + Bottom, +} + +impl FixAlignment for OuterVAlignment { + fn fix(self, _: Dir) -> FixedAlignment { + // The vertical alignment does not depend on text direction. + match self { + Self::Top => FixedAlignment::Start, + Self::Bottom => FixedAlignment::End, + } + } +} + +impl From<OuterVAlignment> for VAlignment { + fn from(value: OuterVAlignment) -> Self { + match value { + OuterVAlignment::Top => Self::Top, + OuterVAlignment::Bottom => Self::Bottom, + } + } +} + +impl TryFrom<Alignment> for OuterVAlignment { + type Error = EcoString; + + fn try_from(value: Alignment) -> StrResult<Self> { + match value { + Alignment::V(VAlignment::Top) => Ok(Self::Top), + Alignment::V(VAlignment::Bottom) => Ok(Self::Bottom), + v => bail!("expected `top` or `bottom`, found {}", v.repr()), + } + } +} + +cast! { + OuterVAlignment, + self => VAlignment::from(self).into_value(), + align: Alignment => Self::try_from(align)?, +} + +/// An internal representation that combines horizontal or vertical alignments. The +/// allowed alignment positions are designated by the type parameter `H` and `V`. +/// +/// This is not user-visible, but an internal type to impose type safety. For example, +/// `SpecificAlignment<HAlignment, OuterVAlignment>` does not allow vertical alignment +/// position "center", because `V = OuterVAlignment` doesn't have it. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum SpecificAlignment<H, V> { + H(H), + V(V), + Both(H, V), +} + +impl<H, V> SpecificAlignment<H, V> +where + H: Default + Copy + FixAlignment, + V: Default + Copy + FixAlignment, +{ + /// The horizontal component. + pub const fn x(self) -> Option<H> { + match self { + Self::H(h) | Self::Both(h, _) => Some(h), + Self::V(_) => None, + } + } + + /// The vertical component. + pub const fn y(self) -> Option<V> { + match self { + Self::V(v) | Self::Both(_, v) => Some(v), + Self::H(_) => None, + } + } + + /// Normalize the alignment to a LTR-TTB space. + pub fn fix(self, text_dir: Dir) -> Axes<FixedAlignment> { + Axes::new( + self.x().unwrap_or_default().fix(text_dir), + self.y().unwrap_or_default().fix(text_dir), + ) + } +} + +impl<H, V> Resolve for SpecificAlignment<H, V> +where + H: Default + Copy + FixAlignment, + V: Default + Copy + FixAlignment, +{ + type Output = Axes<FixedAlignment>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.fix(TextElem::dir_in(styles)) + } +} + +impl<H, V> From<SpecificAlignment<H, V>> for Alignment +where + HAlignment: From<H>, + VAlignment: From<V>, +{ + fn from(value: SpecificAlignment<H, V>) -> Self { + type FromType<H, V> = SpecificAlignment<H, V>; + match value { + FromType::H(h) => Self::H(HAlignment::from(h)), + FromType::V(v) => Self::V(VAlignment::from(v)), + FromType::Both(h, v) => Self::Both(HAlignment::from(h), VAlignment::from(v)), + } + } +} + +impl<H, V> Reflect for SpecificAlignment<H, V> +where + H: Reflect, + V: Reflect, +{ + fn input() -> CastInfo { + Alignment::input() + } + + fn output() -> CastInfo { + Alignment::output() + } + + fn castable(value: &Value) -> bool { + H::castable(value) || V::castable(value) + } +} + +impl<H, V> IntoValue for SpecificAlignment<H, V> +where + HAlignment: From<H>, + VAlignment: From<V>, +{ + fn into_value(self) -> Value { + Alignment::from(self).into_value() + } +} + +impl<H, V> FromValue for SpecificAlignment<H, V> +where + H: Reflect + TryFrom<Alignment, Error = EcoString>, + V: Reflect + TryFrom<Alignment, Error = EcoString>, +{ + fn from_value(value: Value) -> HintedStrResult<Self> { + if Alignment::castable(&value) { + let align = Alignment::from_value(value)?; + let result = match align { + Alignment::H(_) => Self::H(H::try_from(align)?), + Alignment::V(_) => Self::V(V::try_from(align)?), + Alignment::Both(h, v) => { + Self::Both(H::try_from(h.into())?, V::try_from(v.into())?) + } + }; + return Ok(result); + } + Err(Self::error(&value)) + } +} + +/// A fixed alignment in the global coordinate space. +/// +/// For horizontal alignment, start is globally left and for vertical alignment +/// it is globally top. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum FixedAlignment { + Start, + Center, + End, +} + +impl FixedAlignment { + /// Returns the position of this alignment in a container with the given + /// extent. + pub fn position(self, extent: Abs) -> Abs { + match self { + Self::Start => Abs::zero(), + Self::Center => extent / 2.0, + Self::End => extent, + } + } + + /// The inverse alignment. + pub const fn inv(self) -> Self { + match self { + Self::Start => Self::End, + Self::Center => Self::Center, + Self::End => Self::Start, + } + } +} + +impl From<Side> for FixedAlignment { + fn from(side: Side) -> Self { + match side { + Side::Left => Self::Start, + Side::Top => Self::Start, + Side::Right => Self::End, + Side::Bottom => Self::End, + } + } +} diff --git a/crates/typst-library/src/layout/angle.rs b/crates/typst-library/src/layout/angle.rs new file mode 100644 index 00000000..d1410786 --- /dev/null +++ b/crates/typst-library/src/layout/angle.rs @@ -0,0 +1,244 @@ +use std::f64::consts::PI; +use std::fmt::{self, Debug, Formatter}; +use std::iter::Sum; +use std::ops::{Add, Div, Mul, Neg}; + +use ecow::EcoString; +use typst_utils::{Numeric, Scalar}; + +use crate::foundations::{func, repr, scope, ty, Repr}; + +/// An angle describing a rotation. +/// +/// Typst supports the following angular units: +/// +/// - Degrees: `{180deg}` +/// - Radians: `{3.14rad}` +/// +/// # Example +/// ```example +/// #rotate(10deg)[Hello there!] +/// ``` +#[ty(scope, cast)] +#[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::ZERO) + } + + /// Create an angle from a number of raw units. + pub const fn raw(raw: f64) -> Self { + Self(Scalar::new(raw)) + } + + /// Create an angle from a value in a unit. + pub fn with_unit(val: f64, unit: AngleUnit) -> Self { + Self(Scalar::new(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).get() + } + + /// Get the value of this angle in a unit. + pub fn to_unit(self, unit: AngleUnit) -> f64 { + self.to_raw() / unit.raw_scale() + } + + /// 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() + } + + /// Get the quadrant of the Cartesian plane that this angle lies in. + /// + /// The angle is automatically normalized to the range `0deg..=360deg`. + /// + /// The quadrants are defined as follows: + /// - First: `0deg..=90deg` (top-right) + /// - Second: `90deg..=180deg` (top-left) + /// - Third: `180deg..=270deg` (bottom-left) + /// - Fourth: `270deg..=360deg` (bottom-right) + pub fn quadrant(self) -> Quadrant { + let angle = self.to_deg().rem_euclid(360.0); + if angle <= 90.0 { + Quadrant::First + } else if angle <= 180.0 { + Quadrant::Second + } else if angle <= 270.0 { + Quadrant::Third + } else { + Quadrant::Fourth + } + } +} + +#[scope] +impl Angle { + /// Converts this angle to radians. + #[func(name = "rad", title = "Radians")] + pub fn to_rad(self) -> f64 { + self.to_unit(AngleUnit::Rad) + } + + /// Converts this angle to degrees. + #[func(name = "deg", title = "Degrees")] + pub fn to_deg(self) -> f64 { + self.to_unit(AngleUnit::Deg) + } +} + +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", self.to_deg()) + } +} + +impl Repr for Angle { + fn repr(&self) -> EcoString { + repr::format_float_with_unit(self.to_deg(), "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) + } +} + +typst_utils::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 for Angle { + type Output = f64; + + fn div(self, other: Self) -> f64 { + self.to_raw() / other.to_raw() + } +} + +impl Div<f64> for Angle { + type Output = Self; + + fn div(self, other: f64) -> Self { + Self(self.0 / other) + } +} + +typst_utils::assign_impl!(Angle += Angle); +typst_utils::assign_impl!(Angle -= Angle); +typst_utils::assign_impl!(Angle *= f64); +typst_utils::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(Debug, 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, + } + } +} + +/// A quadrant of the Cartesian plane. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum Quadrant { + /// The first quadrant, containing positive x and y values. + First, + /// The second quadrant, containing negative x and positive y values. + Second, + /// The third quadrant, containing negative x and y values. + Third, + /// The fourth quadrant, containing positive x and negative y values. + Fourth, +} + +#[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/crates/typst-library/src/layout/axes.rs b/crates/typst-library/src/layout/axes.rs new file mode 100644 index 00000000..7a73ba79 --- /dev/null +++ b/crates/typst-library/src/layout/axes.rs @@ -0,0 +1,320 @@ +use std::any::Any; +use std::fmt::{self, Debug, Formatter}; +use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref, Not}; + +use typst_utils::Get; + +use crate::diag::bail; +use crate::foundations::{array, cast, Array, Resolve, Smart, StyleChain}; +use crate::layout::{Abs, Dir, Length, Ratio, Rel, Size}; + +/// 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) } + } + + /// Apply a function to this and another-instance componentwise. + pub fn zip_map<F, V, U>(self, other: Axes<V>, mut f: F) -> Axes<U> + where + F: FnMut(T, V) -> U, + { + Axes { x: f(self.x, other.x), y: f(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) + } +} + +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 Axes<Rel<Abs>> { + /// Evaluate the axes relative to the given `size`. + pub fn relative_to(&self, size: Size) -> Size { + Size { + x: self.x.relative_to(size.x), + y: self.y.relative_to(size.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 (&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(Debug, 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, + } + } +} + +cast! { + Axis, + self => match self { + Self::X => "horizontal".into_value(), + Self::Y => "vertical".into_value(), + }, + "horizontal" => Self::X, + "vertical" => Self::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"), + } + }, +} + +cast! { + Axes<Ratio>, + 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!("ratio array must contain exactly two entries"), + } + }, +} + +cast! { + Axes<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!("length 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)) + } +} diff --git a/crates/typst-library/src/layout/columns.rs b/crates/typst-library/src/layout/columns.rs new file mode 100644 index 00000000..f2f36441 --- /dev/null +++ b/crates/typst-library/src/layout/columns.rs @@ -0,0 +1,98 @@ +use std::num::NonZeroUsize; + +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain}; +use crate::layout::{BlockElem, Length, Ratio, Rel}; + +/// Separates a region into multiple equally sized columns. +/// +/// The `column` function lets you separate the interior of any container into +/// multiple columns. It will currently not balance the height of the columns. +/// Instead, the columns will take up the height of their container or the +/// remaining height on the page. Support for balanced columns is planned for +/// the future. +/// +/// # Page-level columns { #page-level } +/// If you need to insert columns across your whole document, use the `{page}` +/// function's [`columns` parameter]($page.columns) instead. This will create +/// the columns directly at the page-level rather than wrapping all of your +/// content in a layout container. As a result, things like +/// [pagebreaks]($pagebreak), [footnotes]($footnote), and [line +/// numbers]($par.line) will continue to work as expected. For more information, +/// also read the [relevant part of the page setup +/// guide]($guides/page-setup-guide/#columns). +/// +/// # Breaking out of columns { #breaking-out } +/// To temporarily break out of columns (e.g. for a paper's title), use +/// parent-scoped floating placement: +/// +/// ```example:single +/// #set page(columns: 2, height: 150pt) +/// +/// #place( +/// top + center, +/// scope: "parent", +/// float: true, +/// text(1.4em, weight: "bold")[ +/// My document +/// ], +/// ) +/// +/// #lorem(40) +/// ``` +#[elem(Show)] +pub struct ColumnsElem { + /// The number of columns. + #[positional] + #[default(NonZeroUsize::new(2).unwrap())] + pub count: NonZeroUsize, + + /// The size of the gutter space between each column. + #[resolve] + #[default(Ratio::new(0.04).into())] + pub gutter: Rel<Length>, + + /// The content that should be layouted into the columns. + #[required] + pub body: Content, +} + +impl Show for Packed<ColumnsElem> { + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_columns) + .pack() + .spanned(self.span())) + } +} + +/// Forces a column break. +/// +/// The function will behave like a [page break]($pagebreak) when used in a +/// single column layout or the last column on a page. Otherwise, content after +/// the column break will be placed in the next column. +/// +/// # Example +/// ```example +/// #set page(columns: 2) +/// Preliminary findings from our +/// ongoing research project have +/// revealed a hitherto unknown +/// phenomenon of extraordinary +/// significance. +/// +/// #colbreak() +/// Through rigorous experimentation +/// and analysis, we have discovered +/// a hitherto uncharacterized process +/// that defies our current +/// understanding of the fundamental +/// laws of nature. +/// ``` +#[elem(title = "Column Break")] +pub struct ColbreakElem { + /// If `{true}`, the column break is skipped if the current column is + /// already empty. + #[default(false)] + pub weak: bool, +} diff --git a/crates/typst-library/src/layout/container.rs b/crates/typst-library/src/layout/container.rs new file mode 100644 index 00000000..266d1d88 --- /dev/null +++ b/crates/typst-library/src/layout/container.rs @@ -0,0 +1,563 @@ +use crate::diag::{bail, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, Args, AutoValue, Construct, Content, NativeElement, Packed, Smart, + StyleChain, Value, +}; +use crate::introspection::Locator; +use crate::layout::{ + Abs, Corners, Em, Fr, Fragment, Frame, Length, Region, Regions, Rel, Sides, Size, + Spacing, +}; +use crate::visualize::{Paint, Stroke}; + +/// An inline-level container that sizes content. +/// +/// All elements except inline math, text, and boxes are block-level and cannot +/// occur inside of a paragraph. The box function can be used to integrate such +/// elements into a paragraph. Boxes take the size of their contents by default +/// but can also be sized explicitly. +/// +/// # Example +/// ```example +/// Refer to the docs +/// #box( +/// height: 9pt, +/// image("docs.svg") +/// ) +/// for more information. +/// ``` +#[elem] +pub struct BoxElem { + /// The width of the box. + /// + /// Boxes can have [fractional]($fraction) widths, as the example below + /// demonstrates. + /// + /// _Note:_ Currently, only boxes and only their widths might be fractionally + /// sized within paragraphs. Support for fractionally sized images, shapes, + /// and more might be added in the future. + /// + /// ```example + /// Line in #box(width: 1fr, line(length: 100%)) between. + /// ``` + pub width: Sizing, + + /// The height of the box. + pub height: Smart<Rel<Length>>, + + /// An amount to shift the box's baseline by. + /// + /// ```example + /// Image: #box(baseline: 40%, image("tiger.jpg", width: 2cm)). + /// ``` + #[resolve] + pub baseline: Rel<Length>, + + /// The box's background color. See the + /// [rectangle's documentation]($rect.fill) for more details. + pub fill: Option<Paint>, + + /// The box's border color. See the + /// [rectangle's documentation]($rect.stroke) for more details. + #[resolve] + #[fold] + pub stroke: Sides<Option<Option<Stroke>>>, + + /// How much to round the box's corners. See the + /// [rectangle's documentation]($rect.radius) for more details. + #[resolve] + #[fold] + pub radius: Corners<Option<Rel<Length>>>, + + /// How much to pad the box's content. + /// + /// _Note:_ When the box contains text, its exact size depends on the + /// current [text edges]($text.top-edge). + /// + /// ```example + /// #rect(inset: 0pt)[Tight] + /// ``` + #[resolve] + #[fold] + pub inset: Sides<Option<Rel<Length>>>, + + /// How much to expand the box's size without affecting the layout. + /// + /// This is useful to prevent padding from affecting line layout. For a + /// generalized version of the example below, see the documentation for the + /// [raw text's block parameter]($raw.block). + /// + /// ```example + /// An inline + /// #box( + /// fill: luma(235), + /// inset: (x: 3pt, y: 0pt), + /// outset: (y: 3pt), + /// radius: 2pt, + /// )[rectangle]. + /// ``` + #[resolve] + #[fold] + pub outset: Sides<Option<Rel<Length>>>, + + /// Whether to clip the content inside the box. + /// + /// Clipping is useful when the box's content is larger than the box itself, + /// as any content that exceeds the box's bounds will be hidden. + /// + /// ```example + /// #box( + /// width: 50pt, + /// height: 50pt, + /// clip: true, + /// image("tiger.jpg", width: 100pt, height: 100pt) + /// ) + /// ``` + #[default(false)] + pub clip: bool, + + /// The contents of the box. + #[positional] + #[borrowed] + pub body: Option<Content>, +} + +/// An inline-level container that can produce arbitrary items that can break +/// across lines. +#[elem(Construct)] +pub struct InlineElem { + /// A callback that is invoked with the regions to produce arbitrary + /// inline items. + #[required] + #[internal] + body: callbacks::InlineCallback, +} + +impl Construct for InlineElem { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> { + bail!(args.span, "cannot be constructed manually"); + } +} + +impl InlineElem { + /// Create an inline-level item with a custom layouter. + #[allow(clippy::type_complexity)] + pub fn layouter<T: NativeElement>( + captured: Packed<T>, + callback: fn( + content: &Packed<T>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Size, + ) -> SourceResult<Vec<InlineItem>>, + ) -> Self { + Self::new(callbacks::InlineCallback::new(captured, callback)) + } +} + +impl Packed<InlineElem> { + /// Layout the element. + pub fn layout( + &self, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Size, + ) -> SourceResult<Vec<InlineItem>> { + self.body().call(engine, locator, styles, region) + } +} + +/// Layouted items suitable for placing in a paragraph. +#[derive(Debug, Clone)] +pub enum InlineItem { + /// Absolute spacing between other items, and whether it is weak. + Space(Abs, bool), + /// Layouted inline-level content. + Frame(Frame), +} + +/// A block-level container. +/// +/// Such a container can be used to separate content, size it, and give it a +/// background or border. +/// +/// # Examples +/// With a block, you can give a background to content while still allowing it +/// to break across multiple pages. +/// ```example +/// #set page(height: 100pt) +/// #block( +/// fill: luma(230), +/// inset: 8pt, +/// radius: 4pt, +/// lorem(30), +/// ) +/// ``` +/// +/// Blocks are also useful to force elements that would otherwise be inline to +/// become block-level, especially when writing show rules. +/// ```example +/// #show heading: it => it.body +/// = Blockless +/// More text. +/// +/// #show heading: it => block(it.body) +/// = Blocky +/// More text. +/// ``` +#[elem] +pub struct BlockElem { + /// The block's width. + /// + /// ```example + /// #set align(center) + /// #block( + /// width: 60%, + /// inset: 8pt, + /// fill: silver, + /// lorem(10), + /// ) + /// ``` + pub width: Smart<Rel<Length>>, + + /// The block's height. When the height is larger than the remaining space + /// on a page and [`breakable`]($block.breakable) is `{true}`, the + /// block will continue on the next page with the remaining height. + /// + /// ```example + /// #set page(height: 80pt) + /// #set align(center) + /// #block( + /// width: 80%, + /// height: 150%, + /// fill: aqua, + /// ) + /// ``` + pub height: Sizing, + + /// Whether the block can be broken and continue on the next page. + /// + /// ```example + /// #set page(height: 80pt) + /// The following block will + /// jump to its own page. + /// #block( + /// breakable: false, + /// lorem(15), + /// ) + /// ``` + #[default(true)] + pub breakable: bool, + + /// The block's background color. See the + /// [rectangle's documentation]($rect.fill) for more details. + pub fill: Option<Paint>, + + /// The block's border color. See the + /// [rectangle's documentation]($rect.stroke) for more details. + #[resolve] + #[fold] + pub stroke: Sides<Option<Option<Stroke>>>, + + /// How much to round the block's corners. See the + /// [rectangle's documentation]($rect.radius) for more details. + #[resolve] + #[fold] + pub radius: Corners<Option<Rel<Length>>>, + + /// How much to pad the block's content. See the + /// [box's documentation]($box.inset) for more details. + #[resolve] + #[fold] + pub inset: Sides<Option<Rel<Length>>>, + + /// How much to expand the block's size without affecting the layout. See + /// the [box's documentation]($box.outset) for more details. + #[resolve] + #[fold] + pub outset: Sides<Option<Rel<Length>>>, + + /// The spacing around the block. When `{auto}`, inherits the paragraph + /// [`spacing`]($par.spacing). + /// + /// For two adjacent blocks, the larger of the first block's `above` and the + /// second block's `below` spacing wins. Moreover, block spacing takes + /// precedence over paragraph [`spacing`]($par.spacing). + /// + /// Note that this is only a shorthand to set `above` and `below` to the + /// same value. Since the values for `above` and `below` might differ, a + /// [context] block only provides access to `{block.above}` and + /// `{block.below}`, not to `{block.spacing}` directly. + /// + /// This property can be used in combination with a show rule to adjust the + /// spacing around arbitrary block-level elements. + /// + /// ```example + /// #set align(center) + /// #show math.equation: set block(above: 8pt, below: 16pt) + /// + /// This sum of $x$ and $y$: + /// $ x + y = z $ + /// A second paragraph. + /// ``` + #[external] + #[default(Em::new(1.2).into())] + pub spacing: Spacing, + + /// The spacing between this block and its predecessor. + #[parse( + let spacing = args.named("spacing")?; + args.named("above")?.or(spacing) + )] + pub above: Smart<Spacing>, + + /// The spacing between this block and its successor. + #[parse(args.named("below")?.or(spacing))] + pub below: Smart<Spacing>, + + /// Whether to clip the content inside the block. + /// + /// Clipping is useful when the block's content is larger than the block itself, + /// as any content that exceeds the block's bounds will be hidden. + /// + /// ```example + /// #block( + /// width: 50pt, + /// height: 50pt, + /// clip: true, + /// image("tiger.jpg", width: 100pt, height: 100pt) + /// ) + /// ``` + #[default(false)] + pub clip: bool, + + /// Whether this block must stick to the following one, with no break in + /// between. + /// + /// This is, by default, set on heading blocks to prevent orphaned headings + /// at the bottom of the page. + /// + /// ```example + /// >>> #set page(height: 140pt) + /// // Disable stickiness of headings. + /// #show heading: set block(sticky: false) + /// #lorem(20) + /// + /// = Chapter + /// #lorem(10) + /// ``` + #[default(false)] + pub sticky: bool, + + /// The contents of the block. + #[positional] + #[borrowed] + pub body: Option<BlockBody>, +} + +impl BlockElem { + /// Create a block with a custom single-region layouter. + /// + /// Such a block must have `breakable: false` (which is set by this + /// constructor). + pub fn single_layouter<T: NativeElement>( + captured: Packed<T>, + f: fn( + content: &Packed<T>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame>, + ) -> Self { + Self::new() + .with_breakable(false) + .with_body(Some(BlockBody::SingleLayouter( + callbacks::BlockSingleCallback::new(captured, f), + ))) + } + + /// Create a block with a custom multi-region layouter. + pub fn multi_layouter<T: NativeElement>( + captured: Packed<T>, + f: fn( + content: &Packed<T>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment>, + ) -> Self { + Self::new().with_body(Some(BlockBody::MultiLayouter( + callbacks::BlockMultiCallback::new(captured, f), + ))) + } +} + +/// The contents of a block. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum BlockBody { + /// The block contains normal content. + Content(Content), + /// The block contains a layout callback that needs access to just one + /// base region. + SingleLayouter(callbacks::BlockSingleCallback), + /// The block contains a layout callback that needs access to the exact + /// regions. + MultiLayouter(callbacks::BlockMultiCallback), +} + +impl Default for BlockBody { + fn default() -> Self { + Self::Content(Content::default()) + } +} + +cast! { + BlockBody, + self => match self { + Self::Content(content) => content.into_value(), + _ => Value::Auto, + }, + v: Content => Self::Content(v), +} + +/// Defines how to size something along an axis. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Sizing { + /// A track that fits its item's contents. + Auto, + /// A size specified in absolute terms and relative to the parent's size. + Rel(Rel), + /// A size specified as a fraction of the remaining free space in the + /// parent. + Fr(Fr), +} + +impl Sizing { + /// Whether this is an automatic sizing. + pub fn is_auto(self) -> bool { + matches!(self, Self::Auto) + } + + /// Whether this is fractional sizing. + pub fn is_fractional(self) -> bool { + matches!(self, Self::Fr(_)) + } +} + +impl Default for Sizing { + fn default() -> Self { + Self::Auto + } +} + +impl From<Smart<Rel>> for Sizing { + fn from(smart: Smart<Rel>) -> Self { + match smart { + Smart::Auto => Self::Auto, + Smart::Custom(rel) => Self::Rel(rel), + } + } +} + +impl<T: Into<Spacing>> From<T> for Sizing { + fn from(spacing: T) -> Self { + match spacing.into() { + Spacing::Rel(rel) => Self::Rel(rel), + Spacing::Fr(fr) => Self::Fr(fr), + } + } +} + +cast! { + Sizing, + self => match self { + Self::Auto => Value::Auto, + Self::Rel(rel) => rel.into_value(), + Self::Fr(fr) => fr.into_value(), + }, + _: AutoValue => Self::Auto, + v: Rel<Length> => Self::Rel(v), + v: Fr => Self::Fr(v), +} + +/// Manual closure implementations for layout callbacks. +/// +/// Normal closures are not `Hash`, so we can't use them. +mod callbacks { + use super::*; + + macro_rules! callback { + ($name:ident = ($($param:ident: $param_ty:ty),* $(,)?) -> $ret:ty) => { + #[derive(Debug, Clone, PartialEq, Hash)] + pub struct $name { + captured: Content, + f: fn(&Content, $($param_ty),*) -> $ret, + } + + impl $name { + pub fn new<T: NativeElement>( + captured: Packed<T>, + f: fn(&Packed<T>, $($param_ty),*) -> $ret, + ) -> Self { + Self { + // Type-erased the content. + captured: captured.pack(), + // Safety: The only difference between the two function + // pointer types is the type of the first parameter, + // which changes from `&Packed<T>` to `&Content`. This + // is safe because: + // - `Packed<T>` is a transparent wrapper around + // `Content`, so for any `T` it has the same memory + // representation as `Content`. + // - While `Packed<T>` imposes the additional constraint + // that the content is of type `T`, this constraint is + // upheld: It is initially the case because we store a + // `Packed<T>` above. It keeps being the case over the + // lifetime of the closure because `capture` is a + // private field and `Content`'s `Clone` impl is + // guaranteed to retain the type (if it didn't, + // literally everything would break). + #[allow(clippy::missing_transmute_annotations)] + f: unsafe { std::mem::transmute(f) }, + } + } + + pub fn call(&self, $($param: $param_ty),*) -> $ret { + (self.f)(&self.captured, $($param),*) + } + } + }; + } + + callback! { + InlineCallback = ( + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Size, + ) -> SourceResult<Vec<InlineItem>> + } + + callback! { + BlockSingleCallback = ( + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + } + + callback! { + BlockMultiCallback = ( + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> + } +} diff --git a/crates/typst-library/src/layout/corners.rs b/crates/typst-library/src/layout/corners.rs new file mode 100644 index 00000000..fe69126a --- /dev/null +++ b/crates/typst-library/src/layout/corners.rs @@ -0,0 +1,315 @@ +use std::fmt::{self, Debug, Formatter}; + +use typst_utils::Get; + +use crate::diag::HintedStrResult; +use crate::foundations::{ + AlternativeFold, CastInfo, Dict, Fold, FromValue, IntoValue, Reflect, Resolve, + StyleChain, Value, +}; +use crate::layout::Side; + +/// A container with components for the four corners of a rectangle. +#[derive(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> Corners<Option<T>> { + /// Unwrap-or-default the individual corners. + pub fn unwrap_or_default(self) -> Corners<T> + where + T: Default, + { + self.map(Option::unwrap_or_default) + } +} + +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, + } + } +} + +impl<T: Debug + PartialEq> Debug for Corners<T> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if self.is_uniform() { + f.write_str("Corners::splat(")?; + self.top_left.fmt(f)?; + f.write_str(")") + } else { + f.debug_struct("Corners") + .field("top_left", &self.top_left) + .field("top_right", &self.top_right) + .field("bottom_right", &self.bottom_right) + .field("bottom_left", &self.bottom_left) + .finish() + } + } +} + +impl<T: Reflect> Reflect for Corners<Option<T>> { + fn input() -> CastInfo { + T::input() + Dict::input() + } + + fn output() -> CastInfo { + T::output() + Dict::output() + } + + fn castable(value: &Value) -> bool { + Dict::castable(value) || T::castable(value) + } +} + +impl<T> IntoValue for Corners<Option<T>> +where + T: PartialEq + IntoValue, +{ + fn into_value(self) -> Value { + if self.is_uniform() { + if let Some(top_left) = self.top_left { + return top_left.into_value(); + } + } + + let mut dict = Dict::new(); + let mut handle = |key: &str, component: Option<T>| { + if let Some(c) = component { + dict.insert(key.into(), c.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) -> HintedStrResult<Self> { + let expected_keys = [ + "top-left", + "top-right", + "bottom-right", + "bottom-left", + "left", + "top", + "right", + "bottom", + "rest", + ]; + + if let Value::Dict(dict) = &mut value { + if dict.is_empty() { + return Ok(Self::splat(None)); + } else if dict.iter().any(|(key, _)| expected_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(&expected_keys)?; + return Ok(corners); + } + } + + if T::castable(&value) { + Ok(Self::splat(Some(T::from_value(value)?))) + } else if let Value::Dict(dict) = &value { + let keys = dict.iter().map(|kv| kv.0.as_str()).collect(); + // Do not hint at expected_keys, because T may be castable from Dict + // objects with other sets of expected keys. + Err(Dict::unexpected_keys(keys, None).into()) + } 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>> { + fn fold(self, outer: Self) -> Self { + // Usually, folding an inner `None` with an `outer` prefers the + // explicit `None`. However, here `None` means unspecified and thus + // we want `outer`, so we use `fold_or` to opt into such behavior. + self.zip(outer).map(|(inner, outer)| inner.fold_or(outer)) + } +} + +/// 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 Corner { + /// The opposite corner. + pub fn inv(self) -> Self { + match self { + Self::TopLeft => Self::BottomRight, + Self::TopRight => Self::BottomLeft, + Self::BottomRight => Self::TopLeft, + Self::BottomLeft => Self::TopRight, + } + } + + /// The next corner, clockwise. + pub fn next_cw(self) -> Self { + match self { + Self::TopLeft => Self::TopRight, + Self::TopRight => Self::BottomRight, + Self::BottomRight => Self::BottomLeft, + Self::BottomLeft => Self::TopLeft, + } + } + + /// The next corner, counter-clockwise. + pub fn next_ccw(self) -> Self { + match self { + Self::TopLeft => Self::BottomLeft, + Self::TopRight => Self::TopLeft, + Self::BottomRight => Self::TopRight, + Self::BottomLeft => Self::BottomRight, + } + } + + /// The next side, clockwise. + pub fn side_cw(self) -> Side { + match self { + Self::TopLeft => Side::Top, + Self::TopRight => Side::Right, + Self::BottomRight => Side::Bottom, + Self::BottomLeft => Side::Left, + } + } + + /// The next side, counter-clockwise. + pub fn side_ccw(self) -> Side { + match self { + Self::TopLeft => Side::Left, + Self::TopRight => Side::Top, + Self::BottomRight => Side::Right, + Self::BottomLeft => Side::Bottom, + } + } +} diff --git a/crates/typst-library/src/layout/dir.rs b/crates/typst-library/src/layout/dir.rs new file mode 100644 index 00000000..9a2e7710 --- /dev/null +++ b/crates/typst-library/src/layout/dir.rs @@ -0,0 +1,132 @@ +use ecow::EcoString; + +use crate::foundations::{func, scope, ty, Repr}; +use crate::layout::{Axis, Side}; + +/// The four directions into which content can be laid out. +/// +/// Possible values are: +/// - `{ltr}`: Left to right. +/// - `{rtl}`: Right to left. +/// - `{ttb}`: Top to bottom. +/// - `{btt}`: Bottom to top. +/// +/// These values are available globally and +/// also in the direction type's scope, so you can write either of the following +/// two: +/// ```example +/// #stack(dir: rtl)[A][B][C] +/// #stack(dir: direction.rtl)[A][B][C] +/// ``` +#[ty(scope, name = "direction")] +#[derive(Debug, 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 { + /// 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, + } + } +} + +#[scope] +impl Dir { + pub const LTR: Self = Self::LTR; + pub const RTL: Self = Self::RTL; + pub const TTB: Self = Self::TTB; + pub const BTT: Self = Self::BTT; + + /// The axis this direction belongs to, either `{"horizontal"}` or + /// `{"vertical"}`. + /// + /// ```example + /// #ltr.axis() \ + /// #ttb.axis() + /// ``` + #[func] + pub const fn axis(self) -> Axis { + match self { + Self::LTR | Self::RTL => Axis::X, + Self::TTB | Self::BTT => Axis::Y, + } + } + + /// The start point of this direction, as an alignment. + /// + /// ```example + /// #ltr.start() \ + /// #rtl.start() \ + /// #ttb.start() \ + /// #btt.start() + /// ``` + #[func] + 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 end point of this direction, as an alignment. + /// + /// ```example + /// #ltr.end() \ + /// #rtl.end() \ + /// #ttb.end() \ + /// #btt.end() + /// ``` + #[func] + 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. + /// + /// ```example + /// #ltr.inv() \ + /// #rtl.inv() \ + /// #ttb.inv() \ + /// #btt.inv() + /// ``` + #[func(title = "Inverse")] + pub const fn inv(self) -> Dir { + match self { + Self::LTR => Self::RTL, + Self::RTL => Self::LTR, + Self::TTB => Self::BTT, + Self::BTT => Self::TTB, + } + } +} + +impl Repr for Dir { + fn repr(&self) -> EcoString { + match self { + Self::LTR => "ltr".into(), + Self::RTL => "rtl".into(), + Self::TTB => "ttb".into(), + Self::BTT => "btt".into(), + } + } +} diff --git a/crates/typst-library/src/layout/em.rs b/crates/typst-library/src/layout/em.rs new file mode 100644 index 00000000..e2d8b120 --- /dev/null +++ b/crates/typst-library/src/layout/em.rs @@ -0,0 +1,168 @@ +use std::fmt::{self, Debug, Formatter}; +use std::iter::Sum; +use std::ops::{Add, Div, Mul, Neg}; + +use ecow::EcoString; +use typst_utils::{Numeric, Scalar}; + +use crate::foundations::{cast, repr, Repr, Resolve, StyleChain, Value}; +use crate::layout::Abs; +use crate::text::TextElem; + +/// 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::ZERO) + } + + /// The font size. + pub const fn one() -> Self { + Self(Scalar::ONE) + } + + /// Create a font-relative length. + pub const fn new(em: f64) -> Self { + Self(Scalar::new(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::new(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::new(result)) + } else { + Self::zero() + } + } + + /// The number of em units. + pub const fn get(self) -> f64 { + (self.0).get() + } + + /// 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 Repr for Em { + fn repr(&self) -> EcoString { + repr::format_float_with_unit(self.get(), "em") + } +} + +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) + } +} + +typst_utils::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() + } +} + +typst_utils::assign_impl!(Em += Em); +typst_utils::assign_impl!(Em -= Em); +typst_utils::assign_impl!(Em *= f64); +typst_utils::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(TextElem::size_in(styles)) + } + } +} diff --git a/crates/typst-library/src/layout/fr.rs b/crates/typst-library/src/layout/fr.rs new file mode 100644 index 00000000..ff8a6e6b --- /dev/null +++ b/crates/typst-library/src/layout/fr.rs @@ -0,0 +1,145 @@ +use std::fmt::{self, Debug, Formatter}; +use std::iter::Sum; +use std::ops::{Add, Div, Mul, Neg}; + +use ecow::EcoString; +use typst_utils::{Numeric, Scalar}; + +use crate::foundations::{repr, ty, Repr}; +use crate::layout::Abs; + +/// Defines how the remaining space in a layout is distributed. +/// +/// Each fractionally sized element gets space based on the ratio of its +/// fraction to the sum of all fractions. +/// +/// For more details, also see the [h] and [v] functions and the +/// [grid function]($grid). +/// +/// # Example +/// ```example +/// Left #h(1fr) Left-ish #h(2fr) Right +/// ``` +#[ty(cast, name = "fraction")] +#[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::ZERO) + } + + /// Takes up as much space as all other items with this fraction: `1fr`. + pub const fn one() -> Self { + Self(Scalar::ONE) + } + + /// Create a new fraction. + pub const fn new(ratio: f64) -> Self { + Self(Scalar::new(ratio)) + } + + /// Get the underlying number. + pub const fn get(self) -> f64 { + (self.0).get() + } + + /// 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", self.get()) + } +} + +impl Repr for Fr { + fn repr(&self) -> EcoString { + repr::format_float_with_unit(self.get(), "fr") + } +} + +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) + } +} + +typst_utils::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 for Fr { + type Output = f64; + + fn div(self, other: Self) -> f64 { + self.get() / other.get() + } +} + +impl Div<f64> for Fr { + type Output = Self; + + fn div(self, other: f64) -> Self { + Self(self.0 / other) + } +} + +typst_utils::assign_impl!(Fr += Fr); +typst_utils::assign_impl!(Fr -= Fr); +typst_utils::assign_impl!(Fr *= f64); +typst_utils::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/crates/typst-library/src/layout/fragment.rs b/crates/typst-library/src/layout/fragment.rs new file mode 100644 index 00000000..c2666115 --- /dev/null +++ b/crates/typst-library/src/layout/fragment.rs @@ -0,0 +1,94 @@ +use std::fmt::{self, Debug, Formatter}; + +use crate::layout::Frame; + +/// A partial layout result. +#[derive(Clone)] +pub struct Fragment(Vec<Frame>); + +impl Fragment { + /// Create a fragment from a single frame. + pub fn frame(frame: Frame) -> Self { + Self(vec![frame]) + } + + /// Create a fragment from multiple frames. + pub fn frames(frames: Vec<Frame>) -> Self { + Self(frames) + } + + /// Return `true` if the length is 0. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// The number of frames in the fragment. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Extract the first and only frame. + /// + /// Panics if there are multiple frames. + #[track_caller] + pub fn into_frame(self) -> Frame { + assert_eq!(self.0.len(), 1, "expected exactly one frame"); + self.0.into_iter().next().unwrap() + } + + /// Extract the frames. + pub fn into_frames(self) -> Vec<Frame> { + self.0 + } + + /// Extract a slice with the contained frames. + pub fn as_slice(&self) -> &[Frame] { + &self.0 + } + + /// Iterate over the contained frames. + pub fn iter(&self) -> std::slice::Iter<Frame> { + self.0.iter() + } + + /// Iterate over the contained frames. + pub fn iter_mut(&mut self) -> std::slice::IterMut<Frame> { + self.0.iter_mut() + } +} + +impl Debug for Fragment { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self.0.as_slice() { + [frame] => frame.fmt(f), + frames => frames.fmt(f), + } + } +} + +impl IntoIterator for Fragment { + type Item = Frame; + type IntoIter = std::vec::IntoIter<Frame>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'a> IntoIterator for &'a Fragment { + type Item = &'a Frame; + type IntoIter = std::slice::Iter<'a, Frame>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl<'a> IntoIterator for &'a mut Fragment { + type Item = &'a mut Frame; + type IntoIter = std::slice::IterMut<'a, Frame>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter_mut() + } +} diff --git a/crates/typst-library/src/layout/frame.rs b/crates/typst-library/src/layout/frame.rs new file mode 100644 index 00000000..204584fc --- /dev/null +++ b/crates/typst-library/src/layout/frame.rs @@ -0,0 +1,606 @@ +//! Finished documents. + +use std::fmt::{self, Debug, Formatter}; +use std::num::NonZeroUsize; +use std::sync::Arc; + +use smallvec::SmallVec; +use typst_syntax::Span; +use typst_utils::{LazyHash, Numeric}; + +use crate::foundations::{cast, dict, Dict, Label, StyleChain, Value}; +use crate::introspection::{Location, Tag}; +use crate::layout::{ + Abs, Axes, FixedAlignment, HideElem, Length, Point, Size, Transform, +}; +use crate::model::{Destination, LinkElem}; +use crate::text::TextItem; +use crate::visualize::{Color, FixedStroke, Geometry, Image, Paint, Path, Shape}; + +/// 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<LazyHash<Vec<(Point, FrameItem)>>>, + /// The hardness of this frame. + /// + /// Determines whether it is a boundary for gradient drawing. + kind: FrameKind, +} + +/// Constructor, accessors and setters. +impl Frame { + /// Create a new, empty frame. + /// + /// Panics the size is not finite. + #[track_caller] + pub fn new(size: Size, kind: FrameKind) -> Self { + assert!(size.is_finite()); + Self { + size, + baseline: None, + items: Arc::new(LazyHash::new(vec![])), + kind, + } + } + + /// Create a new, empty soft frame. + /// + /// Panics if the size is not finite. + #[track_caller] + pub fn soft(size: Size) -> Self { + Self::new(size, FrameKind::Soft) + } + + /// Create a new, empty hard frame. + /// + /// Panics if the size is not finite. + #[track_caller] + pub fn hard(size: Size) -> Self { + Self::new(size, FrameKind::Hard) + } + + /// Sets the frame's hardness. + pub fn set_kind(&mut self, kind: FrameKind) { + self.kind = kind; + } + + /// Sets the frame's hardness builder-style. + pub fn with_kind(mut self, kind: FrameKind) -> Self { + self.kind = kind; + self + } + + /// Whether the frame is hard or soft. + pub fn kind(&self) -> FrameKind { + self.kind + } + + /// 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 multiple items at a position in the foreground. + /// + /// The first item in the iterator will be the one that is most in the + /// background. + pub fn push_multiple<I>(&mut self, items: I) + where + I: IntoIterator<Item = (Point, FrameItem)>, + { + Arc::make_mut(&mut self.items).extend(items); + } + + /// 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, item: FrameItem) { + Arc::make_mut(&mut self.items).insert(layer, (pos, item)); + } + + /// Add an item at a position in the background. + pub fn prepend(&mut self, pos: Point, item: FrameItem) { + self.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 { + // We do not inline big frames and hard frames. + frame.kind().is_soft() && (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) { + // Skip work if there's nothing to do. + if frame.items.is_empty() { + return; + } + + // 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.into_inner()); + } + 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_inner().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(LazyHash::new(vec![])); + } + } + + /// Adjust the frame's size, translate the original content by an offset + /// computed according to the given alignments, and return the amount of + /// offset. + pub fn resize(&mut self, target: Size, align: Axes<FixedAlignment>) -> Point { + if self.size == target { + return Point::zero(); + } + let offset = + align.zip_map(target - self.size, FixedAlignment::position).to_point(); + self.size = target; + self.translate(offset); + 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).iter_mut() { + *point += offset; + } + } + } + + /// Apply late-stage properties from the style chain to this frame. This + /// includes: + /// - `HideElem::hidden` + /// - `LinkElem::dests` + /// + /// This must be called on all frames produced by elements + /// that manually handle styles (because their children can have varying + /// styles). This currently includes flow, par, and equation. + /// + /// Other elements don't manually need to handle it because their parents + /// that result from realization will take care of it and the styles can + /// only apply to them as a whole, not part of it (because they don't manage + /// styles). + pub fn post_processed(mut self, styles: StyleChain) -> Self { + self.post_process(styles); + self + } + + /// Post process in place. + pub fn post_process(&mut self, styles: StyleChain) { + if !self.is_empty() { + self.post_process_raw( + LinkElem::dests_in(styles), + HideElem::hidden_in(styles), + ); + } + } + + /// Apply raw late-stage properties from the raw data. + pub fn post_process_raw(&mut self, dests: SmallVec<[Destination; 1]>, hide: bool) { + if !self.is_empty() { + let size = self.size; + self.push_multiple( + dests + .into_iter() + .map(|dest| (Point::zero(), FrameItem::Link(dest, size))), + ); + if hide { + self.hide(); + } + } + } + + /// Hide all content in the frame, but keep metadata. + pub fn hide(&mut self) { + Arc::make_mut(&mut self.items).retain_mut(|(_, item)| match item { + FrameItem::Group(group) => { + group.frame.hide(); + !group.frame.is_empty() + } + FrameItem::Tag(_) => true, + _ => false, + }); + } + + /// Add a background fill. + pub fn fill(&mut self, fill: impl Into<Paint>) { + self.prepend( + Point::zero(), + FrameItem::Shape(Geometry::Rect(self.size()).filled(fill), Span::detached()), + ); + } + + /// 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 a clip path. + /// + /// The clip path can be the size of the frame in the case of a + /// rectangular frame. In the case of a frame with rounded corner, + /// this should be a path that matches the frame's outline. + pub fn clip(&mut self, clip_path: Path) { + if !self.is_empty() { + self.group(|g| g.clip_path = Some(clip_path)); + } + } + + /// Add a label to the frame. + pub fn label(&mut self, label: Label) { + self.group(|g| g.label = Some(label)); + } + + /// Set a parent for the frame. As a result, all elements in the frame + /// become logically ordered immediately after the given location. + pub fn set_parent(&mut self, parent: Location) { + if !self.is_empty() { + self.group(|g| g.parent = Some(parent)); + } + } + + /// 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::soft(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 mark_box(mut self) -> Self { + self.mark_box_in_place(); + self + } + + /// Debug in place. Add a full size aqua background and a red baseline for debugging. + pub fn mark_box_in_place(&mut self) { + self.insert( + 0, + Point::zero(), + FrameItem::Shape( + Geometry::Rect(self.size).filled(Color::TEAL.with_alpha(0.5)), + Span::detached(), + ), + ); + self.insert( + 1, + Point::with_y(self.baseline()), + FrameItem::Shape( + Geometry::Line(Point::with_x(self.size.x)) + .stroked(FixedStroke::from_pair(Color::RED, Abs::pt(1.0))), + Span::detached(), + ), + ); + } + + /// 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( + Geometry::Path(Path::ellipse(Size::splat(2.0 * radius))) + .filled(Color::GREEN), + 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(FixedStroke::from_pair(Color::GREEN, Abs::pt(1.0))), + 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 hardness of a frame. +/// +/// This corresponds to whether or not the frame is considered to be the +/// innermost parent of its contents. This is used to determine the coordinate +/// reference system for gradients. +#[derive(Default, Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub enum FrameKind { + /// A container which follows its parent's size. + /// + /// Soft frames are the default since they do not impact the layout of + /// a gradient set on one of its children. + #[default] + Soft, + /// A container which uses its own size. + /// + /// This is used for pages, blocks, and boxes. + Hard, +} + +impl FrameKind { + /// Returns `true` if the frame is soft. + pub fn is_soft(self) -> bool { + matches!(self, Self::Soft) + } + + /// Returns `true` if the frame is hard. + pub fn is_hard(self) -> bool { + matches!(self, Self::Hard) + } +} + +/// 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), + /// An internal or external link to a destination. + Link(Destination, Size), + /// An introspectable element that produced something within this frame + /// alongside its key. + Tag(Tag), +} + +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::Link(dest, _) => write!(f, "Link({dest:?})"), + Self::Tag(tag) => write!(f, "{tag:?}"), + } + } +} + +/// 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 clip_path: Option<Path>, + /// The group's label. + pub label: Option<Label>, + /// The group's logical parent. All elements in this group are logically + /// ordered immediately after the parent's start location. + pub parent: Option<Location>, +} + +impl GroupItem { + /// Create a new group with default settings. + pub fn new(frame: Frame) -> Self { + Self { + frame, + transform: Transform::identity(), + clip_path: None, + label: None, + parent: None, + } + } +} + +impl Debug for GroupItem { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Group ")?; + self.frame.fmt(f) + } +} + +/// 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, + } + } +} diff --git a/crates/typst-library/src/layout/grid.rs b/crates/typst-library/src/layout/grid.rs new file mode 100644 index 00000000..2e1e9abc --- /dev/null +++ b/crates/typst-library/src/layout/grid.rs @@ -0,0 +1,924 @@ +use std::num::NonZeroUsize; +use std::sync::Arc; + +use comemo::Track; +use smallvec::{smallvec, SmallVec}; +use typst_utils::NonZeroExt; + +use crate::diag::{bail, At, HintedStrResult, HintedString, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, scope, Array, CastInfo, Content, Context, Fold, FromValue, Func, + IntoValue, NativeElement, Packed, Reflect, Resolve, Show, Smart, StyleChain, Value, +}; +use crate::layout::{ + Alignment, BlockElem, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, Sizing, +}; +use crate::model::{TableCell, TableFooter, TableHLine, TableHeader, TableVLine}; +use crate::visualize::{Paint, Stroke}; + +/// Arranges content in a grid. +/// +/// The grid element allows you to arrange content in a grid. You can define the +/// number of rows and columns, as well as the size of the gutters between them. +/// There are multiple sizing modes for columns and rows that can be used to +/// create complex layouts. +/// +/// While the grid and table elements work very similarly, they are intended for +/// different use cases and carry different semantics. The grid element is +/// intended for presentational and layout purposes, while the +/// [`{table}`]($table) element is intended for, in broad terms, presenting +/// multiple related data points. In the future, Typst will annotate its output +/// such that screenreaders will announce content in `table` as tabular while a +/// grid's content will be announced no different than multiple content blocks +/// in the document flow. Set and show rules on one of these elements do not +/// affect the other. +/// +/// A grid's sizing is determined by the track sizes specified in the arguments. +/// Because each of the sizing parameters accepts the same values, we will +/// explain them just once, here. Each sizing argument accepts an array of +/// individual track sizes. A track size is either: +/// +/// - `{auto}`: The track will be sized to fit its contents. It will be at most +/// as large as the remaining space. If there is more than one `{auto}` track +/// width, and together they claim more than the available space, the `{auto}` +/// tracks will fairly distribute the available space among themselves. +/// +/// - A fixed or relative length (e.g. `{10pt}` or `{20% - 1cm}`): The track +/// will be exactly of this size. +/// +/// - A fractional length (e.g. `{1fr}`): Once all other tracks have been sized, +/// the remaining space will be divided among the fractional tracks according +/// to their fractions. For example, if there are two fractional tracks, each +/// with a fraction of `{1fr}`, they will each take up half of the remaining +/// space. +/// +/// To specify a single track, the array can be omitted in favor of a single +/// value. To specify multiple `{auto}` tracks, enter the number of tracks +/// instead of an array. For example, `columns:` `{3}` is equivalent to +/// `columns:` `{(auto, auto, auto)}`. +/// +/// # Examples +/// The example below demonstrates the different track sizing options. It also +/// shows how you can use [`grid.cell`]($grid.cell) to make an individual cell +/// span two grid tracks. +/// +/// ```example +/// // We use `rect` to emphasize the +/// // area of cells. +/// #set rect( +/// inset: 8pt, +/// fill: rgb("e4e5ea"), +/// width: 100%, +/// ) +/// +/// #grid( +/// columns: (60pt, 1fr, 2fr), +/// rows: (auto, 60pt), +/// gutter: 3pt, +/// rect[Fixed width, auto height], +/// rect[1/3 of the remains], +/// rect[2/3 of the remains], +/// rect(height: 100%)[Fixed height], +/// grid.cell( +/// colspan: 2, +/// image("tiger.jpg", width: 100%), +/// ), +/// ) +/// ``` +/// +/// You can also [spread]($arguments/#spreading) an array of strings or content +/// into a grid to populate its cells. +/// +/// ```example +/// #grid( +/// columns: 5, +/// gutter: 5pt, +/// ..range(25).map(str) +/// ) +/// ``` +/// +/// # Styling the grid +/// The grid's appearance can be customized through different parameters. These +/// are the most important ones: +/// +/// - [`fill`]($grid.fill) to give all cells a background +/// - [`align`]($grid.align) to change how cells are aligned +/// - [`inset`]($grid.inset) to optionally add internal padding to each cell +/// - [`stroke`]($grid.stroke) to optionally enable grid lines with a certain +/// stroke +/// +/// If you need to override one of the above options for a single cell, you can +/// use the [`grid.cell`]($grid.cell) element. Likewise, you can override +/// individual grid lines with the [`grid.hline`]($grid.hline) and +/// [`grid.vline`]($grid.vline) elements. +/// +/// Alternatively, if you need the appearance options to depend on a cell's +/// position (column and row), you may specify a function to `fill` or `align` +/// of the form `(column, row) => value`. You may also use a show rule on +/// [`grid.cell`]($grid.cell) - see that element's examples or the examples +/// below for more information. +/// +/// Locating most of your styling in set and show rules is recommended, as it +/// keeps the grid's or table's actual usages clean and easy to read. It also +/// allows you to easily change the grid's appearance in one place. +/// +/// ## Stroke styling precedence +/// There are three ways to set the stroke of a grid cell: through +/// [`{grid.cell}`'s `stroke` field]($grid.cell.stroke), by using +/// [`{grid.hline}`]($grid.hline) and [`{grid.vline}`]($grid.vline), or by +/// setting the [`{grid}`'s `stroke` field]($grid.stroke). When multiple of +/// these settings are present and conflict, the `hline` and `vline` settings +/// take the highest precedence, followed by the `cell` settings, and finally +/// the `grid` settings. +/// +/// Furthermore, strokes of a repeated grid header or footer will take +/// precedence over regular cell strokes. +#[elem(scope, Show)] +pub struct GridElem { + /// The column sizes. + /// + /// Either specify a track size array or provide an integer to create a grid + /// with that many `{auto}`-sized columns. Note that opposed to rows and + /// gutters, providing a single track size will only ever create a single + /// column. + #[borrowed] + pub columns: TrackSizings, + + /// The row sizes. + /// + /// If there are more cells than fit the defined rows, the last row is + /// repeated until there are no more cells. + #[borrowed] + pub rows: TrackSizings, + + /// The gaps between rows and columns. + /// + /// If there are more gutters than defined sizes, the last gutter is + /// repeated. + /// + /// This is a shorthand to set `column-gutter` and `row-gutter` to the same + /// value. + #[external] + pub gutter: TrackSizings, + + /// The gaps between columns. + #[parse( + let gutter = args.named("gutter")?; + args.named("column-gutter")?.or_else(|| gutter.clone()) + )] + #[borrowed] + pub column_gutter: TrackSizings, + + /// The gaps between rows. + #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))] + #[borrowed] + pub row_gutter: TrackSizings, + + /// How to fill the cells. + /// + /// This can be a color or a function that returns a color. The function + /// receives the cells' column and row indices, starting from zero. This can + /// be used to implement striped grids. + /// + /// ```example + /// #grid( + /// fill: (x, y) => + /// if calc.even(x + y) { luma(230) } + /// else { white }, + /// align: center + horizon, + /// columns: 4, + /// inset: 2pt, + /// [X], [O], [X], [O], + /// [O], [X], [O], [X], + /// [X], [O], [X], [O], + /// [O], [X], [O], [X], + /// ) + /// ``` + #[borrowed] + pub fill: Celled<Option<Paint>>, + + /// How to align the cells' content. + /// + /// This can either be a single alignment, an array of alignments + /// (corresponding to each column) or a function that returns an alignment. + /// The function receives the cells' column and row indices, starting from + /// zero. If set to `{auto}`, the outer alignment is used. + /// + /// You can find an example for this argument at the + /// [`table.align`]($table.align) parameter. + #[borrowed] + pub align: Celled<Smart<Alignment>>, + + /// How to [stroke]($stroke) the cells. + /// + /// Grids have no strokes by default, which can be changed by setting this + /// option to the desired stroke. + /// + /// If it is necessary to place lines which can cross spacing between cells + /// produced by the `gutter` option, or to override the stroke between + /// multiple specific cells, consider specifying one or more of + /// [`grid.hline`]($grid.hline) and [`grid.vline`]($grid.vline) alongside + /// your grid cells. + /// + /// ```example + /// #set page(height: 13em, width: 26em) + /// + /// #let cv(..jobs) = grid( + /// columns: 2, + /// inset: 5pt, + /// stroke: (x, y) => if x == 0 and y > 0 { + /// (right: ( + /// paint: luma(180), + /// thickness: 1.5pt, + /// dash: "dotted" + /// )) + /// }, + /// grid.header(grid.cell(colspan: 2)[ + /// *Professional Experience* + /// #box(width: 1fr, line(length: 100%, stroke: luma(180))) + /// ]), + /// ..{ + /// let last = none + /// for job in jobs.pos() { + /// ( + /// if job.year != last [*#job.year*], + /// [ + /// *#job.company* - #job.role _(#job.timeframe)_ \ + /// #job.details + /// ] + /// ) + /// last = job.year + /// } + /// } + /// ) + /// + /// #cv( + /// ( + /// year: 2012, + /// company: [Pear Seed & Co.], + /// role: [Lead Engineer], + /// timeframe: [Jul - Dec], + /// details: [ + /// - Raised engineers from 3x to 10x + /// - Did a great job + /// ], + /// ), + /// ( + /// year: 2012, + /// company: [Mega Corp.], + /// role: [VP of Sales], + /// timeframe: [Mar - Jun], + /// details: [- Closed tons of customers], + /// ), + /// ( + /// year: 2013, + /// company: [Tiny Co.], + /// role: [CEO], + /// timeframe: [Jan - Dec], + /// details: [- Delivered 4x more shareholder value], + /// ), + /// ( + /// year: 2014, + /// company: [Glorbocorp Ltd], + /// role: [CTO], + /// timeframe: [Jan - Mar], + /// details: [- Drove containerization forward], + /// ), + /// ) + /// ``` + #[resolve] + #[fold] + pub stroke: Celled<Sides<Option<Option<Arc<Stroke>>>>>, + + /// How much to pad the cells' content. + /// + /// You can find an example for this argument at the + /// [`table.inset`]($table.inset) parameter. + #[fold] + pub inset: Celled<Sides<Option<Rel<Length>>>>, + + /// The contents of the grid cells, plus any extra grid lines specified + /// with the [`grid.hline`]($grid.hline) and [`grid.vline`]($grid.vline) + /// elements. + /// + /// The cells are populated in row-major order. + #[variadic] + pub children: Vec<GridChild>, +} + +#[scope] +impl GridElem { + #[elem] + type GridCell; + + #[elem] + type GridHLine; + + #[elem] + type GridVLine; + + #[elem] + type GridHeader; + + #[elem] + type GridFooter; +} + +impl Show for Packed<GridElem> { + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_grid) + .pack() + .spanned(self.span())) + } +} + +/// Track sizing definitions. +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct TrackSizings(pub SmallVec<[Sizing; 4]>); + +cast! { + TrackSizings, + self => self.0.into_value(), + sizing: Sizing => Self(smallvec![sizing]), + count: NonZeroUsize => Self(smallvec![Sizing::Auto; count.get()]), + values: Array => Self(values.into_iter().map(Value::cast).collect::<HintedStrResult<_>>()?), +} + +/// Any child of a grid element. +#[derive(Debug, PartialEq, Clone, Hash)] +pub enum GridChild { + Header(Packed<GridHeader>), + Footer(Packed<GridFooter>), + Item(GridItem), +} + +cast! { + GridChild, + self => match self { + Self::Header(header) => header.into_value(), + Self::Footer(footer) => footer.into_value(), + Self::Item(item) => item.into_value(), + }, + v: Content => { + v.try_into()? + }, +} + +impl TryFrom<Content> for GridChild { + type Error = HintedString; + fn try_from(value: Content) -> HintedStrResult<Self> { + if value.is::<TableHeader>() { + bail!( + "cannot use `table.header` as a grid header"; + hint: "use `grid.header` instead" + ) + } + if value.is::<TableFooter>() { + bail!( + "cannot use `table.footer` as a grid footer"; + hint: "use `grid.footer` instead" + ) + } + + value + .into_packed::<GridHeader>() + .map(Self::Header) + .or_else(|value| value.into_packed::<GridFooter>().map(Self::Footer)) + .or_else(|value| GridItem::try_from(value).map(Self::Item)) + } +} + +/// A grid item, which is the basic unit of grid specification. +#[derive(Debug, PartialEq, Clone, Hash)] +pub enum GridItem { + HLine(Packed<GridHLine>), + VLine(Packed<GridVLine>), + Cell(Packed<GridCell>), +} + +cast! { + GridItem, + self => match self { + Self::HLine(hline) => hline.into_value(), + Self::VLine(vline) => vline.into_value(), + Self::Cell(cell) => cell.into_value(), + }, + v: Content => { + v.try_into()? + } +} + +impl TryFrom<Content> for GridItem { + type Error = HintedString; + fn try_from(value: Content) -> HintedStrResult<Self> { + if value.is::<GridHeader>() { + bail!("cannot place a grid header within another header or footer"); + } + if value.is::<TableHeader>() { + bail!("cannot place a table header within another header or footer"); + } + if value.is::<GridFooter>() { + bail!("cannot place a grid footer within another footer or header"); + } + if value.is::<TableFooter>() { + bail!("cannot place a table footer within another footer or header"); + } + if value.is::<TableCell>() { + bail!( + "cannot use `table.cell` as a grid cell"; + hint: "use `grid.cell` instead" + ); + } + if value.is::<TableHLine>() { + bail!( + "cannot use `table.hline` as a grid line"; + hint: "use `grid.hline` instead" + ); + } + if value.is::<TableVLine>() { + bail!( + "cannot use `table.vline` as a grid line"; + hint: "use `grid.vline` instead" + ); + } + + Ok(value + .into_packed::<GridHLine>() + .map(Self::HLine) + .or_else(|value| value.into_packed::<GridVLine>().map(Self::VLine)) + .or_else(|value| value.into_packed::<GridCell>().map(Self::Cell)) + .unwrap_or_else(|value| { + let span = value.span(); + Self::Cell(Packed::new(GridCell::new(value)).spanned(span)) + })) + } +} + +/// A repeatable grid header. +/// +/// If `repeat` is set to `true`, the header will be repeated across pages. For +/// an example, refer to the [`table.header`]($table.header) element and the +/// [`grid.stroke`]($grid.stroke) parameter. +#[elem(name = "header", title = "Grid Header")] +pub struct GridHeader { + /// Whether this header should be repeated across pages. + #[default(true)] + pub repeat: bool, + + /// The cells and lines within the header. + #[variadic] + pub children: Vec<GridItem>, +} + +/// A repeatable grid footer. +/// +/// Just like the [`grid.header`]($grid.header) element, the footer can repeat +/// itself on every page of the table. +/// +/// No other grid cells may be placed after the footer. +#[elem(name = "footer", title = "Grid Footer")] +pub struct GridFooter { + /// Whether this footer should be repeated across pages. + #[default(true)] + pub repeat: bool, + + /// The cells and lines within the footer. + #[variadic] + pub children: Vec<GridItem>, +} + +/// A horizontal line in the grid. +/// +/// Overrides any per-cell stroke, including stroke specified through the grid's +/// `stroke` field. Can cross spacing between cells created through the grid's +/// `column-gutter` option. +/// +/// An example for this function can be found at the +/// [`table.hline`]($table.hline) element. +#[elem(name = "hline", title = "Grid Horizontal Line")] +pub struct GridHLine { + /// The row above which the horizontal line is placed (zero-indexed). + /// If the `position` field is set to `{bottom}`, the line is placed below + /// the row with the given index instead (see that field's docs for + /// details). + /// + /// Specifying `{auto}` causes the line to be placed at the row below the + /// last automatically positioned cell (that is, cell without coordinate + /// overrides) before the line among the grid's children. If there is no + /// such cell before the line, it is placed at the top of the grid (row 0). + /// Note that specifying for this option exactly the total amount of rows + /// in the grid causes this horizontal line to override the bottom border + /// of the grid, while a value of 0 overrides the top border. + pub y: Smart<usize>, + + /// The column at which the horizontal line starts (zero-indexed, inclusive). + pub start: usize, + + /// The column before which the horizontal line ends (zero-indexed, + /// exclusive). + /// Therefore, the horizontal line will be drawn up to and across column + /// `end - 1`. + /// + /// A value equal to `{none}` or to the amount of columns causes it to + /// extend all the way towards the end of the grid. + pub end: Option<NonZeroUsize>, + + /// The line's stroke. + /// + /// Specifying `{none}` removes any lines previously placed across this + /// line's range, including hlines or per-cell stroke below it. + #[resolve] + #[fold] + #[default(Some(Arc::new(Stroke::default())))] + pub stroke: Option<Arc<Stroke>>, + + /// The position at which the line is placed, given its row (`y`) - either + /// `{top}` to draw above it or `{bottom}` to draw below it. + /// + /// This setting is only relevant when row gutter is enabled (and + /// shouldn't be used otherwise - prefer just increasing the `y` field by + /// one instead), since then the position below a row becomes different + /// from the position above the next row due to the spacing between both. + #[default(OuterVAlignment::Top)] + pub position: OuterVAlignment, +} + +/// A vertical line in the grid. +/// +/// Overrides any per-cell stroke, including stroke specified through the +/// grid's `stroke` field. Can cross spacing between cells created through +/// the grid's `row-gutter` option. +#[elem(name = "vline", title = "Grid Vertical Line")] +pub struct GridVLine { + /// The column before which the horizontal line is placed (zero-indexed). + /// If the `position` field is set to `{end}`, the line is placed after the + /// column with the given index instead (see that field's docs for + /// details). + /// + /// Specifying `{auto}` causes the line to be placed at the column after + /// the last automatically positioned cell (that is, cell without + /// coordinate overrides) before the line among the grid's children. If + /// there is no such cell before the line, it is placed before the grid's + /// first column (column 0). + /// Note that specifying for this option exactly the total amount of + /// columns in the grid causes this vertical line to override the end + /// border of the grid (right in LTR, left in RTL), while a value of 0 + /// overrides the start border (left in LTR, right in RTL). + pub x: Smart<usize>, + + /// The row at which the vertical line starts (zero-indexed, inclusive). + pub start: usize, + + /// The row on top of which the vertical line ends (zero-indexed, + /// exclusive). + /// Therefore, the vertical line will be drawn up to and across row + /// `end - 1`. + /// + /// A value equal to `{none}` or to the amount of rows causes it to extend + /// all the way towards the bottom of the grid. + pub end: Option<NonZeroUsize>, + + /// The line's stroke. + /// + /// Specifying `{none}` removes any lines previously placed across this + /// line's range, including vlines or per-cell stroke below it. + #[resolve] + #[fold] + #[default(Some(Arc::new(Stroke::default())))] + pub stroke: Option<Arc<Stroke>>, + + /// The position at which the line is placed, given its column (`x`) - + /// either `{start}` to draw before it or `{end}` to draw after it. + /// + /// The values `{left}` and `{right}` are also accepted, but discouraged as + /// they cause your grid to be inconsistent between left-to-right and + /// right-to-left documents. + /// + /// This setting is only relevant when column gutter is enabled (and + /// shouldn't be used otherwise - prefer just increasing the `x` field by + /// one instead), since then the position after a column becomes different + /// from the position before the next column due to the spacing between + /// both. + #[default(OuterHAlignment::Start)] + pub position: OuterHAlignment, +} + +/// A cell in the grid. You can use this function in the argument list of a grid +/// to override grid style properties for an individual cell or manually +/// positioning it within the grid. You can also use this function in show rules +/// to apply certain styles to multiple cells at once. +/// +/// For example, you can override the position and stroke for a single cell: +/// +/// ```example +/// >>> #set page(width: auto) +/// >>> #set text(15pt, font: "Noto Sans Symbols 2", bottom-edge: -.2em) +/// <<< #set text(15pt, font: "Noto Sans Symbols 2") +/// #show regex("[♚-♟︎]"): set text(fill: rgb("21212A")) +/// #show regex("[♔-♙]"): set text(fill: rgb("111015")) +/// +/// #grid( +/// fill: (x, y) => rgb( +/// if calc.odd(x + y) { "7F8396" } +/// else { "EFF0F3" } +/// ), +/// columns: (1em,) * 8, +/// rows: 1em, +/// align: center + horizon, +/// +/// [♖], [♘], [♗], [♕], [♔], [♗], [♘], [♖], +/// [♙], [♙], [♙], [♙], [], [♙], [♙], [♙], +/// grid.cell( +/// x: 4, y: 3, +/// stroke: blue.transparentize(60%) +/// )[♙], +/// +/// ..(grid.cell(y: 6)[♟],) * 8, +/// ..([♜], [♞], [♝], [♛], [♚], [♝], [♞], [♜]) +/// .map(grid.cell.with(y: 7)), +/// ) +/// ``` +/// +/// You may also apply a show rule on `grid.cell` to style all cells at once, +/// which allows you, for example, to apply styles based on a cell's position. +/// Refer to the examples of the [`table.cell`]($table.cell) element to learn +/// more about this. +#[elem(name = "cell", title = "Grid Cell", Show)] +pub struct GridCell { + /// The cell's body. + #[required] + pub body: Content, + + /// The cell's column (zero-indexed). + /// This field may be used in show rules to style a cell depending on its + /// column. + /// + /// You may override this field to pick in which column the cell must + /// be placed. If no row (`y`) is chosen, the cell will be placed in the + /// first row (starting at row 0) with that column available (or a new row + /// if none). If both `x` and `y` are chosen, however, the cell will be + /// placed in that exact position. An error is raised if that position is + /// not available (thus, it is usually wise to specify cells with a custom + /// position before cells with automatic positions). + /// + /// ```example + /// #let circ(c) = circle( + /// fill: c, width: 5mm + /// ) + /// + /// #grid( + /// columns: 4, + /// rows: 7mm, + /// stroke: .5pt + blue, + /// align: center + horizon, + /// inset: 1mm, + /// + /// grid.cell(x: 2, y: 2, circ(aqua)), + /// circ(yellow), + /// grid.cell(x: 3, circ(green)), + /// circ(black), + /// ) + /// ``` + pub x: Smart<usize>, + + /// The cell's row (zero-indexed). + /// This field may be used in show rules to style a cell depending on its + /// row. + /// + /// You may override this field to pick in which row the cell must be + /// placed. If no column (`x`) is chosen, the cell will be placed in the + /// first column (starting at column 0) available in the chosen row. If all + /// columns in the chosen row are already occupied, an error is raised. + /// + /// ```example + /// #let tri(c) = polygon.regular( + /// fill: c, + /// size: 5mm, + /// vertices: 3, + /// ) + /// + /// #grid( + /// columns: 2, + /// stroke: blue, + /// inset: 1mm, + /// + /// tri(black), + /// grid.cell(y: 1, tri(teal)), + /// grid.cell(y: 1, tri(red)), + /// grid.cell(y: 2, tri(orange)) + /// ) + /// ``` + pub y: Smart<usize>, + + /// The amount of columns spanned by this cell. + #[default(NonZeroUsize::ONE)] + pub colspan: NonZeroUsize, + + /// The amount of rows spanned by this cell. + #[default(NonZeroUsize::ONE)] + pub rowspan: NonZeroUsize, + + /// The cell's [fill]($grid.fill) override. + pub fill: Smart<Option<Paint>>, + + /// The cell's [alignment]($grid.align) override. + pub align: Smart<Alignment>, + + /// The cell's [inset]($grid.inset) override. + pub inset: Smart<Sides<Option<Rel<Length>>>>, + + /// The cell's [stroke]($grid.stroke) override. + #[resolve] + #[fold] + pub stroke: Sides<Option<Option<Arc<Stroke>>>>, + + /// Whether rows spanned by this cell can be placed in different pages. + /// When equal to `{auto}`, a cell spanning only fixed-size rows is + /// unbreakable, while a cell spanning at least one `{auto}`-sized row is + /// breakable. + pub breakable: Smart<bool>, +} + +cast! { + GridCell, + v: Content => v.into(), +} + +impl Show for Packed<GridCell> { + fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles)) + } +} + +impl Default for Packed<GridCell> { + fn default() -> Self { + Packed::new(GridCell::new(Content::default())) + } +} + +impl From<Content> for GridCell { + fn from(value: Content) -> Self { + #[allow(clippy::unwrap_or_default)] + value.unpack::<Self>().unwrap_or_else(Self::new) + } +} + +/// Function with common code to display a grid cell or table cell. +pub(crate) fn show_grid_cell( + mut body: Content, + inset: Smart<Sides<Option<Rel<Length>>>>, + align: Smart<Alignment>, +) -> SourceResult<Content> { + let inset = inset.unwrap_or_default().map(Option::unwrap_or_default); + + if inset != Sides::default() { + // Only pad if some inset is not 0pt. + // Avoids a bug where using .padded() in any way inside Show causes + // alignment in align(...) to break. + body = body.padded(inset); + } + + if let Smart::Custom(alignment) = align { + body = body.aligned(alignment); + } + + Ok(body) +} + +/// A value that can be configured per cell. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum Celled<T> { + /// A bare value, the same for all cells. + Value(T), + /// A closure mapping from cell coordinates to a value. + Func(Func), + /// An array of alignment values corresponding to each column. + Array(Vec<T>), +} + +impl<T: Default + Clone + FromValue> Celled<T> { + /// Resolve the value based on the cell position. + pub fn resolve( + &self, + engine: &mut Engine, + styles: StyleChain, + x: usize, + y: usize, + ) -> SourceResult<T> { + Ok(match self { + Self::Value(value) => value.clone(), + Self::Func(func) => func + .call(engine, Context::new(None, Some(styles)).track(), [x, y])? + .cast() + .at(func.span())?, + Self::Array(array) => x + .checked_rem(array.len()) + .and_then(|i| array.get(i)) + .cloned() + .unwrap_or_default(), + }) + } +} + +impl<T: Default> Default for Celled<T> { + fn default() -> Self { + Self::Value(T::default()) + } +} + +impl<T: Reflect> Reflect for Celled<T> { + fn input() -> CastInfo { + T::input() + Array::input() + Func::input() + } + + fn output() -> CastInfo { + T::output() + Array::output() + Func::output() + } + + fn castable(value: &Value) -> bool { + Array::castable(value) || Func::castable(value) || T::castable(value) + } +} + +impl<T: IntoValue> IntoValue for Celled<T> { + fn into_value(self) -> Value { + match self { + Self::Value(value) => value.into_value(), + Self::Func(func) => func.into_value(), + Self::Array(arr) => arr.into_value(), + } + } +} + +impl<T: FromValue> FromValue for Celled<T> { + fn from_value(value: Value) -> HintedStrResult<Self> { + match value { + Value::Func(v) => Ok(Self::Func(v)), + Value::Array(array) => Ok(Self::Array( + array.into_iter().map(T::from_value).collect::<HintedStrResult<_>>()?, + )), + v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)), + v => Err(Self::error(&v)), + } + } +} + +impl<T: Fold> Fold for Celled<T> { + fn fold(self, outer: Self) -> Self { + match (self, outer) { + (Self::Value(inner), Self::Value(outer)) => Self::Value(inner.fold(outer)), + (self_, _) => self_, + } + } +} + +impl<T: Resolve> Resolve for Celled<T> { + type Output = ResolvedCelled<T>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + match self { + Self::Value(value) => ResolvedCelled(Celled::Value(value.resolve(styles))), + Self::Func(func) => ResolvedCelled(Celled::Func(func)), + Self::Array(values) => ResolvedCelled(Celled::Array( + values.into_iter().map(|value| value.resolve(styles)).collect(), + )), + } + } +} + +/// The result of resolving a Celled's value according to styles. +/// Holds resolved values which depend on each grid cell's position. +/// When it is a closure, however, it is only resolved when the closure is +/// called. +#[derive(Default, Clone)] +pub struct ResolvedCelled<T: Resolve>(Celled<T::Output>); + +impl<T> ResolvedCelled<T> +where + T: FromValue + Resolve, + <T as Resolve>::Output: Default + Clone, +{ + /// Resolve the value based on the cell position. + pub fn resolve( + &self, + engine: &mut Engine, + styles: StyleChain, + x: usize, + y: usize, + ) -> SourceResult<T::Output> { + Ok(match &self.0 { + Celled::Value(value) => value.clone(), + Celled::Func(func) => func + .call(engine, Context::new(None, Some(styles)).track(), [x, y])? + .cast::<T>() + .at(func.span())? + .resolve(styles), + Celled::Array(array) => x + .checked_rem(array.len()) + .and_then(|i| array.get(i)) + .cloned() + .unwrap_or_default(), + }) + } +} diff --git a/crates/typst-library/src/layout/hide.rs b/crates/typst-library/src/layout/hide.rs new file mode 100644 index 00000000..1b8b9bd5 --- /dev/null +++ b/crates/typst-library/src/layout/hide.rs @@ -0,0 +1,34 @@ +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{elem, Content, Packed, Show, StyleChain}; + +/// Hides content without affecting layout. +/// +/// The `hide` function allows you to hide content while the layout still 'sees' +/// it. This is useful to create whitespace that is exactly as large as some +/// content. It may also be useful to redact content because its arguments are +/// not included in the output. +/// +/// # Example +/// ```example +/// Hello Jane \ +/// #hide[Hello] Joe +/// ``` +#[elem(Show)] +pub struct HideElem { + /// The content to hide. + #[required] + pub body: Content, + + /// This style is set on the content contained in the `hide` element. + #[internal] + #[ghost] + pub hidden: bool, +} + +impl Show for Packed<HideElem> { + #[typst_macros::time(name = "hide", span = self.span())] + fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(self.body().clone().styled(HideElem::set_hidden(true))) + } +} diff --git a/crates/typst-library/src/layout/layout.rs b/crates/typst-library/src/layout/layout.rs new file mode 100644 index 00000000..c3d112e1 --- /dev/null +++ b/crates/typst-library/src/layout/layout.rs @@ -0,0 +1,107 @@ +use comemo::Track; +use typst_syntax::Span; + +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{ + dict, elem, func, Content, Context, Func, NativeElement, Packed, Show, StyleChain, +}; +use crate::introspection::Locatable; +use crate::layout::{BlockElem, Size}; + +/// Provides access to the current outer container's (or page's, if none) +/// dimensions (width and height). +/// +/// Accepts a function that receives a single parameter, which is a dictionary +/// with keys `width` and `height`, both of type [`length`]. The function is +/// provided [context], meaning you don't need to use it in combination with the +/// `context` keyword. This is why [`measure`] can be called in the example +/// below. +/// +/// ```example +/// #let text = lorem(30) +/// #layout(size => [ +/// #let (height,) = measure( +/// block(width: size.width, text), +/// ) +/// This text is #height high with +/// the current page width: \ +/// #text +/// ]) +/// ``` +/// +/// Note that the `layout` function forces its contents into a [block]-level +/// container, so placement relative to the page or pagebreaks are not possible +/// within it. +/// +/// If the `layout` call is placed inside a box with a width of `{800pt}` and a +/// height of `{400pt}`, then the specified function will be given the argument +/// `{(width: 800pt, height: 400pt)}`. If it is placed directly into the page, it +/// receives the page's dimensions minus its margins. This is mostly useful in +/// combination with [measurement]($measure). +/// +/// You can also use this function to resolve [`ratio`] to fixed lengths. This +/// might come in handy if you're building your own layout abstractions. +/// +/// ```example +/// #layout(size => { +/// let half = 50% * size.width +/// [Half a page is #half wide.] +/// }) +/// ``` +/// +/// Note that the width or height provided by `layout` will be infinite if the +/// corresponding page dimension is set to `{auto}`. +#[func] +pub fn layout( + /// The call span of this function. + span: Span, + /// A function to call with the outer container's size. Its return value is + /// displayed in the document. + /// + /// The container's size is given as a [dictionary] with the keys `width` + /// and `height`. + /// + /// This function is called once for each time the content returned by + /// `layout` appears in the document. This makes it possible to generate + /// content that depends on the dimensions of its container. + func: Func, +) -> Content { + LayoutElem::new(func).pack().spanned(span) +} + +/// Executes a `layout` call. +#[elem(Locatable, Show)] +struct LayoutElem { + /// The function to call with the outer container's (or page's) size. + #[required] + func: Func, +} + +impl Show for Packed<LayoutElem> { + fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::multi_layouter( + self.clone(), + |elem, engine, locator, styles, regions| { + // Gets the current region's base size, which will be the size of the + // outer container, or of the page if there is no such container. + let Size { x, y } = regions.base(); + let loc = elem.location().unwrap(); + let context = Context::new(Some(loc), Some(styles)); + let result = elem + .func() + .call( + engine, + context.track(), + [dict! { "width" => x, "height" => y }], + )? + .display(); + (engine.routines.layout_fragment)( + engine, &result, locator, styles, regions, + ) + }, + ) + .pack() + .spanned(self.span())) + } +} diff --git a/crates/typst-library/src/layout/length.rs b/crates/typst-library/src/layout/length.rs new file mode 100644 index 00000000..800140c4 --- /dev/null +++ b/crates/typst-library/src/layout/length.rs @@ -0,0 +1,276 @@ +use std::cmp::Ordering; +use std::fmt::{self, Debug, Formatter}; +use std::ops::{Add, Div, Mul, Neg}; + +use comemo::Tracked; +use ecow::{eco_format, EcoString}; +use typst_syntax::Span; +use typst_utils::Numeric; + +use crate::diag::{bail, HintedStrResult, SourceResult}; +use crate::foundations::{func, scope, ty, Context, Fold, Repr, Resolve, StyleChain}; +use crate::layout::{Abs, Em}; + +/// A size or distance, possibly expressed with contextual units. +/// +/// Typst supports the following length units: +/// +/// - Points: `{72pt}` +/// - Millimeters: `{254mm}` +/// - Centimeters: `{2.54cm}` +/// - Inches: `{1in}` +/// - Relative to font size: `{2.5em}` +/// +/// You can multiply lengths with and divide them by integers and floats. +/// +/// # Example +/// ```example +/// #rect(width: 20pt) +/// #rect(width: 2em) +/// #rect(width: 1in) +/// +/// #(3em + 5pt).em \ +/// #(20pt).em \ +/// #(40em + 2pt).abs \ +/// #(5em).abs +/// ``` +/// +/// # Fields +/// - `abs`: A length with just the absolute component of the current length +/// (that is, excluding the `em` component). +/// - `em`: The amount of `em` units in this length, as a [float]. +#[ty(scope, cast)] +#[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 + } + } + + /// Convert to an absolute length at the given font size. + pub fn at(self, font_size: Abs) -> Abs { + self.abs + self.em.at(font_size) + } + + /// Fails with an error if the length has a non-zero font-relative part. + fn ensure_that_em_is_zero(&self, span: Span, unit: &str) -> SourceResult<()> { + if self.em == Em::zero() { + return Ok(()); + } + + bail!( + span, + "cannot convert a length with non-zero em units (`{}`) to {unit}", + self.repr(); + hint: "use `length.to-absolute()` to resolve its em component \ + (requires context)"; + hint: "or use `length.abs.{unit}()` instead to ignore its em component" + ) + } +} + +#[scope] +impl Length { + /// Converts this length to points. + /// + /// Fails with an error if this length has non-zero `em` units (such as + /// `5em + 2pt` instead of just `2pt`). Use the `abs` field (such as in + /// `(5em + 2pt).abs.pt()`) to ignore the `em` component of the length (thus + /// converting only its absolute component). + #[func(name = "pt", title = "Points")] + pub fn to_pt(&self, span: Span) -> SourceResult<f64> { + self.ensure_that_em_is_zero(span, "pt")?; + Ok(self.abs.to_pt()) + } + + /// Converts this length to millimeters. + /// + /// Fails with an error if this length has non-zero `em` units. See the + /// [`pt`]($length.pt) method for more details. + #[func(name = "mm", title = "Millimeters")] + pub fn to_mm(&self, span: Span) -> SourceResult<f64> { + self.ensure_that_em_is_zero(span, "mm")?; + Ok(self.abs.to_mm()) + } + + /// Converts this length to centimeters. + /// + /// Fails with an error if this length has non-zero `em` units. See the + /// [`pt`]($length.pt) method for more details. + #[func(name = "cm", title = "Centimeters")] + pub fn to_cm(&self, span: Span) -> SourceResult<f64> { + self.ensure_that_em_is_zero(span, "cm")?; + Ok(self.abs.to_cm()) + } + + /// Converts this length to inches. + /// + /// Fails with an error if this length has non-zero `em` units. See the + /// [`pt`]($length.pt) method for more details. + #[func(name = "inches")] + pub fn to_inches(&self, span: Span) -> SourceResult<f64> { + self.ensure_that_em_is_zero(span, "inches")?; + Ok(self.abs.to_inches()) + } + + /// Resolve this length to an absolute length. + /// + /// ```example + /// #set text(size: 12pt) + /// #context [ + /// #(6pt).to-absolute() \ + /// #(6pt + 10em).to-absolute() \ + /// #(10em).to-absolute() + /// ] + /// + /// #set text(size: 6pt) + /// #context [ + /// #(6pt).to-absolute() \ + /// #(6pt + 10em).to-absolute() \ + /// #(10em).to-absolute() + /// ] + /// ``` + #[func] + pub fn to_absolute(&self, context: Tracked<Context>) -> HintedStrResult<Length> { + Ok(self.resolve(context.styles()?).into()) + } +} + +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 Repr for Length { + fn repr(&self) -> EcoString { + match (self.abs.is_zero(), self.em.is_zero()) { + (false, false) => eco_format!("{} + {}", self.abs.repr(), self.em.repr()), + (true, false) => self.em.repr(), + (_, true) => self.abs.repr(), + } + } +} + +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 } + } +} + +typst_utils::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 Mul<Length> for f64 { + type Output = Length; + + fn mul(self, rhs: Length) -> Self::Output { + rhs * self + } +} + +impl Div<f64> for Length { + type Output = Self; + + fn div(self, rhs: f64) -> Self::Output { + Self { abs: self.abs / rhs, em: self.em / rhs } + } +} + +typst_utils::assign_impl!(Length += Length); +typst_utils::assign_impl!(Length -= Length); +typst_utils::assign_impl!(Length *= f64); +typst_utils::assign_impl!(Length /= f64); + +impl Resolve for Length { + type Output = Abs; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.abs + self.em.resolve(styles) + } +} + +impl Fold for Length { + fn fold(self, _: Self) -> Self { + self + } +} diff --git a/crates/typst-library/src/layout/measure.rs b/crates/typst-library/src/layout/measure.rs new file mode 100644 index 00000000..2fa51b2d --- /dev/null +++ b/crates/typst-library/src/layout/measure.rs @@ -0,0 +1,115 @@ +use comemo::Tracked; +use typst_syntax::Span; + +use crate::diag::{warning, At, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + dict, func, Content, Context, Dict, Resolve, Smart, StyleChain, Styles, +}; +use crate::introspection::{Locator, LocatorLink}; +use crate::layout::{Abs, Axes, Length, Region, Size}; + +/// Measures the layouted size of content. +/// +/// The `measure` function lets you determine the layouted size of content. +/// By default an infinite space is assumed, so the measured dimensions may +/// not necessarily match the final dimensions of the content. +/// If you want to measure in the current layout dimensions, you can combine +/// `measure` and [`layout`]. +/// +/// # Example +/// The same content can have a different size depending on the [context] that +/// it is placed into. In the example below, the `[#content]` is of course +/// bigger when we increase the font size. +/// +/// ```example +/// #let content = [Hello!] +/// #content +/// #set text(14pt) +/// #content +/// ``` +/// +/// For this reason, you can only measure when context is available. +/// +/// ```example +/// #let thing(body) = context { +/// let size = measure(body) +/// [Width of "#body" is #size.width] +/// } +/// +/// #thing[Hey] \ +/// #thing[Welcome] +/// ``` +/// +/// The measure function returns a dictionary with the entries `width` and +/// `height`, both of type [`length`]. +#[func(contextual)] +pub fn measure( + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// The callsite span. + span: Span, + /// The width available to layout the content. + /// + /// Setting this to `{auto}` indicates infinite available width. + /// + /// Note that using the `width` and `height` parameters of this function is + /// different from measuring a sized [`block`] containing the content. In + /// the following example, the former will get the dimensions of the inner + /// content instead of the dimensions of the block. + /// + /// ```example + /// #context measure(lorem(100), width: 400pt) + /// + /// #context measure(block(lorem(100), width: 400pt)) + /// ``` + #[named] + #[default(Smart::Auto)] + width: Smart<Length>, + /// The height available to layout the content. + /// + /// Setting this to `{auto}` indicates infinite available height. + #[named] + #[default(Smart::Auto)] + height: Smart<Length>, + /// The content whose size to measure. + content: Content, + /// _Compatibility:_ This argument is deprecated. It only exists for + /// compatibility with Typst 0.10 and lower and shouldn't be used anymore. + #[default] + styles: Option<Styles>, +) -> SourceResult<Dict> { + let styles = match &styles { + Some(styles) => { + engine.sink.warn(warning!( + span, "calling `measure` with a styles argument is deprecated"; + hint: "try removing the styles argument" + )); + StyleChain::new(styles) + } + None => context.styles().at(span)?, + }; + + // Create a pod region with the available space. + let pod = Region::new( + Axes::new( + width.resolve(styles).unwrap_or(Abs::inf()), + height.resolve(styles).unwrap_or(Abs::inf()), + ), + Axes::splat(false), + ); + + // We put the locator into a special "measurement mode" to ensure that + // introspection-driven features within the content continue to work. Read + // the "Dealing with measurement" section of the [`Locator`] docs for more + // details. + let here = context.location().at(span)?; + let link = LocatorLink::measure(here); + let locator = Locator::link(&link); + + let frame = (engine.routines.layout_frame)(engine, &content, locator, styles, pod)?; + let Size { x, y } = frame.size(); + Ok(dict! { "width" => x, "height" => y }) +} diff --git a/crates/typst-library/src/layout/mod.rs b/crates/typst-library/src/layout/mod.rs new file mode 100644 index 00000000..b54d6906 --- /dev/null +++ b/crates/typst-library/src/layout/mod.rs @@ -0,0 +1,106 @@ +//! Composable layouts. + +mod abs; +mod align; +mod angle; +mod axes; +mod columns; +mod container; +mod corners; +mod dir; +mod em; +mod fr; +mod fragment; +mod frame; +mod grid; +mod hide; +#[path = "layout.rs"] +mod layout_; +mod length; +#[path = "measure.rs"] +mod measure_; +mod pad; +mod page; +mod place; +mod point; +mod ratio; +mod regions; +mod rel; +mod repeat; +mod sides; +mod size; +mod spacing; +mod stack; +mod transform; + +pub use self::abs::*; +pub use self::align::*; +pub use self::angle::*; +pub use self::axes::*; +pub use self::columns::*; +pub use self::container::*; +pub use self::corners::*; +pub use self::dir::*; +pub use self::em::*; +pub use self::fr::*; +pub use self::fragment::*; +pub use self::frame::*; +pub use self::grid::*; +pub use self::hide::*; +pub use self::layout_::*; +pub use self::length::*; +pub use self::measure_::*; +pub use self::pad::*; +pub use self::page::*; +pub use self::place::*; +pub use self::point::*; +pub use self::ratio::*; +pub use self::regions::*; +pub use self::rel::*; +pub use self::repeat::*; +pub use self::sides::*; +pub use self::size::*; +pub use self::spacing::*; +pub use self::stack::*; +pub use self::transform::*; + +use crate::foundations::{category, Category, Scope}; + +/// Arranging elements on the page in different ways. +/// +/// By combining layout functions, you can create complex and automatic layouts. +#[category] +pub static LAYOUT: Category; + +/// Hook up all `layout` definitions. +pub fn define(global: &mut Scope) { + global.category(LAYOUT); + global.define_type::<Length>(); + global.define_type::<Angle>(); + global.define_type::<Ratio>(); + global.define_type::<Rel<Length>>(); + global.define_type::<Fr>(); + global.define_type::<Dir>(); + global.define_type::<Alignment>(); + global.define_elem::<PageElem>(); + global.define_elem::<PagebreakElem>(); + global.define_elem::<VElem>(); + global.define_elem::<HElem>(); + global.define_elem::<BoxElem>(); + global.define_elem::<BlockElem>(); + global.define_elem::<StackElem>(); + global.define_elem::<GridElem>(); + global.define_elem::<ColumnsElem>(); + global.define_elem::<ColbreakElem>(); + global.define_elem::<PlaceElem>(); + global.define_elem::<AlignElem>(); + global.define_elem::<PadElem>(); + global.define_elem::<RepeatElem>(); + global.define_elem::<MoveElem>(); + global.define_elem::<ScaleElem>(); + global.define_elem::<RotateElem>(); + global.define_elem::<SkewElem>(); + global.define_elem::<HideElem>(); + global.define_func::<measure>(); + global.define_func::<layout>(); +} diff --git a/crates/typst-library/src/layout/pad.rs b/crates/typst-library/src/layout/pad.rs new file mode 100644 index 00000000..1dc6d131 --- /dev/null +++ b/crates/typst-library/src/layout/pad.rs @@ -0,0 +1,65 @@ +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain}; +use crate::layout::{BlockElem, Length, Rel}; + +/// Adds spacing around content. +/// +/// The spacing can be specified for each side individually, or for all sides at +/// once by specifying a positional argument. +/// +/// # Example +/// ```example +/// #set align(center) +/// +/// #pad(x: 16pt, image("typing.jpg")) +/// _Typing speeds can be +/// measured in words per minute._ +/// ``` +#[elem(title = "Padding", Show)] +pub struct PadElem { + /// The padding at the left side. + #[parse( + let all = args.named("rest")?.or(args.find()?); + let x = args.named("x")?.or(all); + let y = args.named("y")?.or(all); + args.named("left")?.or(x) + )] + pub left: Rel<Length>, + + /// The padding at the top side. + #[parse(args.named("top")?.or(y))] + pub top: Rel<Length>, + + /// The padding at the right side. + #[parse(args.named("right")?.or(x))] + pub right: Rel<Length>, + + /// The padding at the bottom side. + #[parse(args.named("bottom")?.or(y))] + pub bottom: Rel<Length>, + + /// A shorthand to set `left` and `right` to the same value. + #[external] + pub x: Rel<Length>, + + /// A shorthand to set `top` and `bottom` to the same value. + #[external] + pub y: Rel<Length>, + + /// A shorthand to set all four sides to the same value. + #[external] + pub rest: Rel<Length>, + + /// The content to pad at the sides. + #[required] + pub body: Content, +} + +impl Show for Packed<PadElem> { + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_pad) + .pack() + .spanned(self.span())) + } +} diff --git a/crates/typst-library/src/layout/page.rs b/crates/typst-library/src/layout/page.rs new file mode 100644 index 00000000..de278cb6 --- /dev/null +++ b/crates/typst-library/src/layout/page.rs @@ -0,0 +1,924 @@ +use std::borrow::Cow; +use std::num::NonZeroUsize; +use std::ops::RangeInclusive; +use std::str::FromStr; + +use comemo::Track; +use typst_utils::{singleton, NonZeroExt, Scalar}; + +use crate::diag::{bail, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, Args, AutoValue, Cast, Construct, Content, Context, Dict, Fold, Func, + NativeElement, Set, Smart, StyleChain, Value, +}; +use crate::layout::{ + Abs, Alignment, FlushElem, Frame, HAlignment, Length, OuterVAlignment, Ratio, Rel, + Sides, SpecificAlignment, +}; +use crate::model::Numbering; +use crate::visualize::{Color, Paint}; + +/// Layouts its child onto one or multiple pages. +/// +/// Although this function is primarily used in set rules to affect page +/// properties, it can also be used to explicitly render its argument onto +/// a set of pages of its own. +/// +/// Pages can be set to use `{auto}` as their width or height. In this case, the +/// pages will grow to fit their content on the respective axis. +/// +/// The [Guide for Page Setup]($guides/page-setup-guide) explains how to use +/// this and related functions to set up a document with many examples. +/// +/// # Example +/// ```example +/// >>> #set page(margin: auto) +/// #set page("us-letter") +/// +/// There you go, US friends! +/// ``` +#[elem(Construct)] +pub struct PageElem { + /// A standard paper size to set width and height. + /// + /// This is just a shorthand for setting `width` and `height` and, as such, + /// cannot be retrieved in a context expression. + #[external] + #[default(Paper::A4)] + pub paper: Paper, + + /// The width of the page. + /// + /// ```example + /// #set page( + /// width: 3cm, + /// margin: (x: 0cm), + /// ) + /// + /// #for i in range(3) { + /// box(square(width: 1cm)) + /// } + /// ``` + #[resolve] + #[parse( + let paper = args.named_or_find::<Paper>("paper")?; + args.named("width")? + .or_else(|| paper.map(|paper| Smart::Custom(paper.width().into()))) + )] + #[default(Smart::Custom(Paper::A4.width().into()))] + #[ghost] + pub width: Smart<Length>, + + /// The height of the page. + /// + /// If this is set to `{auto}`, page breaks can only be triggered manually + /// by inserting a [page break]($pagebreak). Most examples throughout this + /// documentation use `{auto}` for the height of the page to dynamically + /// grow and shrink to fit their content. + #[resolve] + #[parse( + args.named("height")? + .or_else(|| paper.map(|paper| Smart::Custom(paper.height().into()))) + )] + #[default(Smart::Custom(Paper::A4.height().into()))] + #[ghost] + pub height: Smart<Length>, + + /// Whether the page is flipped into landscape orientation. + /// + /// ```example + /// #set page( + /// "us-business-card", + /// flipped: true, + /// fill: rgb("f2e5dd"), + /// ) + /// + /// #set align(bottom + end) + /// #text(14pt)[*Sam H. Richards*] \ + /// _Procurement Manager_ + /// + /// #set text(10pt) + /// 17 Main Street \ + /// New York, NY 10001 \ + /// +1 555 555 5555 + /// ``` + #[default(false)] + #[ghost] + pub flipped: bool, + + /// The page's margins. + /// + /// - `{auto}`: The margins are set automatically to 2.5/21 times the smaller + /// dimension of the page. This results in 2.5cm margins for an A4 page. + /// - A single length: The same margin on all sides. + /// - A dictionary: With a dictionary, the margins can be set individually. + /// The dictionary can contain the following keys in order of precedence: + /// - `top`: The top margin. + /// - `right`: The right margin. + /// - `bottom`: The bottom margin. + /// - `left`: The left margin. + /// - `inside`: The margin at the inner side of the page (where the + /// [binding]($page.binding) is). + /// - `outside`: The margin at the outer side of the page (opposite to the + /// [binding]($page.binding)). + /// - `x`: The horizontal margins. + /// - `y`: The vertical margins. + /// - `rest`: The margins on all sides except those for which the + /// dictionary explicitly sets a size. + /// + /// The values for `left` and `right` are mutually exclusive with + /// the values for `inside` and `outside`. + /// + /// ```example + /// #set page( + /// width: 3cm, + /// height: 4cm, + /// margin: (x: 8pt, y: 4pt), + /// ) + /// + /// #rect( + /// width: 100%, + /// height: 100%, + /// fill: aqua, + /// ) + /// ``` + #[fold] + #[ghost] + pub margin: Margin, + + /// On which side the pages will be bound. + /// + /// - `{auto}`: Equivalent to `left` if the [text direction]($text.dir) + /// is left-to-right and `right` if it is right-to-left. + /// - `left`: Bound on the left side. + /// - `right`: Bound on the right side. + /// + /// This affects the meaning of the `inside` and `outside` options for + /// margins. + #[ghost] + pub binding: Smart<Binding>, + + /// How many columns the page has. + /// + /// If you need to insert columns into a page or other container, you can + /// also use the [`columns` function]($columns). + /// + /// ```example:single + /// #set page(columns: 2, height: 4.8cm) + /// Climate change is one of the most + /// pressing issues of our time, with + /// the potential to devastate + /// communities, ecosystems, and + /// economies around the world. It's + /// clear that we need to take urgent + /// action to reduce our carbon + /// emissions and mitigate the impacts + /// of a rapidly changing climate. + /// ``` + #[default(NonZeroUsize::ONE)] + #[ghost] + pub columns: NonZeroUsize, + + /// The page's background fill. + /// + /// Setting this to something non-transparent instructs the printer to color + /// the complete page. If you are considering larger production runs, it may + /// be more environmentally friendly and cost-effective to source pre-dyed + /// pages and not set this property. + /// + /// When set to `{none}`, the background becomes transparent. Note that PDF + /// pages will still appear with a (usually white) background in viewers, + /// but they are actually transparent. (If you print them, no color is used + /// for the background.) + /// + /// The default of `{auto}` results in `{none}` for PDF output, and + /// `{white}` for PNG and SVG. + /// + /// ```example + /// #set page(fill: rgb("444352")) + /// #set text(fill: rgb("fdfdfd")) + /// *Dark mode enabled.* + /// ``` + #[borrowed] + #[ghost] + pub fill: Smart<Option<Paint>>, + + /// How to [number]($numbering) the pages. + /// + /// If an explicit `footer` (or `header` for top-aligned numbering) is + /// given, the numbering is ignored. + /// + /// ```example + /// #set page( + /// height: 100pt, + /// margin: (top: 16pt, bottom: 24pt), + /// numbering: "1 / 1", + /// ) + /// + /// #lorem(48) + /// ``` + #[borrowed] + #[ghost] + pub numbering: Option<Numbering>, + + /// The alignment of the page numbering. + /// + /// If the vertical component is `top`, the numbering is placed into the + /// header and if it is `bottom`, it is placed in the footer. Horizon + /// alignment is forbidden. If an explicit matching `header` or `footer` is + /// given, the numbering is ignored. + /// + /// ```example + /// #set page( + /// margin: (top: 16pt, bottom: 24pt), + /// numbering: "1", + /// number-align: right, + /// ) + /// + /// #lorem(30) + /// ``` + #[default(SpecificAlignment::Both(HAlignment::Center, OuterVAlignment::Bottom))] + #[ghost] + pub number_align: SpecificAlignment<HAlignment, OuterVAlignment>, + + /// The page's header. Fills the top margin of each page. + /// + /// - Content: Shows the content as the header. + /// - `{auto}`: Shows the page number if a `numbering` is set and + /// `number-align` is `top`. + /// - `{none}`: Suppresses the header. + /// + /// ```example + /// #set par(justify: true) + /// #set page( + /// margin: (top: 32pt, bottom: 20pt), + /// header: [ + /// #set text(8pt) + /// #smallcaps[Typst Academcy] + /// #h(1fr) _Exercise Sheet 3_ + /// ], + /// ) + /// + /// #lorem(19) + /// ``` + #[borrowed] + #[ghost] + pub header: Smart<Option<Content>>, + + /// The amount the header is raised into the top margin. + #[resolve] + #[default(Ratio::new(0.3).into())] + #[ghost] + pub header_ascent: Rel<Length>, + + /// The page's footer. Fills the bottom margin of each page. + /// + /// - Content: Shows the content as the footer. + /// - `{auto}`: Shows the page number if a `numbering` is set and + /// `number-align` is `bottom`. + /// - `{none}`: Suppresses the footer. + /// + /// For just a page number, the `numbering` property typically suffices. If + /// you want to create a custom footer but still display the page number, + /// you can directly access the [page counter]($counter). + /// + /// ```example + /// #set par(justify: true) + /// #set page( + /// height: 100pt, + /// margin: 20pt, + /// footer: context [ + /// #set align(right) + /// #set text(8pt) + /// #counter(page).display( + /// "1 of I", + /// both: true, + /// ) + /// ] + /// ) + /// + /// #lorem(48) + /// ``` + #[borrowed] + #[ghost] + pub footer: Smart<Option<Content>>, + + /// The amount the footer is lowered into the bottom margin. + #[resolve] + #[default(Ratio::new(0.3).into())] + #[ghost] + pub footer_descent: Rel<Length>, + + /// Content in the page's background. + /// + /// This content will be placed behind the page's body. It can be + /// used to place a background image or a watermark. + /// + /// ```example + /// #set page(background: rotate(24deg, + /// text(18pt, fill: rgb("FFCBC4"))[ + /// *CONFIDENTIAL* + /// ] + /// )) + /// + /// = Typst's secret plans + /// In the year 2023, we plan to take + /// over the world (of typesetting). + /// ``` + #[borrowed] + #[ghost] + pub background: Option<Content>, + + /// Content in the page's foreground. + /// + /// This content will overlay the page's body. + /// + /// ```example + /// #set page(foreground: text(24pt)[🥸]) + /// + /// Reviewer 2 has marked our paper + /// "Weak Reject" because they did + /// not understand our approach... + /// ``` + #[borrowed] + #[ghost] + pub foreground: Option<Content>, + + /// The contents of the page(s). + /// + /// Multiple pages will be created if the content does not fit on a single + /// page. A new page with the page properties prior to the function invocation + /// will be created after the body has been typeset. + #[external] + #[required] + pub body: Content, +} + +impl Construct for PageElem { + fn construct(engine: &mut Engine, args: &mut Args) -> SourceResult<Content> { + // The page constructor is special: It doesn't create a page element. + // Instead, it just ensures that the passed content lives in a separate + // page and styles it. + let styles = Self::set(engine, args)?; + let body = args.expect::<Content>("body")?; + Ok(Content::sequence([ + PagebreakElem::shared_weak().clone(), + // We put an effectless, invisible non-tag element on the page. + // This has two desirable consequences: + // - The page is kept even if the body is empty + // - The page doesn't inherit shared styles from the body + FlushElem::new().pack(), + body, + PagebreakElem::shared_boundary().clone(), + ]) + .styled_with_map(styles)) + } +} + +/// A manual page break. +/// +/// Must not be used inside any containers. +/// +/// # Example +/// ```example +/// The next page contains +/// more details on compound theory. +/// #pagebreak() +/// +/// == Compound Theory +/// In 1984, the first ... +/// ``` +#[elem(title = "Page Break")] +pub struct PagebreakElem { + /// If `{true}`, the page break is skipped if the current page is already + /// empty. + #[default(false)] + pub weak: bool, + + /// If given, ensures that the next page will be an even/odd page, with an + /// empty page in between if necessary. + /// + /// ```example + /// #set page(height: 30pt) + /// + /// First. + /// #pagebreak(to: "odd") + /// Third. + /// ``` + pub to: Option<Parity>, + + /// Whether this pagebreak designates an end boundary of a page run. This is + /// an even weaker version of pagebreak `weak` because it not only doesn't + /// force an empty page, but also doesn't force its initial styles onto a + /// staged empty page. + #[internal] + #[parse(None)] + #[default(false)] + pub boundary: bool, +} + +impl PagebreakElem { + /// Get the globally shared weak pagebreak element. + pub fn shared_weak() -> &'static Content { + singleton!(Content, PagebreakElem::new().with_weak(true).pack()) + } + + /// Get the globally shared boundary pagebreak element. + pub fn shared_boundary() -> &'static Content { + singleton!( + Content, + PagebreakElem::new().with_weak(true).with_boundary(true).pack() + ) + } +} + +/// A finished page. +#[derive(Debug, Clone)] +pub struct Page { + /// The frame that defines the page. + pub frame: Frame, + /// How the page is filled. + /// + /// - When `None`, the background is transparent. + /// - When `Auto`, the background is transparent for PDF and white + /// for raster and SVG targets. + /// + /// Exporters should access the resolved value of this property through + /// `fill_or_transparent()` or `fill_or_white()`. + pub fill: Smart<Option<Paint>>, + /// The page's numbering. + pub numbering: Option<Numbering>, + /// The logical page number (controlled by `counter(page)` and may thus not + /// match the physical number). + pub number: usize, +} + +impl Page { + /// Get the configured background or `None` if it is `Auto`. + /// + /// This is used in PDF export. + pub fn fill_or_transparent(&self) -> Option<Paint> { + self.fill.clone().unwrap_or(None) + } + + /// Get the configured background or white if it is `Auto`. + /// + /// This is used in raster and SVG export. + pub fn fill_or_white(&self) -> Option<Paint> { + self.fill.clone().unwrap_or_else(|| Some(Color::WHITE.into())) + } +} + +/// Specification of the page's margins. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Margin { + /// The margins for each side. + pub sides: Sides<Option<Smart<Rel<Length>>>>, + /// Whether to swap `left` and `right` to make them `inside` and `outside` + /// (when to swap depends on the binding). + pub two_sided: Option<bool>, +} + +impl Margin { + /// Create an instance with four equal components. + pub fn splat(value: Option<Smart<Rel<Length>>>) -> Self { + Self { sides: Sides::splat(value), two_sided: None } + } +} + +impl Default for Margin { + fn default() -> Self { + Self { + sides: Sides::splat(Some(Smart::Auto)), + two_sided: None, + } + } +} + +impl Fold for Margin { + fn fold(self, outer: Self) -> Self { + Margin { + sides: self.sides.fold(outer.sides), + two_sided: self.two_sided.fold(outer.two_sided), + } + } +} + +cast! { + Margin, + self => { + let two_sided = self.two_sided.unwrap_or(false); + if !two_sided && self.sides.is_uniform() { + if let Some(left) = self.sides.left { + return left.into_value(); + } + } + + let mut dict = Dict::new(); + let mut handle = |key: &str, component: Option<Smart<Rel<Length>>>| { + if let Some(c) = component { + dict.insert(key.into(), c.into_value()); + } + }; + + handle("top", self.sides.top); + handle("bottom", self.sides.bottom); + if two_sided { + handle("inside", self.sides.left); + handle("outside", self.sides.right); + } else { + handle("left", self.sides.left); + handle("right", self.sides.right); + } + + Value::Dict(dict) + }, + _: AutoValue => Self::splat(Some(Smart::Auto)), + v: Rel<Length> => Self::splat(Some(Smart::Custom(v))), + mut dict: Dict => { + let mut take = |key| dict.take(key).ok().map(Value::cast).transpose(); + + let rest = take("rest")?; + let x = take("x")?.or(rest); + let y = take("y")?.or(rest); + let top = take("top")?.or(y); + let bottom = take("bottom")?.or(y); + let outside = take("outside")?; + let inside = take("inside")?; + let left = take("left")?; + let right = take("right")?; + + let implicitly_two_sided = outside.is_some() || inside.is_some(); + let implicitly_not_two_sided = left.is_some() || right.is_some(); + if implicitly_two_sided && implicitly_not_two_sided { + bail!("`inside` and `outside` are mutually exclusive with `left` and `right`"); + } + + // - If 'implicitly_two_sided' is false here, then + // 'implicitly_not_two_sided' will be guaranteed to be true + // due to the previous two 'if' conditions. + // - If both are false, this means that this margin change does not + // affect lateral margins, and thus shouldn't make a difference on + // the 'two_sided' attribute of this margin. + let two_sided = (implicitly_two_sided || implicitly_not_two_sided) + .then_some(implicitly_two_sided); + + dict.finish(&[ + "left", "top", "right", "bottom", "outside", "inside", "x", "y", "rest", + ])?; + + Margin { + sides: Sides { + left: inside.or(left).or(x), + top, + right: outside.or(right).or(x), + bottom, + }, + two_sided, + } + } +} + +/// Specification of the page's binding. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Binding { + /// Bound on the left, as customary in LTR languages. + Left, + /// Bound on the right, as customary in RTL languages. + Right, +} + +impl Binding { + /// Whether to swap left and right margin for the page with this number. + pub fn swap(self, number: NonZeroUsize) -> bool { + match self { + // Left-bound must swap on even pages + // (because it is correct on the first page). + Self::Left => number.get() % 2 == 0, + // Right-bound must swap on odd pages + // (because it is wrong on the first page). + Self::Right => number.get() % 2 == 1, + } + } +} + +cast! { + Binding, + self => match self { + Self::Left => Alignment::LEFT.into_value(), + Self::Right => Alignment::RIGHT.into_value(), + }, + v: Alignment => match v { + Alignment::LEFT => Self::Left, + Alignment::RIGHT => Self::Right, + _ => bail!("must be `left` or `right`"), + }, +} + +/// A header, footer, foreground or background definition. +#[derive(Debug, Clone, Hash)] +pub enum Marginal { + /// Bare content. + Content(Content), + /// A closure mapping from a page number to content. + Func(Func), +} + +impl Marginal { + /// Resolve the marginal based on the page number. + pub fn resolve( + &self, + engine: &mut Engine, + styles: StyleChain, + page: usize, + ) -> SourceResult<Cow<'_, Content>> { + Ok(match self { + Self::Content(content) => Cow::Borrowed(content), + Self::Func(func) => Cow::Owned( + func.call(engine, Context::new(None, Some(styles)).track(), [page])? + .display(), + ), + }) + } +} + +cast! { + Marginal, + self => match self { + Self::Content(v) => v.into_value(), + Self::Func(v) => v.into_value(), + }, + v: Content => Self::Content(v), + v: Func => Self::Func(v), +} + +/// A list of page ranges to be exported. +#[derive(Debug, Clone)] +pub struct PageRanges(Vec<PageRange>); + +/// A range of pages to export. +/// +/// The range is one-indexed. For example, `1..=3` indicates the first, second +/// and third pages should be exported. +pub type PageRange = RangeInclusive<Option<NonZeroUsize>>; + +impl PageRanges { + /// Create new page ranges. + pub fn new(ranges: Vec<PageRange>) -> Self { + Self(ranges) + } + + /// Check if a page, given its number, should be included when exporting the + /// document while restricting the exported pages to these page ranges. + /// This is the one-indexed version of 'includes_page_index'. + pub fn includes_page(&self, page: NonZeroUsize) -> bool { + self.includes_page_index(page.get() - 1) + } + + /// Check if a page, given its index, should be included when exporting the + /// document while restricting the exported pages to these page ranges. + /// This is the zero-indexed version of 'includes_page'. + pub fn includes_page_index(&self, page: usize) -> bool { + let page = NonZeroUsize::try_from(page + 1).unwrap(); + self.0.iter().any(|range| match (range.start(), range.end()) { + (Some(start), Some(end)) => (start..=end).contains(&&page), + (Some(start), None) => (start..).contains(&&page), + (None, Some(end)) => (..=end).contains(&&page), + (None, None) => true, + }) + } +} + +/// Whether something should be even or odd. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum Parity { + /// Next page will be an even page. + Even, + /// Next page will be an odd page. + Odd, +} + +impl Parity { + /// Whether the given number matches the parity. + pub fn matches(self, number: usize) -> bool { + match self { + Self::Even => number % 2 == 0, + Self::Odd => number % 2 == 1, + } + } +} + +/// Specification of a paper. +#[derive(Debug, Copy, Clone, Hash)] +pub struct Paper { + /// The name of the paper. + name: &'static str, + /// The width of the paper in millimeters. + width: Scalar, + /// The height of the paper in millimeters. + height: Scalar, +} + +impl Paper { + /// The width of the paper. + pub fn width(self) -> Abs { + Abs::mm(self.width.get()) + } + + /// The height of the paper. + pub fn height(self) -> Abs { + Abs::mm(self.height.get()) + } +} + +/// Defines paper constants and a paper parsing implementation. +macro_rules! papers { + ($(($var:ident: $width:expr, $height: expr, $name:literal))*) => { + /// Predefined papers. + /// + /// Each paper is parsable from its name in kebab-case. + impl Paper { + $(pub const $var: Self = Self { + name: $name, + width: Scalar::new($width), + height: Scalar::new($height), + };)* + } + + impl FromStr for Paper { + type Err = &'static str; + + fn from_str(name: &str) -> Result<Self, Self::Err> { + match name.to_lowercase().as_str() { + $($name => Ok(Self::$var),)* + _ => Err("unknown paper size"), + } + } + } + + cast! { + Paper, + self => self.name.into_value(), + $( + /// Produces a paper of the respective size. + $name => Self::$var, + )* + } + }; +} + +// All paper sizes in mm. +// +// Resources: +// - https://papersizes.io/ +// - https://en.wikipedia.org/wiki/Paper_size +// - https://www.theedkins.co.uk/jo/units/oldunits/print.htm +// - https://vintagepaper.co/blogs/news/traditional-paper-sizes +papers! { + // ---------------------------------------------------------------------- // + // ISO 216 A Series + (A0: 841.0, 1189.0, "a0") + (A1: 594.0, 841.0, "a1") + (A2: 420.0, 594.0, "a2") + (A3: 297.0, 420.0, "a3") + (A4: 210.0, 297.0, "a4") + (A5: 148.0, 210.0, "a5") + (A6: 105.0, 148.0, "a6") + (A7: 74.0, 105.0, "a7") + (A8: 52.0, 74.0, "a8") + (A9: 37.0, 52.0, "a9") + (A10: 26.0, 37.0, "a10") + (A11: 18.0, 26.0, "a11") + + // ISO 216 B Series + (ISO_B1: 707.0, 1000.0, "iso-b1") + (ISO_B2: 500.0, 707.0, "iso-b2") + (ISO_B3: 353.0, 500.0, "iso-b3") + (ISO_B4: 250.0, 353.0, "iso-b4") + (ISO_B5: 176.0, 250.0, "iso-b5") + (ISO_B6: 125.0, 176.0, "iso-b6") + (ISO_B7: 88.0, 125.0, "iso-b7") + (ISO_B8: 62.0, 88.0, "iso-b8") + + // ISO 216 C Series + (ISO_C3: 324.0, 458.0, "iso-c3") + (ISO_C4: 229.0, 324.0, "iso-c4") + (ISO_C5: 162.0, 229.0, "iso-c5") + (ISO_C6: 114.0, 162.0, "iso-c6") + (ISO_C7: 81.0, 114.0, "iso-c7") + (ISO_C8: 57.0, 81.0, "iso-c8") + + // DIN D Series (extension to ISO) + (DIN_D3: 272.0, 385.0, "din-d3") + (DIN_D4: 192.0, 272.0, "din-d4") + (DIN_D5: 136.0, 192.0, "din-d5") + (DIN_D6: 96.0, 136.0, "din-d6") + (DIN_D7: 68.0, 96.0, "din-d7") + (DIN_D8: 48.0, 68.0, "din-d8") + + // SIS (used in academia) + (SIS_G5: 169.0, 239.0, "sis-g5") + (SIS_E5: 115.0, 220.0, "sis-e5") + + // ANSI Extensions + (ANSI_A: 216.0, 279.0, "ansi-a") + (ANSI_B: 279.0, 432.0, "ansi-b") + (ANSI_C: 432.0, 559.0, "ansi-c") + (ANSI_D: 559.0, 864.0, "ansi-d") + (ANSI_E: 864.0, 1118.0, "ansi-e") + + // ANSI Architectural Paper + (ARCH_A: 229.0, 305.0, "arch-a") + (ARCH_B: 305.0, 457.0, "arch-b") + (ARCH_C: 457.0, 610.0, "arch-c") + (ARCH_D: 610.0, 914.0, "arch-d") + (ARCH_E1: 762.0, 1067.0, "arch-e1") + (ARCH_E: 914.0, 1219.0, "arch-e") + + // JIS B Series + (JIS_B0: 1030.0, 1456.0, "jis-b0") + (JIS_B1: 728.0, 1030.0, "jis-b1") + (JIS_B2: 515.0, 728.0, "jis-b2") + (JIS_B3: 364.0, 515.0, "jis-b3") + (JIS_B4: 257.0, 364.0, "jis-b4") + (JIS_B5: 182.0, 257.0, "jis-b5") + (JIS_B6: 128.0, 182.0, "jis-b6") + (JIS_B7: 91.0, 128.0, "jis-b7") + (JIS_B8: 64.0, 91.0, "jis-b8") + (JIS_B9: 45.0, 64.0, "jis-b9") + (JIS_B10: 32.0, 45.0, "jis-b10") + (JIS_B11: 22.0, 32.0, "jis-b11") + + // SAC D Series + (SAC_D0: 764.0, 1064.0, "sac-d0") + (SAC_D1: 532.0, 760.0, "sac-d1") + (SAC_D2: 380.0, 528.0, "sac-d2") + (SAC_D3: 264.0, 376.0, "sac-d3") + (SAC_D4: 188.0, 260.0, "sac-d4") + (SAC_D5: 130.0, 184.0, "sac-d5") + (SAC_D6: 92.0, 126.0, "sac-d6") + + // ISO 7810 ID + (ISO_ID_1: 85.6, 53.98, "iso-id-1") + (ISO_ID_2: 74.0, 105.0, "iso-id-2") + (ISO_ID_3: 88.0, 125.0, "iso-id-3") + + // ---------------------------------------------------------------------- // + // Asia + (ASIA_F4: 210.0, 330.0, "asia-f4") + + // Japan + (JP_SHIROKU_BAN_4: 264.0, 379.0, "jp-shiroku-ban-4") + (JP_SHIROKU_BAN_5: 189.0, 262.0, "jp-shiroku-ban-5") + (JP_SHIROKU_BAN_6: 127.0, 188.0, "jp-shiroku-ban-6") + (JP_KIKU_4: 227.0, 306.0, "jp-kiku-4") + (JP_KIKU_5: 151.0, 227.0, "jp-kiku-5") + (JP_BUSINESS_CARD: 91.0, 55.0, "jp-business-card") + + // China + (CN_BUSINESS_CARD: 90.0, 54.0, "cn-business-card") + + // Europe + (EU_BUSINESS_CARD: 85.0, 55.0, "eu-business-card") + + // French Traditional (AFNOR) + (FR_TELLIERE: 340.0, 440.0, "fr-tellière") + (FR_COURONNE_ECRITURE: 360.0, 460.0, "fr-couronne-écriture") + (FR_COURONNE_EDITION: 370.0, 470.0, "fr-couronne-édition") + (FR_RAISIN: 500.0, 650.0, "fr-raisin") + (FR_CARRE: 450.0, 560.0, "fr-carré") + (FR_JESUS: 560.0, 760.0, "fr-jésus") + + // United Kingdom Imperial + (UK_BRIEF: 406.4, 342.9, "uk-brief") + (UK_DRAFT: 254.0, 406.4, "uk-draft") + (UK_FOOLSCAP: 203.2, 330.2, "uk-foolscap") + (UK_QUARTO: 203.2, 254.0, "uk-quarto") + (UK_CROWN: 508.0, 381.0, "uk-crown") + (UK_BOOK_A: 111.0, 178.0, "uk-book-a") + (UK_BOOK_B: 129.0, 198.0, "uk-book-b") + + // Unites States + (US_LETTER: 215.9, 279.4, "us-letter") + (US_LEGAL: 215.9, 355.6, "us-legal") + (US_TABLOID: 279.4, 431.8, "us-tabloid") + (US_EXECUTIVE: 84.15, 266.7, "us-executive") + (US_FOOLSCAP_FOLIO: 215.9, 342.9, "us-foolscap-folio") + (US_STATEMENT: 139.7, 215.9, "us-statement") + (US_LEDGER: 431.8, 279.4, "us-ledger") + (US_OFICIO: 215.9, 340.36, "us-oficio") + (US_GOV_LETTER: 203.2, 266.7, "us-gov-letter") + (US_GOV_LEGAL: 215.9, 330.2, "us-gov-legal") + (US_BUSINESS_CARD: 88.9, 50.8, "us-business-card") + (US_DIGEST: 139.7, 215.9, "us-digest") + (US_TRADE: 152.4, 228.6, "us-trade") + + // ---------------------------------------------------------------------- // + // Other + (NEWSPAPER_COMPACT: 280.0, 430.0, "newspaper-compact") + (NEWSPAPER_BERLINER: 315.0, 470.0, "newspaper-berliner") + (NEWSPAPER_BROADSHEET: 381.0, 578.0, "newspaper-broadsheet") + (PRESENTATION_16_9: 297.0, 167.0625, "presentation-16-9") + (PRESENTATION_4_3: 280.0, 210.0, "presentation-4-3") +} diff --git a/crates/typst-library/src/layout/place.rs b/crates/typst-library/src/layout/place.rs new file mode 100644 index 00000000..bedeb507 --- /dev/null +++ b/crates/typst-library/src/layout/place.rs @@ -0,0 +1,208 @@ +use crate::foundations::{elem, scope, Cast, Content, Packed, Smart}; +use crate::introspection::{Locatable, Unqueriable}; +use crate::layout::{Alignment, Em, Length, Rel}; + +/// Places content relatively to its parent container. +/// +/// Placed content can be either overlaid (the default) or floating. Overlaid +/// content is aligned with the parent container according to the given +/// [`alignment`]($place.alignment), and shown over any other content added so +/// far in the container. Floating content is placed at the top or bottom of +/// the container, displacing other content down or up respectively. In both +/// cases, the content position can be adjusted with [`dx`]($place.dx) and +/// [`dy`]($place.dy) offsets without affecting the layout. +/// +/// The parent can be any container such as a [`block`], [`box`], +/// [`rect`], etc. A top level `place` call will place content directly +/// in the text area of the current page. This can be used for absolute +/// positioning on the page: with a `top + left` +/// [`alignment`]($place.alignment), the offsets `dx` and `dy` will set the +/// position of the element's top left corner relatively to the top left corner +/// of the text area. For absolute positioning on the full page including +/// margins, you can use `place` in [`page.foreground`]($page.foreground) or +/// [`page.background`]($page.background). +/// +/// # Examples +/// ```example +/// #set page(height: 120pt) +/// Hello, world! +/// +/// #rect( +/// width: 100%, +/// height: 2cm, +/// place(horizon + right, square()), +/// ) +/// +/// #place( +/// top + left, +/// dx: -5pt, +/// square(size: 5pt, fill: red), +/// ) +/// ``` +/// +/// # Effect on the position of other elements { #effect-on-other-elements } +/// Overlaid elements don't take space in the flow of content, but a `place` +/// call inserts an invisible block-level element in the flow. This can +/// affect the layout by breaking the current paragraph. To avoid this, +/// you can wrap the `place` call in a [`box`] when the call is made +/// in the middle of a paragraph. The alignment and offsets will then be +/// relative to this zero-size box. To make sure it doesn't interfere with +/// spacing, the box should be attached to a word using a word joiner. +/// +/// For example, the following defines a function for attaching an annotation +/// to the following word: +/// +/// ```example +/// >>> #set page(height: 70pt) +/// #let annotate(..args) = { +/// box(place(..args)) +/// sym.wj +/// h(0pt, weak: true) +/// } +/// +/// A placed #annotate(square(), dy: 2pt) +/// square in my text. +/// ``` +/// +/// The zero-width weak spacing serves to discard spaces between the function +/// call and the next word. +#[elem(scope, Locatable, Unqueriable)] +pub struct PlaceElem { + /// Relative to which position in the parent container to place the content. + /// + /// - If `float` is `{false}`, then this can be any alignment other than `{auto}`. + /// - If `float` is `{true}`, then this must be `{auto}`, `{top}`, or `{bottom}`. + /// + /// When `float` is `{false}` and no vertical alignment is specified, the + /// content is placed at the current position on the vertical axis. + #[positional] + #[default(Smart::Custom(Alignment::START))] + pub alignment: Smart<Alignment>, + + /// Relative to which containing scope something is placed. + /// + /// The parent scope is primarily used with figures and, for + /// this reason, the figure function has a mirrored [`scope` + /// parameter]($figure.scope). Nonetheless, it can also be more generally + /// useful to break out of the columns. A typical example would be to + /// [create a single-column title section]($guides/page-setup-guide/#columns) + /// in a two-column document. + /// + /// Note that parent-scoped placement is currently only supported if `float` + /// is `{true}`. This may change in the future. + /// + /// ```example + /// #set page(height: 150pt, columns: 2) + /// #place( + /// top + center, + /// scope: "parent", + /// float: true, + /// rect(width: 80%, fill: aqua), + /// ) + /// + /// #lorem(25) + /// ``` + pub scope: PlacementScope, + + /// Whether the placed element has floating layout. + /// + /// Floating elements are positioned at the top or bottom of the parent + /// container, displacing in-flow content. They are always placed in the + /// in-flow order relative to each other, as well as before any content + /// following a later [`place.flush`] element. + /// + /// ```example + /// #set page(height: 150pt) + /// #let note(where, body) = place( + /// center + where, + /// float: true, + /// clearance: 6pt, + /// rect(body), + /// ) + /// + /// #lorem(10) + /// #note(bottom)[Bottom 1] + /// #note(bottom)[Bottom 2] + /// #lorem(40) + /// #note(top)[Top] + /// #lorem(10) + /// ``` + pub float: bool, + + /// The spacing between the placed element and other elements in a floating + /// layout. + /// + /// Has no effect if `float` is `{false}`. + #[default(Em::new(1.5).into())] + #[resolve] + pub clearance: Length, + + /// The horizontal displacement of the placed content. + /// + /// ```example + /// #set page(height: 100pt) + /// #for i in range(16) { + /// let amount = i * 4pt + /// place(center, dx: amount - 32pt, dy: amount)[A] + /// } + /// ``` + /// + /// This does not affect the layout of in-flow content. + /// In other words, the placed content is treated as if it + /// were wrapped in a [`move`] element. + pub dx: Rel<Length>, + + /// The vertical displacement of the placed content. + /// + /// This does not affect the layout of in-flow content. + /// In other words, the placed content is treated as if it + /// were wrapped in a [`move`] element. + pub dy: Rel<Length>, + + /// The content to place. + #[required] + pub body: Content, +} + +/// `PlaceElem` must be locatable to support logical ordering of floats, but I +/// do not want to expose `query(place)` for now. +impl Unqueriable for Packed<PlaceElem> {} + +#[scope] +impl PlaceElem { + #[elem] + type FlushElem; +} + +/// Relative to which containing scope something shall be placed. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum PlacementScope { + /// Place into the current column. + #[default] + Column, + /// Place relative to the parent, letting the content span over all columns. + Parent, +} + +/// Asks the layout algorithm to place pending floating elements before +/// continuing with the content. +/// +/// This is useful for preventing floating figures from spilling +/// into the next section. +/// +/// ```example +/// >>> #set page(height: 160pt, width: 150pt) +/// #lorem(15) +/// +/// #figure( +/// rect(width: 100%, height: 50pt), +/// placement: auto, +/// caption: [A rectangle], +/// ) +/// +/// #place.flush() +/// +/// This text appears after the figure. +/// ``` +#[elem] +pub struct FlushElem {} diff --git a/crates/typst-library/src/layout/point.rs b/crates/typst-library/src/layout/point.rs new file mode 100644 index 00000000..c3d1594e --- /dev/null +++ b/crates/typst-library/src/layout/point.rs @@ -0,0 +1,168 @@ +use std::fmt::{self, Debug, Formatter}; +use std::ops::{Add, Div, Mul, Neg}; + +use typst_utils::{Get, Numeric}; + +use crate::layout::{Abs, Axis, Size, Transform}; + +/// 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) } + } + + /// Maps the point with the given function. + pub fn map(self, f: impl Fn(Abs) -> Abs) -> Self { + Self { x: f(self.x), y: f(self.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. + /// + /// In the event that one of the coordinates is infinite, the result will + /// be zero. + 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, + ) + } + + /// Transforms the point with the given transformation, without accounting + /// for infinite values. + pub fn transform_inf(self, ts: Transform) -> Self { + Self::new( + ts.sx.get() * self.x + ts.kx.get() * self.y + ts.tx, + ts.ky.get() * self.x + ts.sy.get() * 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 } + } +} + +typst_utils::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 } + } +} + +typst_utils::assign_impl!(Point += Point); +typst_utils::assign_impl!(Point -= Point); +typst_utils::assign_impl!(Point *= f64); +typst_utils::assign_impl!(Point /= f64); diff --git a/crates/typst-library/src/layout/ratio.rs b/crates/typst-library/src/layout/ratio.rs new file mode 100644 index 00000000..1c0dcd29 --- /dev/null +++ b/crates/typst-library/src/layout/ratio.rs @@ -0,0 +1,153 @@ +use std::fmt::{self, Debug, Formatter}; +use std::ops::{Add, Div, Mul, Neg}; + +use ecow::EcoString; +use typst_utils::{Numeric, Scalar}; + +use crate::foundations::{repr, ty, Repr}; + +/// A ratio of a whole. +/// +/// Written as a number, followed by a percent sign. +/// +/// # Example +/// ```example +/// #set align(center) +/// #scale(x: 150%)[ +/// Scaled apart. +/// ] +/// ``` +#[ty(cast)] +#[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::ZERO) + } + + /// A ratio of `100%` represented as `1.0`. + pub const fn one() -> Self { + Self(Scalar::ONE) + } + + /// Create a new ratio from a value, where `1.0` means `100%`. + pub const fn new(ratio: f64) -> Self { + Self(Scalar::new(ratio)) + } + + /// Get the underlying ratio. + pub const fn get(self) -> f64 { + (self.0).get() + } + + /// 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, "{:?}%", self.get() * 100.0) + } +} + +impl Repr for Ratio { + fn repr(&self) -> EcoString { + repr::format_float_with_unit(self.get() * 100.0, "%") + } +} + +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) + } +} + +typst_utils::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 for Ratio { + type Output = f64; + + fn div(self, other: Self) -> f64 { + self.get() / other.get() + } +} + +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() + } +} + +typst_utils::assign_impl!(Ratio += Ratio); +typst_utils::assign_impl!(Ratio -= Ratio); +typst_utils::assign_impl!(Ratio *= Ratio); +typst_utils::assign_impl!(Ratio *= f64); +typst_utils::assign_impl!(Ratio /= f64); diff --git a/crates/typst-library/src/layout/regions.rs b/crates/typst-library/src/layout/regions.rs new file mode 100644 index 00000000..385664bb --- /dev/null +++ b/crates/typst-library/src/layout/regions.rs @@ -0,0 +1,159 @@ +use std::fmt::{self, Debug, Formatter}; + +use crate::layout::{Abs, Axes, Size}; + +/// A single region to layout into. +#[derive(Debug, Copy, Clone, Hash)] +pub struct Region { + /// The size of the region. + pub size: Size, + /// Whether elements should expand to fill the regions instead of shrinking + /// to fit the content. + pub expand: Axes<bool>, +} + +impl Region { + /// Create a new region. + pub fn new(size: Size, expand: Axes<bool>) -> Self { + Self { size, expand } + } +} + +impl From<Region> for Regions<'_> { + fn from(region: Region) -> Self { + Regions { + size: region.size, + expand: region.expand, + full: region.size.y, + backlog: &[], + last: None, + } + } +} + +/// A sequence of regions to layout into. +/// +/// A *region* is a contiguous rectangular space in which elements +/// can be laid out. All regions within a `Regions` object have the +/// same width, namely `self.size.x`. This means that it is not +/// currently possible to, for instance, have content wrap to the +/// side of a floating element. +#[derive(Copy, Clone, Hash)] +pub struct Regions<'a> { + /// The remaining size of the first region. + pub size: Size, + /// Whether elements should expand to fill the regions instead of shrinking + /// to fit the content. + pub expand: Axes<bool>, + /// The full height of the region for relative sizing. + pub full: Abs, + /// The height of followup regions. The width is the same for all regions. + pub backlog: &'a [Abs], + /// The height of the final region that is repeated once the backlog is + /// drained. The width is the same for all regions. + pub last: Option<Abs>, +} + +impl Regions<'_> { + /// Create a new sequence of same-size regions that repeats indefinitely. + pub fn repeat(size: Size, expand: Axes<bool>) -> Self { + Self { + size, + full: size.y, + backlog: &[], + last: Some(size.y), + expand, + } + } + + /// The base size, which doesn't take into account that the regions is + /// already partially used up. + /// + /// This is also used for relative sizing. + pub fn base(&self) -> Size { + Size::new(self.size.x, self.full) + } + + /// Create new regions where all sizes are mapped with `f`. + /// + /// Note that since all regions must have the same width, the width returned + /// by `f` is ignored for the backlog and the final region. + pub fn map<'v, F>(&self, backlog: &'v mut Vec<Abs>, mut f: F) -> Regions<'v> + where + F: FnMut(Size) -> Size, + { + let x = self.size.x; + backlog.clear(); + backlog.extend(self.backlog.iter().map(|&y| f(Size::new(x, y)).y)); + Regions { + size: f(self.size), + full: f(Size::new(x, self.full)).y, + backlog, + last: self.last.map(|y| f(Size::new(x, y)).y), + expand: self.expand, + } + } + + /// Whether the first region is full and a region break is called for. + pub fn is_full(&self) -> bool { + Abs::zero().fits(self.size.y) && self.may_progress() + } + + /// Whether a region break is permitted. + pub fn may_break(&self) -> bool { + !self.backlog.is_empty() || self.last.is_some() + } + + /// Whether calling `next()` may improve a situation where there is a lack + /// of space. + pub fn may_progress(&self) -> bool { + !self.backlog.is_empty() || self.last.is_some_and(|height| self.size.y != height) + } + + /// Advance to the next region if there is any. + pub fn next(&mut self) { + if let Some(height) = self + .backlog + .split_first() + .map(|(first, tail)| { + self.backlog = tail; + *first + }) + .or(self.last) + { + self.size.y = height; + self.full = height; + } + } + + /// An iterator that returns the sizes of the first and all following + /// regions, equivalently to what would be produced by calling + /// [`next()`](Self::next) repeatedly until all regions are exhausted. + /// This iterator may be infinite. + pub fn iter(&self) -> impl Iterator<Item = Size> + '_ { + let first = std::iter::once(self.size); + let backlog = self.backlog.iter(); + let last = self.last.iter().cycle(); + first.chain(backlog.chain(last).map(|&h| Size::new(self.size.x, h))) + } +} + +impl Debug for Regions<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("Regions ")?; + let mut list = f.debug_list(); + let mut prev = self.size.y; + list.entry(&self.size); + for &height in self.backlog { + list.entry(&Size::new(self.size.x, height)); + prev = height; + } + if let Some(last) = self.last { + if last != prev { + list.entry(&Size::new(self.size.x, last)); + } + list.entry(&(..)); + } + list.finish() + } +} diff --git a/crates/typst-library/src/layout/rel.rs b/crates/typst-library/src/layout/rel.rs new file mode 100644 index 00000000..30441a42 --- /dev/null +++ b/crates/typst-library/src/layout/rel.rs @@ -0,0 +1,276 @@ +use std::cmp::Ordering; +use std::fmt::{self, Debug, Formatter}; +use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; + +use ecow::{eco_format, EcoString}; +use typst_utils::Numeric; + +use crate::foundations::{cast, ty, Fold, Repr, Resolve, StyleChain}; +use crate::layout::{Abs, Em, Length, Ratio}; + +/// A length in relation to some known length. +/// +/// This type is a combination of a [length] with a [ratio]. It results from +/// addition and subtraction of a length and a ratio. Wherever a relative length +/// is expected, you can also use a bare length or ratio. +/// +/// # Example +/// ```example +/// #rect(width: 100% - 50pt) +/// +/// #(100% - 50pt).length \ +/// #(100% - 50pt).ratio +/// ``` +/// +/// A relative length has the following fields: +/// - `length`: Its length component. +/// - `ratio`: Its ratio component. +#[ty(cast, name = "relative", title = "Relative Length")] +#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Rel<T: Numeric = Length> { + /// 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 + } + } + + /// Convert to a relative length with the absolute part resolved at the + /// given font size. + pub fn at(self, font_size: Abs) -> Rel<Abs> { + self.map(|abs| abs.at(font_size)) + } +} + +impl<T: Numeric + Debug> 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<T: Numeric + Repr> Repr for Rel<T> { + fn repr(&self) -> EcoString { + eco_format!("{} + {}", self.rel.repr(), self.abs.repr()) + } +} + +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<T> Fold for Rel<T> +where + T: Numeric + Fold, +{ + fn fold(self, outer: Self) -> Self { + Self { rel: self.rel, abs: self.abs.fold(outer.abs) } + } +} + +cast! { + Rel<Abs>, + self => self.map(Length::from).into_value(), +} diff --git a/crates/typst-library/src/layout/repeat.rs b/crates/typst-library/src/layout/repeat.rs new file mode 100644 index 00000000..e423410a --- /dev/null +++ b/crates/typst-library/src/layout/repeat.rs @@ -0,0 +1,49 @@ +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain}; +use crate::layout::{BlockElem, Length}; + +/// Repeats content to the available space. +/// +/// This can be useful when implementing a custom index, reference, or outline. +/// +/// Space may be inserted between the instances of the body parameter, so be +/// sure to adjust the [`justify`]($repeat.justify) parameter accordingly. +/// +/// Errors if there no bounds on the available space, as it would create +/// infinite content. +/// +/// # Example +/// ```example +/// Sign on the dotted line: +/// #box(width: 1fr, repeat[.]) +/// +/// #set text(10pt) +/// #v(8pt, weak: true) +/// #align(right)[ +/// Berlin, the 22nd of December, 2022 +/// ] +/// ``` +#[elem(Show)] +pub struct RepeatElem { + /// The content to repeat. + #[required] + pub body: Content, + + /// The gap between each instance of the body. + #[default] + pub gap: Length, + + /// Whether to increase the gap between instances to completely fill the + /// available space. + #[default(true)] + pub justify: bool, +} + +impl Show for Packed<RepeatElem> { + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_repeat) + .pack() + .spanned(self.span())) + } +} diff --git a/crates/typst-library/src/layout/sides.rs b/crates/typst-library/src/layout/sides.rs new file mode 100644 index 00000000..e04b63d9 --- /dev/null +++ b/crates/typst-library/src/layout/sides.rs @@ -0,0 +1,343 @@ +use std::fmt::{self, Debug, Formatter}; +use std::ops::Add; + +use typst_utils::Get; + +use crate::diag::{bail, HintedStrResult}; +use crate::foundations::{ + cast, AlternativeFold, CastInfo, Dict, Fold, FromValue, IntoValue, Reflect, Resolve, + StyleChain, Value, +}; +use crate::layout::{Abs, Alignment, Axes, Axis, Corner, Rel, Size}; + +/// A container with left, top, right and bottom components. +#[derive(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), + } + } + + /// Convert from `&Sides<T>` to `Sides<&T>`. + pub fn as_ref(&self) -> Sides<&T> { + Sides { + left: &self.left, + top: &self.top, + right: &self.right, + bottom: &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<T> Sides<Option<T>> { + /// Unwrap-or-default the individual sides. + pub fn unwrap_or_default(self) -> Sides<T> + where + T: Default, + { + self.map(Option::unwrap_or_default) + } +} + +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), + } + } + + /// Whether all sides are zero. + pub fn is_zero(&self) -> bool { + self.left.is_zero() + && self.top.is_zero() + && self.right.is_zero() + && self.bottom.is_zero() + } +} + +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, + } + } +} + +impl<T: Debug + PartialEq> Debug for Sides<T> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if self.is_uniform() { + f.write_str("Sides::splat(")?; + self.left.fmt(f)?; + f.write_str(")") + } else { + f.debug_struct("Sides") + .field("left", &self.left) + .field("top", &self.top) + .field("right", &self.right) + .field("bottom", &self.bottom) + .finish() + } + } +} + +impl<T: Reflect> Reflect for Sides<Option<T>> { + fn input() -> CastInfo { + T::input() + Dict::input() + } + + fn output() -> CastInfo { + T::output() + Dict::output() + } + + fn castable(value: &Value) -> bool { + Dict::castable(value) || T::castable(value) + } +} + +impl<T> IntoValue for Sides<Option<T>> +where + T: PartialEq + IntoValue, +{ + fn into_value(self) -> Value { + if self.is_uniform() { + if let Some(left) = self.left { + return left.into_value(); + } + } + + let mut dict = Dict::new(); + let mut handle = |key: &str, component: Option<T>| { + if let Some(c) = component { + dict.insert(key.into(), c.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) -> HintedStrResult<Self> { + let expected_keys = ["left", "top", "right", "bottom", "x", "y", "rest"]; + if let Value::Dict(dict) = &mut value { + if dict.is_empty() { + return Ok(Self::splat(None)); + } else if dict.iter().any(|(key, _)| expected_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(&expected_keys)?; + return Ok(sides); + } + } + + if T::castable(&value) { + Ok(Self::splat(Some(T::from_value(value)?))) + } else if let Value::Dict(dict) = &value { + let keys = dict.iter().map(|kv| kv.0.as_str()).collect(); + // Do not hint at expected_keys, because T may be castable from Dict + // objects with other sets of expected keys. + Err(Dict::unexpected_keys(keys, None).into()) + } 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>> { + fn fold(self, outer: Self) -> Self { + // Usually, folding an inner `None` with an `outer` prefers the + // explicit `None`. However, here `None` means unspecified and thus + // we want `outer`, so we use `fold_or` to opt into such behavior. + self.zip(outer).map(|(inner, outer)| inner.fold_or(outer)) + } +} + +/// 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, + } + } +} + +cast! { + Side, + self => Alignment::from(self).into_value(), + align: Alignment => match align { + Alignment::LEFT => Self::Left, + Alignment::RIGHT => Self::Right, + Alignment::TOP => Self::Top, + Alignment::BOTTOM => Self::Bottom, + _ => bail!("cannot convert this alignment to a side"), + }, +} diff --git a/crates/typst-library/src/layout/size.rs b/crates/typst-library/src/layout/size.rs new file mode 100644 index 00000000..294eec66 --- /dev/null +++ b/crates/typst-library/src/layout/size.rs @@ -0,0 +1,87 @@ +use std::ops::{Add, Div, Mul, Neg}; + +use typst_utils::Numeric; + +use crate::layout::{Abs, Axes, Point, Ratio}; + +/// 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) + } + + /// Converts to a ratio of width to height. + pub fn aspect_ratio(self) -> Ratio { + Ratio::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 } + } +} + +typst_utils::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 } + } +} + +typst_utils::assign_impl!(Size -= Size); +typst_utils::assign_impl!(Size += Size); +typst_utils::assign_impl!(Size *= f64); +typst_utils::assign_impl!(Size /= f64); diff --git a/crates/typst-library/src/layout/spacing.rs b/crates/typst-library/src/layout/spacing.rs new file mode 100644 index 00000000..b3ca1e81 --- /dev/null +++ b/crates/typst-library/src/layout/spacing.rs @@ -0,0 +1,193 @@ +use typst_utils::Numeric; + +use crate::foundations::{cast, elem, Content}; +use crate::layout::{Abs, Em, Fr, Length, Ratio, Rel}; + +/// Inserts horizontal spacing into a paragraph. +/// +/// The spacing can be absolute, relative, or fractional. In the last case, the +/// remaining space on the line is distributed among all fractional spacings +/// according to their relative fractions. +/// +/// # Example +/// ```example +/// First #h(1cm) Second \ +/// First #h(30%) Second +/// ``` +/// +/// # Fractional spacing +/// With fractional spacing, you can align things within a line without forcing +/// a paragraph break (like [`align`] would). Each fractionally sized element +/// gets space based on the ratio of its fraction to the sum of all fractions. +/// +/// ```example +/// First #h(1fr) Second \ +/// First #h(1fr) Second #h(1fr) Third \ +/// First #h(2fr) Second #h(1fr) Third +/// ``` +/// +/// # Mathematical Spacing { #math-spacing } +/// In [mathematical formulas]($category/math), you can additionally use these +/// constants to add spacing between elements: `thin` (1/6 em), `med` (2/9 em), +/// `thick` (5/18 em), `quad` (1 em), `wide` (2 em). +#[elem(title = "Spacing (H)")] +pub struct HElem { + /// How much spacing to insert. + #[required] + pub amount: Spacing, + + /// If `{true}`, the spacing collapses at the start or end of a paragraph. + /// Moreover, from multiple adjacent weak spacings all but the largest one + /// collapse. + /// + /// Weak spacing in markup also causes all adjacent markup spaces to be + /// removed, regardless of the amount of spacing inserted. To force a space + /// next to weak spacing, you can explicitly write `[#" "]` (for a normal + /// space) or `[~]` (for a non-breaking space). The latter can be useful to + /// create a construct that always attaches to the preceding word with one + /// non-breaking space, independently of whether a markup space existed in + /// front or not. + /// + /// ```example + /// #h(1cm, weak: true) + /// We identified a group of _weak_ + /// specimens that fail to manifest + /// in most cases. However, when + /// #h(8pt, weak: true) supported + /// #h(8pt, weak: true) on both sides, + /// they do show up. + /// + /// Further #h(0pt, weak: true) more, + /// even the smallest of them swallow + /// adjacent markup spaces. + /// ``` + #[default(false)] + pub weak: bool, +} + +impl HElem { + /// Zero-width horizontal weak spacing that eats surrounding spaces. + pub fn hole() -> Self { + Self::new(Abs::zero().into()).with_weak(true) + } +} + +/// Inserts vertical spacing into a flow of blocks. +/// +/// The spacing can be absolute, relative, or fractional. In the last case, +/// the remaining space on the page is distributed among all fractional spacings +/// according to their relative fractions. +/// +/// # Example +/// ```example +/// #grid( +/// rows: 3cm, +/// columns: 6, +/// gutter: 1fr, +/// [A #parbreak() B], +/// [A #v(0pt) B], +/// [A #v(10pt) B], +/// [A #v(0pt, weak: true) B], +/// [A #v(40%, weak: true) B], +/// [A #v(1fr) B], +/// ) +/// ``` +#[elem(title = "Spacing (V)")] +pub struct VElem { + /// How much spacing to insert. + #[required] + pub amount: Spacing, + + /// If `{true}`, the spacing collapses at the start or end of a flow. + /// Moreover, from multiple adjacent weak spacings all but the largest one + /// collapse. Weak spacings will always collapse adjacent paragraph spacing, + /// even if the paragraph spacing is larger. + /// + /// ```example + /// The following theorem is + /// foundational to the field: + /// #v(4pt, weak: true) + /// $ x^2 + y^2 = r^2 $ + /// #v(4pt, weak: true) + /// The proof is simple: + /// ``` + pub weak: bool, + + /// Whether the spacing collapses if not immediately preceded by a + /// paragraph. + #[internal] + #[parse(Some(false))] + pub attach: bool, +} + +cast! { + VElem, + v: Content => v.unpack::<Self>().map_err(|_| "expected `v` element")?, +} + +/// Kinds of spacing. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Spacing { + /// Spacing specified in absolute terms and relative to the parent's size. + Rel(Rel<Length>), + /// Spacing specified as a fraction of the remaining free space in the + /// parent. + Fr(Fr), +} + +impl Spacing { + /// Whether this is fractional spacing. + pub fn is_fractional(self) -> bool { + matches!(self, Self::Fr(_)) + } + + /// Whether the spacing is actually no spacing. + pub fn is_zero(&self) -> bool { + match self { + Self::Rel(rel) => rel.is_zero(), + Self::Fr(fr) => fr.is_zero(), + } + } +} + +impl From<Abs> for Spacing { + fn from(abs: Abs) -> Self { + Self::Rel(abs.into()) + } +} + +impl From<Em> for Spacing { + fn from(em: Em) -> Self { + Self::Rel(Rel::new(Ratio::zero(), em.into())) + } +} + +impl From<Length> for Spacing { + fn from(length: Length) -> Self { + Self::Rel(length.into()) + } +} + +impl From<Fr> for Spacing { + fn from(fr: Fr) -> Self { + Self::Fr(fr) + } +} + +cast! { + Spacing, + self => match self { + Self::Rel(rel) => { + if rel.rel.is_zero() { + rel.abs.into_value() + } else if rel.abs.is_zero() { + rel.rel.into_value() + } else { + rel.into_value() + } + } + Self::Fr(fr) => fr.into_value(), + }, + v: Rel<Length> => Self::Rel(v), + v: Fr => Self::Fr(v), +} diff --git a/crates/typst-library/src/layout/stack.rs b/crates/typst-library/src/layout/stack.rs new file mode 100644 index 00000000..5fc78480 --- /dev/null +++ b/crates/typst-library/src/layout/stack.rs @@ -0,0 +1,84 @@ +use std::fmt::{self, Debug, Formatter}; + +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{cast, elem, Content, NativeElement, Packed, Show, StyleChain}; +use crate::layout::{BlockElem, Dir, Spacing}; + +/// Arranges content and spacing horizontally or vertically. +/// +/// The stack places a list of items along an axis, with optional spacing +/// between each item. +/// +/// # Example +/// ```example +/// #stack( +/// dir: ttb, +/// rect(width: 40pt), +/// rect(width: 120pt), +/// rect(width: 90pt), +/// ) +/// ``` +#[elem(Show)] +pub struct StackElem { + /// The direction along which the items are stacked. Possible values are: + /// + /// - `{ltr}`: Left to right. + /// - `{rtl}`: Right to left. + /// - `{ttb}`: Top to bottom. + /// - `{btt}`: Bottom to top. + /// + /// You can use the `start` and `end` methods to obtain the initial and + /// final points (respectively) of a direction, as `alignment`. You can also + /// use the `axis` method to determine whether a direction is + /// `{"horizontal"}` or `{"vertical"}`. The `inv` method returns a + /// direction's inverse direction. + /// + /// For example, `{ttb.start()}` is `top`, `{ttb.end()}` is `bottom`, + /// `{ttb.axis()}` is `{"vertical"}` and `{ttb.inv()}` is equal to `btt`. + #[default(Dir::TTB)] + pub dir: Dir, + + /// Spacing to insert between items where no explicit spacing was provided. + pub spacing: Option<Spacing>, + + /// The children to stack along the axis. + #[variadic] + pub children: Vec<StackChild>, +} + +impl Show for Packed<StackElem> { + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_stack) + .pack() + .spanned(self.span())) + } +} + +/// A child of a stack element. +#[derive(Clone, PartialEq, Hash)] +pub enum StackChild { + /// Spacing between other children. + Spacing(Spacing), + /// Arbitrary block-level content. + Block(Content), +} + +impl Debug for StackChild { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Spacing(kind) => kind.fmt(f), + Self::Block(block) => block.fmt(f), + } + } +} + +cast! { + StackChild, + self => match self { + Self::Spacing(spacing) => spacing.into_value(), + Self::Block(content) => content.into_value(), + }, + v: Spacing => Self::Spacing(v), + v: Content => Self::Block(v), +} diff --git a/crates/typst-library/src/layout/transform.rs b/crates/typst-library/src/layout/transform.rs new file mode 100644 index 00000000..183df609 --- /dev/null +++ b/crates/typst-library/src/layout/transform.rs @@ -0,0 +1,408 @@ +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, Content, NativeElement, Packed, Show, Smart, StyleChain, +}; +use crate::layout::{ + Abs, Alignment, Angle, BlockElem, HAlignment, Length, Ratio, Rel, VAlignment, +}; + +/// Moves content without affecting layout. +/// +/// The `move` function allows you to move content while the layout still 'sees' +/// it at the original positions. Containers will still be sized as if the +/// content was not moved. +/// +/// # Example +/// ```example +/// #rect(inset: 0pt, move( +/// dx: 6pt, dy: 6pt, +/// rect( +/// inset: 8pt, +/// fill: white, +/// stroke: black, +/// [Abra cadabra] +/// ) +/// )) +/// ``` +#[elem(Show)] +pub struct MoveElem { + /// The horizontal displacement of the content. + pub dx: Rel<Length>, + + /// The vertical displacement of the content. + pub dy: Rel<Length>, + + /// The content to move. + #[required] + pub body: Content, +} + +impl Show for Packed<MoveElem> { + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_move) + .pack() + .spanned(self.span())) + } +} + +/// Rotates content without affecting layout. +/// +/// Rotates an element by a given angle. The layout will act as if the element +/// was not rotated unless you specify `{reflow: true}`. +/// +/// # Example +/// ```example +/// #stack( +/// dir: ltr, +/// spacing: 1fr, +/// ..range(16) +/// .map(i => rotate(24deg * i)[X]), +/// ) +/// ``` +#[elem(Show)] +pub struct RotateElem { + /// The amount of rotation. + /// + /// ```example + /// #rotate(-1.571rad)[Space!] + /// ``` + /// + #[positional] + pub angle: Angle, + + /// The origin of the rotation. + /// + /// If, for instance, you wanted the bottom left corner of the rotated + /// element to stay aligned with the baseline, you would set it to `bottom + + /// left` instead. + /// + /// ```example + /// #set text(spacing: 8pt) + /// #let square = square.with(width: 8pt) + /// + /// #box(square()) + /// #box(rotate(30deg, origin: center, square())) + /// #box(rotate(30deg, origin: top + left, square())) + /// #box(rotate(30deg, origin: bottom + right, square())) + /// ``` + #[fold] + #[default(HAlignment::Center + VAlignment::Horizon)] + pub origin: Alignment, + + /// Whether the rotation impacts the layout. + /// + /// If set to `{false}`, the rotated content will retain the bounding box of + /// the original content. If set to `{true}`, the bounding box will take the + /// rotation of the content into account and adjust the layout accordingly. + /// + /// ```example + /// Hello #rotate(90deg, reflow: true)[World]! + /// ``` + #[default(false)] + pub reflow: bool, + + /// The content to rotate. + #[required] + pub body: Content, +} + +impl Show for Packed<RotateElem> { + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_rotate) + .pack() + .spanned(self.span())) + } +} + +/// Scales content without affecting layout. +/// +/// Lets you mirror content by specifying a negative scale on a single axis. +/// +/// # Example +/// ```example +/// #set align(center) +/// #scale(x: -100%)[This is mirrored.] +/// #scale(x: -100%, reflow: true)[This is mirrored.] +/// ``` +#[elem(Show)] +pub struct ScaleElem { + /// The scaling factor for both axes, as a positional argument. This is just + /// an optional shorthand notation for setting `x` and `y` to the same + /// value. + #[external] + #[positional] + #[default(Smart::Custom(ScaleAmount::Ratio(Ratio::one())))] + pub factor: Smart<ScaleAmount>, + + /// The horizontal scaling factor. + /// + /// The body will be mirrored horizontally if the parameter is negative. + #[parse( + let all = args.find()?; + args.named("x")?.or(all) + )] + #[default(Smart::Custom(ScaleAmount::Ratio(Ratio::one())))] + pub x: Smart<ScaleAmount>, + + /// The vertical scaling factor. + /// + /// The body will be mirrored vertically if the parameter is negative. + #[parse(args.named("y")?.or(all))] + #[default(Smart::Custom(ScaleAmount::Ratio(Ratio::one())))] + pub y: Smart<ScaleAmount>, + + /// The origin of the transformation. + /// + /// ```example + /// A#box(scale(75%)[A])A \ + /// B#box(scale(75%, origin: bottom + left)[B])B + /// ``` + #[fold] + #[default(HAlignment::Center + VAlignment::Horizon)] + pub origin: Alignment, + + /// Whether the scaling impacts the layout. + /// + /// If set to `{false}`, the scaled content will be allowed to overlap + /// other content. If set to `{true}`, it will compute the new size of + /// the scaled content and adjust the layout accordingly. + /// + /// ```example + /// Hello #scale(x: 20%, y: 40%, reflow: true)[World]! + /// ``` + #[default(false)] + pub reflow: bool, + + /// The content to scale. + #[required] + pub body: Content, +} + +impl Show for Packed<ScaleElem> { + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_scale) + .pack() + .spanned(self.span())) + } +} + +/// To what size something shall be scaled. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum ScaleAmount { + Ratio(Ratio), + Length(Length), +} + +cast! { + ScaleAmount, + self => match self { + ScaleAmount::Ratio(ratio) => ratio.into_value(), + ScaleAmount::Length(length) => length.into_value(), + }, + ratio: Ratio => ScaleAmount::Ratio(ratio), + length: Length => ScaleAmount::Length(length), +} + +/// Skews content. +/// +/// Skews an element in horizontal and/or vertical direction. The layout will +/// act as if the element was not skewed unless you specify `{reflow: true}`. +/// +/// # Example +/// ```example +/// #skew(ax: -12deg)[ +/// This is some fake italic text. +/// ] +/// ``` +#[elem(Show)] +pub struct SkewElem { + /// The horizontal skewing angle. + /// + /// ```example + /// #skew(ax: 30deg)[Skewed] + /// ``` + /// + #[default(Angle::zero())] + pub ax: Angle, + + /// The vertical skewing angle. + /// + /// ```example + /// #skew(ay: 30deg)[Skewed] + /// ``` + /// + #[default(Angle::zero())] + pub ay: Angle, + + /// The origin of the skew transformation. + /// + /// The origin will stay fixed during the operation. + /// + /// ```example + /// X #box(skew(ax: -30deg, origin: center + horizon)[X]) X \ + /// X #box(skew(ax: -30deg, origin: bottom + left)[X]) X \ + /// X #box(skew(ax: -30deg, origin: top + right)[X]) X + /// ``` + #[fold] + #[default(HAlignment::Center + VAlignment::Horizon)] + pub origin: Alignment, + + /// Whether the skew transformation impacts the layout. + /// + /// If set to `{false}`, the skewed content will retain the bounding box of + /// the original content. If set to `{true}`, the bounding box will take the + /// transformation of the content into account and adjust the layout accordingly. + /// + /// ```example + /// Hello #skew(ay: 30deg, reflow: true, "World")! + /// ``` + #[default(false)] + pub reflow: bool, + + /// The content to skew. + #[required] + pub body: Content, +} + +impl Show for Packed<SkewElem> { + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_skew) + .pack() + .spanned(self.span())) + } +} + +/// 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() + } + } + + /// A skew transform. + pub fn skew(ax: Angle, ay: Angle) -> Self { + Self { + kx: Ratio::new(ax.tan()), + ky: Ratio::new(ay.tan()), + ..Self::identity() + } + } + + /// 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) + } + + /// Inverts the transformation. + /// + /// Returns `None` if the determinant of the matrix is zero. + pub fn invert(self) -> Option<Self> { + // Allow the trivial case to be inlined. + if self.is_identity() { + return Some(self); + } + + // Fast path for scale-translate-only transforms. + if self.kx.is_zero() && self.ky.is_zero() { + if self.sx.is_zero() || self.sy.is_zero() { + return Some(Self::translate(-self.tx, -self.ty)); + } + + let inv_x = 1.0 / self.sx; + let inv_y = 1.0 / self.sy; + return Some(Self { + sx: Ratio::new(inv_x), + ky: Ratio::zero(), + kx: Ratio::zero(), + sy: Ratio::new(inv_y), + tx: -self.tx * inv_x, + ty: -self.ty * inv_y, + }); + } + + let det = self.sx * self.sy - self.kx * self.ky; + if det.get().abs() < 1e-12 { + return None; + } + + let inv_det = 1.0 / det; + Some(Self { + sx: (self.sy * inv_det), + ky: (-self.ky * inv_det), + kx: (-self.kx * inv_det), + sy: (self.sx * inv_det), + tx: Abs::pt( + (self.kx.get() * self.ty.to_pt() - self.sy.get() * self.tx.to_pt()) + * inv_det, + ), + ty: Abs::pt( + (self.ky.get() * self.tx.to_pt() - self.sx.get() * self.ty.to_pt()) + * inv_det, + ), + }) + } +} + +impl Default for Transform { + fn default() -> Self { + Self::identity() + } +} diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs new file mode 100644 index 00000000..34c2e526 --- /dev/null +++ b/crates/typst-library/src/lib.rs @@ -0,0 +1,264 @@ +//! Typst's standard library. +//! +//! This crate also contains all of the compiler's central type definitions as +//! these are interwoven with the standard library types. +//! +//! In contrast to the _types,_ most of the compilation _behaviour_ is split out +//! into separate crates (`typst-eval`, `typst-realize`, `typst-layout`, etc.) +//! +//! Note that, unless you are working on the compiler itself, you will rarely +//! need to interact with this crate, as it is fully reexported by the `typst` +//! crate. + +extern crate self as typst_library; + +pub mod diag; +pub mod engine; +pub mod foundations; +pub mod introspection; +pub mod layout; +pub mod loading; +pub mod math; +pub mod model; +pub mod routines; +pub mod symbols; +pub mod text; +pub mod visualize; + +use std::ops::{Deref, Range}; + +use ecow::EcoString; +use typst_syntax::package::PackageSpec; +use typst_syntax::{FileId, Source, Span}; +use typst_utils::LazyHash; + +use crate::diag::FileResult; +use crate::foundations::{Array, Bytes, Datetime, Dict, Module, Scope, Styles, Value}; +use crate::layout::{Alignment, Dir}; +use crate::text::{Font, FontBook}; +use crate::visualize::Color; + +/// 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 each compilation. Advanced +/// clients like language servers can also retain the source files and +/// [edit](Source::edit) them in-place to benefit from better incremental +/// performance. +#[comemo::track] +pub trait World: Send + Sync { + /// The standard library. + /// + /// Can be created through `Library::build()`. + fn library(&self) -> &LazyHash<Library>; + + /// Metadata about all known fonts. + fn book(&self) -> &LazyHash<FontBook>; + + /// Get the file id of the main source file. + fn main(&self) -> FileId; + + /// Try to access the specified source file. + 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>)] { + &[] + } +} + +macro_rules! world_impl { + ($W:ident for $ptr:ty) => { + impl<$W: World> World for $ptr { + fn library(&self) -> &LazyHash<Library> { + self.deref().library() + } + + fn book(&self) -> &LazyHash<FontBook> { + self.deref().book() + } + + fn main(&self) -> FileId { + self.deref().main() + } + + fn source(&self, id: FileId) -> FileResult<Source> { + self.deref().source(id) + } + + fn file(&self, id: FileId) -> FileResult<Bytes> { + self.deref().file(id) + } + + fn font(&self, index: usize) -> Option<Font> { + self.deref().font(index) + } + + fn today(&self, offset: Option<i64>) -> Option<Datetime> { + self.deref().today(offset) + } + + fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] { + self.deref().packages() + } + } + }; +} + +world_impl!(W for std::boxed::Box<W>); +world_impl!(W for std::sync::Arc<W>); +world_impl!(W for &W); + +/// Helper methods on [`World`] implementations. +pub trait WorldExt { + /// Get the byte range for a span. + /// + /// Returns `None` if the `Span` does not point into any source file. + fn range(&self, span: Span) -> Option<Range<usize>>; +} + +impl<T: World> WorldExt for T { + fn range(&self, span: Span) -> Option<Range<usize>> { + self.source(span.id()?).ok()?.range(span) + } +} + +/// Definition of Typst's standard library. +#[derive(Debug, Clone, Hash)] +pub struct Library { + /// The module that contains the definitions that are available everywhere. + pub global: Module, + /// The module that contains the definitions available in math mode. + pub math: Module, + /// The default style properties (for page size, font selection, and + /// everything else configurable via set and show rules). + pub styles: Styles, + /// The standard library as a value. + /// Used to provide the `std` variable. + pub std: Value, +} + +impl Library { + /// Create a new builder for a library. + pub fn builder() -> LibraryBuilder { + LibraryBuilder::default() + } +} + +impl Default for Library { + /// Constructs the standard library with the default configuration. + fn default() -> Self { + Self::builder().build() + } +} + +/// Configurable builder for the standard library. +/// +/// This struct is created by [`Library::builder`]. +#[derive(Debug, Clone, Default)] +pub struct LibraryBuilder { + inputs: Option<Dict>, +} + +impl LibraryBuilder { + /// Configure the inputs visible through `sys.inputs`. + pub fn with_inputs(mut self, inputs: Dict) -> Self { + self.inputs = Some(inputs); + self + } + + /// Consumes the builder and returns a `Library`. + pub fn build(self) -> Library { + let math = math::module(); + let inputs = self.inputs.unwrap_or_default(); + let global = global(math.clone(), inputs); + let std = Value::Module(global.clone()); + Library { global, math, styles: Styles::new(), std } + } +} + +/// Construct the module with global definitions. +fn global(math: Module, inputs: Dict) -> Module { + let mut global = Scope::deduplicating(); + self::foundations::define(&mut global, inputs); + self::model::define(&mut global); + self::text::define(&mut global); + global.reset_category(); + global.define_module(math); + self::layout::define(&mut global); + self::visualize::define(&mut global); + self::introspection::define(&mut global); + self::loading::define(&mut global); + self::symbols::define(&mut global); + prelude(&mut global); + Module::new("global", global) +} + +/// Defines scoped values that are globally available, too. +fn prelude(global: &mut Scope) { + global.reset_category(); + global.define("black", Color::BLACK); + global.define("gray", Color::GRAY); + global.define("silver", Color::SILVER); + global.define("white", Color::WHITE); + global.define("navy", Color::NAVY); + global.define("blue", Color::BLUE); + global.define("aqua", Color::AQUA); + global.define("teal", Color::TEAL); + global.define("eastern", Color::EASTERN); + global.define("purple", Color::PURPLE); + global.define("fuchsia", Color::FUCHSIA); + global.define("maroon", Color::MAROON); + global.define("red", Color::RED); + global.define("orange", Color::ORANGE); + global.define("yellow", Color::YELLOW); + global.define("olive", Color::OLIVE); + global.define("green", Color::GREEN); + global.define("lime", Color::LIME); + global.define("luma", Color::luma_data()); + global.define("oklab", Color::oklab_data()); + global.define("oklch", Color::oklch_data()); + global.define("rgb", Color::rgb_data()); + global.define("cmyk", Color::cmyk_data()); + global.define("range", Array::range_data()); + global.define("ltr", Dir::LTR); + global.define("rtl", Dir::RTL); + global.define("ttb", Dir::TTB); + global.define("btt", Dir::BTT); + global.define("start", Alignment::START); + global.define("left", Alignment::LEFT); + global.define("center", Alignment::CENTER); + global.define("right", Alignment::RIGHT); + global.define("end", Alignment::END); + global.define("top", Alignment::TOP); + global.define("horizon", Alignment::HORIZON); + global.define("bottom", Alignment::BOTTOM); +} diff --git a/crates/typst-library/src/loading/cbor.rs b/crates/typst-library/src/loading/cbor.rs new file mode 100644 index 00000000..977059c3 --- /dev/null +++ b/crates/typst-library/src/loading/cbor.rs @@ -0,0 +1,62 @@ +use ecow::{eco_format, EcoString}; +use typst_syntax::Spanned; + +use crate::diag::{At, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{func, scope, Bytes, Value}; +use crate::World; + +/// Reads structured data from a CBOR file. +/// +/// The file must contain a valid CBOR serialization. Mappings will be +/// converted into Typst dictionaries, and sequences will be converted into +/// Typst arrays. Strings and booleans will be converted into the Typst +/// equivalents, null-values (`null`, `~` or empty ``) will be converted into +/// `{none}`, and numbers will be converted to floats or integers depending on +/// whether they are whole numbers. +/// +/// Be aware that integers larger than 2<sup>63</sup>-1 will be converted to +/// floating point numbers, which may result in an approximative value. +#[func(scope, title = "CBOR")] +pub fn cbor( + /// The engine. + engine: &mut Engine, + /// Path to a CBOR file. + /// + /// For more details, see the [Paths section]($syntax/#paths). + path: Spanned<EcoString>, +) -> SourceResult<Value> { + let Spanned { v: path, span } = path; + let id = span.resolve_path(&path).at(span)?; + let data = engine.world.file(id).at(span)?; + cbor::decode(Spanned::new(data, span)) +} + +#[scope] +impl cbor { + /// Reads structured data from CBOR bytes. + #[func(title = "Decode CBOR")] + pub fn decode( + /// cbor data. + data: Spanned<Bytes>, + ) -> SourceResult<Value> { + let Spanned { v: data, span } = data; + ciborium::from_reader(data.as_slice()) + .map_err(|err| eco_format!("failed to parse CBOR ({err})")) + .at(span) + } + + /// Encode structured data into CBOR bytes. + #[func(title = "Encode CBOR")] + pub fn encode( + /// Value to be encoded. + value: Spanned<Value>, + ) -> SourceResult<Bytes> { + let Spanned { v: value, span } = value; + let mut res = Vec::new(); + ciborium::into_writer(&value, &mut res) + .map(|_| res.into()) + .map_err(|err| eco_format!("failed to encode value as CBOR ({err})")) + .at(span) + } +} diff --git a/crates/typst-library/src/loading/csv.rs b/crates/typst-library/src/loading/csv.rs new file mode 100644 index 00000000..6822505d --- /dev/null +++ b/crates/typst-library/src/loading/csv.rs @@ -0,0 +1,191 @@ +use ecow::{eco_format, EcoString}; +use typst_syntax::Spanned; + +use crate::diag::{bail, At, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{cast, func, scope, Array, Dict, IntoValue, Type, Value}; +use crate::loading::Readable; +use crate::World; + +/// Reads structured data from a CSV file. +/// +/// The CSV file will be read and parsed into a 2-dimensional array of strings: +/// Each row in the CSV file will be represented as an array of strings, and all +/// rows will be collected into a single array. Header rows will not be +/// stripped. +/// +/// # Example +/// ```example +/// #let results = csv("example.csv") +/// +/// #table( +/// columns: 2, +/// [*Condition*], [*Result*], +/// ..results.flatten(), +/// ) +/// ``` +#[func(scope, title = "CSV")] +pub fn csv( + /// The engine. + engine: &mut Engine, + /// Path to a CSV file. + /// + /// For more details, see the [Paths section]($syntax/#paths). + path: Spanned<EcoString>, + /// The delimiter that separates columns in the CSV file. + /// Must be a single ASCII character. + #[named] + #[default] + delimiter: Delimiter, + /// How to represent the file's rows. + /// + /// - If set to `array`, each row is represented as a plain array of + /// strings. + /// - If set to `dictionary`, each row is represented as a dictionary + /// mapping from header keys to strings. This option only makes sense when + /// a header row is present in the CSV file. + #[named] + #[default(RowType::Array)] + row_type: RowType, +) -> SourceResult<Array> { + let Spanned { v: path, span } = path; + let id = span.resolve_path(&path).at(span)?; + let data = engine.world.file(id).at(span)?; + self::csv::decode(Spanned::new(Readable::Bytes(data), span), delimiter, row_type) +} + +#[scope] +impl csv { + /// Reads structured data from a CSV string/bytes. + #[func(title = "Decode CSV")] + pub fn decode( + /// CSV data. + data: Spanned<Readable>, + /// The delimiter that separates columns in the CSV file. + /// Must be a single ASCII character. + #[named] + #[default] + delimiter: Delimiter, + /// How to represent the file's rows. + /// + /// - If set to `array`, each row is represented as a plain array of + /// strings. + /// - If set to `dictionary`, each row is represented as a dictionary + /// mapping from header keys to strings. This option only makes sense + /// when a header row is present in the CSV file. + #[named] + #[default(RowType::Array)] + row_type: RowType, + ) -> SourceResult<Array> { + let Spanned { v: data, span } = data; + let has_headers = row_type == RowType::Dict; + + let mut builder = ::csv::ReaderBuilder::new(); + builder.has_headers(has_headers); + builder.delimiter(delimiter.0 as u8); + + // Counting lines from 1 by default. + let mut line_offset: usize = 1; + let mut reader = builder.from_reader(data.as_slice()); + let mut headers: Option<::csv::StringRecord> = None; + + if has_headers { + // Counting lines from 2 because we have a header. + line_offset += 1; + headers = Some( + reader + .headers() + .map_err(|err| format_csv_error(err, 1)) + .at(span)? + .clone(), + ); + } + + let mut array = Array::new(); + for (line, result) in reader.records().enumerate() { + // Original solution was to use line from error, but that is + // incorrect with `has_headers` set to `false`. See issue: + // https://github.com/BurntSushi/rust-csv/issues/184 + let line = line + line_offset; + let row = result.map_err(|err| format_csv_error(err, line)).at(span)?; + let item = if let Some(headers) = &headers { + let mut dict = Dict::new(); + for (field, value) in headers.iter().zip(&row) { + dict.insert(field.into(), value.into_value()); + } + dict.into_value() + } else { + let sub = row.into_iter().map(|field| field.into_value()).collect(); + Value::Array(sub) + }; + array.push(item); + } + + Ok(array) + } +} + +/// The delimiter to use when parsing CSV files. +pub struct Delimiter(char); + +impl Default for Delimiter { + fn default() -> Self { + Self(',') + } +} + +cast! { + Delimiter, + self => self.0.into_value(), + v: EcoString => { + let mut chars = v.chars(); + let first = chars.next().ok_or("delimiter must not be empty")?; + if chars.next().is_some() { + bail!("delimiter must be a single character"); + } + + if !first.is_ascii() { + bail!("delimiter must be an ASCII character"); + } + + Self(first) + }, +} + +/// The type of parsed rows. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum RowType { + Array, + Dict, +} + +cast! { + RowType, + self => match self { + Self::Array => Type::of::<Array>(), + Self::Dict => Type::of::<Dict>(), + }.into_value(), + ty: Type => { + if ty == Type::of::<Array>() { + Self::Array + } else if ty == Type::of::<Dict>() { + Self::Dict + } else { + bail!("expected `array` or `dictionary`"); + } + }, +} + +/// Format the user-facing CSV error message. +fn format_csv_error(err: ::csv::Error, line: usize) -> EcoString { + match err.kind() { + ::csv::ErrorKind::Utf8 { .. } => "file is not valid utf-8".into(), + ::csv::ErrorKind::UnequalLengths { expected_len, len, .. } => { + eco_format!( + "failed to parse CSV (found {len} instead of \ + {expected_len} fields in line {line})" + ) + } + _ => eco_format!("failed to parse CSV ({err})"), + } +} diff --git a/crates/typst-library/src/loading/json.rs b/crates/typst-library/src/loading/json.rs new file mode 100644 index 00000000..597cf4cc --- /dev/null +++ b/crates/typst-library/src/loading/json.rs @@ -0,0 +1,101 @@ +use ecow::{eco_format, EcoString}; +use typst_syntax::Spanned; + +use crate::diag::{At, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{func, scope, Str, Value}; +use crate::loading::Readable; +use crate::World; + +/// Reads structured data from a JSON file. +/// +/// The file must contain a valid JSON value, such as object or array. JSON +/// objects will be converted into Typst dictionaries, and JSON arrays will be +/// converted into Typst arrays. Strings and booleans will be converted into the +/// Typst equivalents, `null` will be converted into `{none}`, and numbers will +/// be converted to floats or integers depending on whether they are whole +/// numbers. +/// +/// Be aware that integers larger than 2<sup>63</sup>-1 will be converted to +/// floating point numbers, which may result in an approximative value. +/// +/// The function returns a dictionary, an array or, depending on the JSON file, +/// another JSON data type. +/// +/// The JSON files in the example contain objects with the keys `temperature`, +/// `unit`, and `weather`. +/// +/// # Example +/// ```example +/// #let forecast(day) = block[ +/// #box(square( +/// width: 2cm, +/// inset: 8pt, +/// fill: if day.weather == "sunny" { +/// yellow +/// } else { +/// aqua +/// }, +/// align( +/// bottom + right, +/// strong(day.weather), +/// ), +/// )) +/// #h(6pt) +/// #set text(22pt, baseline: -8pt) +/// #day.temperature °#day.unit +/// ] +/// +/// #forecast(json("monday.json")) +/// #forecast(json("tuesday.json")) +/// ``` +#[func(scope, title = "JSON")] +pub fn json( + /// The engine. + engine: &mut Engine, + /// Path to a JSON file. + /// + /// For more details, see the [Paths section]($syntax/#paths). + path: Spanned<EcoString>, +) -> SourceResult<Value> { + let Spanned { v: path, span } = path; + let id = span.resolve_path(&path).at(span)?; + let data = engine.world.file(id).at(span)?; + json::decode(Spanned::new(Readable::Bytes(data), span)) +} + +#[scope] +impl json { + /// Reads structured data from a JSON string/bytes. + #[func(title = "Decode JSON")] + pub fn decode( + /// JSON data. + data: Spanned<Readable>, + ) -> SourceResult<Value> { + let Spanned { v: data, span } = data; + serde_json::from_slice(data.as_slice()) + .map_err(|err| eco_format!("failed to parse JSON ({err})")) + .at(span) + } + + /// Encodes structured data into a JSON string. + #[func(title = "Encode JSON")] + pub fn encode( + /// Value to be encoded. + value: Spanned<Value>, + /// Whether to pretty print the JSON with newlines and indentation. + #[named] + #[default(true)] + pretty: bool, + ) -> SourceResult<Str> { + let Spanned { v: value, span } = value; + if pretty { + serde_json::to_string_pretty(&value) + } else { + serde_json::to_string(&value) + } + .map(|v| v.into()) + .map_err(|err| eco_format!("failed to encode value as JSON ({err})")) + .at(span) + } +} diff --git a/crates/typst-library/src/loading/mod.rs b/crates/typst-library/src/loading/mod.rs new file mode 100644 index 00000000..ae74df86 --- /dev/null +++ b/crates/typst-library/src/loading/mod.rs @@ -0,0 +1,89 @@ +//! Data loading. + +#[path = "cbor.rs"] +mod cbor_; +#[path = "csv.rs"] +mod csv_; +#[path = "json.rs"] +mod json_; +#[path = "read.rs"] +mod read_; +#[path = "toml.rs"] +mod toml_; +#[path = "xml.rs"] +mod xml_; +#[path = "yaml.rs"] +mod yaml_; + +pub use self::cbor_::*; +pub use self::csv_::*; +pub use self::json_::*; +pub use self::read_::*; +pub use self::toml_::*; +pub use self::xml_::*; +pub use self::yaml_::*; + +use crate::foundations::{cast, category, Bytes, Category, Scope, Str}; + +/// Data loading from external files. +/// +/// These functions help you with loading and embedding data, for example from +/// the results of an experiment. +#[category] +pub static DATA_LOADING: Category; + +/// Hook up all `data-loading` definitions. +pub(super) fn define(global: &mut Scope) { + global.category(DATA_LOADING); + global.define_func::<read>(); + global.define_func::<csv>(); + global.define_func::<json>(); + global.define_func::<toml>(); + global.define_func::<yaml>(); + global.define_func::<cbor>(); + global.define_func::<xml>(); +} + +/// A value that can be read from a file. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum Readable { + /// A decoded string. + Str(Str), + /// Raw bytes. + Bytes(Bytes), +} + +impl Readable { + pub fn as_slice(&self) -> &[u8] { + match self { + Readable::Bytes(v) => v, + Readable::Str(v) => v.as_bytes(), + } + } + + pub fn as_str(&self) -> Option<&str> { + match self { + Readable::Str(v) => Some(v.as_str()), + Readable::Bytes(v) => std::str::from_utf8(v).ok(), + } + } +} + +cast! { + Readable, + self => match self { + Self::Str(v) => v.into_value(), + Self::Bytes(v) => v.into_value(), + }, + v: Str => Self::Str(v), + v: Bytes => Self::Bytes(v), +} + +impl From<Readable> for Bytes { + fn from(value: Readable) -> Self { + match value { + Readable::Bytes(v) => v, + Readable::Str(v) => v.as_bytes().into(), + } + } +} diff --git a/crates/typst-library/src/loading/read.rs b/crates/typst-library/src/loading/read.rs new file mode 100644 index 00000000..23e6e27e --- /dev/null +++ b/crates/typst-library/src/loading/read.rs @@ -0,0 +1,59 @@ +use ecow::EcoString; +use typst_syntax::Spanned; + +use crate::diag::{At, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{func, Cast}; +use crate::loading::Readable; +use crate::World; + +/// Reads plain text or data from a file. +/// +/// By default, the file will be read as UTF-8 and returned as a [string]($str). +/// +/// If you specify `{encoding: none}`, this returns raw [bytes] instead. +/// +/// # Example +/// ```example +/// An example for a HTML file: \ +/// #let text = read("example.html") +/// #raw(text, lang: "html") +/// +/// Raw bytes: +/// #read("tiger.jpg", encoding: none) +/// ``` +#[func] +pub fn read( + /// The engine. + engine: &mut Engine, + /// Path to a file. + /// + /// For more details, see the [Paths section]($syntax/#paths). + path: Spanned<EcoString>, + /// The encoding to read the file with. + /// + /// If set to `{none}`, this function returns raw bytes. + #[named] + #[default(Some(Encoding::Utf8))] + encoding: Option<Encoding>, +) -> SourceResult<Readable> { + let Spanned { v: path, span } = path; + let id = span.resolve_path(&path).at(span)?; + let data = engine.world.file(id).at(span)?; + Ok(match encoding { + None => Readable::Bytes(data), + Some(Encoding::Utf8) => Readable::Str( + std::str::from_utf8(&data) + .map_err(|_| "file is not valid utf-8") + .at(span)? + .into(), + ), + }) +} + +/// An encoding of a file. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum Encoding { + /// The Unicode UTF-8 encoding. + Utf8, +} diff --git a/crates/typst-library/src/loading/toml.rs b/crates/typst-library/src/loading/toml.rs new file mode 100644 index 00000000..5167703e --- /dev/null +++ b/crates/typst-library/src/loading/toml.rs @@ -0,0 +1,92 @@ +use ecow::{eco_format, EcoString}; +use typst_syntax::{is_newline, Spanned}; + +use crate::diag::{At, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{func, scope, Str, Value}; +use crate::loading::Readable; +use crate::World; + +/// Reads structured data from a TOML file. +/// +/// The file must contain a valid TOML table. TOML tables will be converted into +/// Typst dictionaries, and TOML arrays will be converted into Typst arrays. +/// Strings, booleans and datetimes will be converted into the Typst equivalents +/// and numbers will be converted to floats or integers depending on whether +/// they are whole numbers. +/// +/// The TOML file in the example consists of a table with the keys `title`, +/// `version`, and `authors`. +/// +/// # Example +/// ```example +/// #let details = toml("details.toml") +/// +/// Title: #details.title \ +/// Version: #details.version \ +/// Authors: #(details.authors +/// .join(", ", last: " and ")) +/// ``` +#[func(scope, title = "TOML")] +pub fn toml( + /// The engine. + engine: &mut Engine, + /// Path to a TOML file. + /// + /// For more details, see the [Paths section]($syntax/#paths). + path: Spanned<EcoString>, +) -> SourceResult<Value> { + let Spanned { v: path, span } = path; + let id = span.resolve_path(&path).at(span)?; + let data = engine.world.file(id).at(span)?; + toml::decode(Spanned::new(Readable::Bytes(data), span)) +} + +#[scope] +impl toml { + /// Reads structured data from a TOML string/bytes. + #[func(title = "Decode TOML")] + pub fn decode( + /// TOML data. + data: Spanned<Readable>, + ) -> SourceResult<Value> { + let Spanned { v: data, span } = data; + let raw = std::str::from_utf8(data.as_slice()) + .map_err(|_| "file is not valid utf-8") + .at(span)?; + ::toml::from_str(raw) + .map_err(|err| format_toml_error(err, raw)) + .at(span) + } + + /// Encodes structured data into a TOML string. + #[func(title = "Encode TOML")] + pub fn encode( + /// Value to be encoded. + value: Spanned<Value>, + /// Whether to pretty-print the resulting TOML. + #[named] + #[default(true)] + pretty: bool, + ) -> SourceResult<Str> { + let Spanned { v: value, span } = value; + if pretty { ::toml::to_string_pretty(&value) } else { ::toml::to_string(&value) } + .map(|v| v.into()) + .map_err(|err| eco_format!("failed to encode value as TOML ({err})")) + .at(span) + } +} + +/// Format the user-facing TOML error message. +fn format_toml_error(error: ::toml::de::Error, raw: &str) -> EcoString { + if let Some(head) = error.span().and_then(|range| raw.get(..range.start)) { + let line = head.lines().count(); + let column = 1 + head.chars().rev().take_while(|&c| !is_newline(c)).count(); + eco_format!( + "failed to parse TOML ({} at line {line} column {column})", + error.message(), + ) + } else { + eco_format!("failed to parse TOML ({})", error.message()) + } +} diff --git a/crates/typst-library/src/loading/xml.rs b/crates/typst-library/src/loading/xml.rs new file mode 100644 index 00000000..3b1a9674 --- /dev/null +++ b/crates/typst-library/src/loading/xml.rs @@ -0,0 +1,123 @@ +use ecow::EcoString; +use roxmltree::ParsingOptions; +use typst_syntax::Spanned; + +use crate::diag::{format_xml_like_error, At, FileError, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{dict, func, scope, Array, Dict, IntoValue, Str, Value}; +use crate::loading::Readable; +use crate::World; + +/// Reads structured data from an XML file. +/// +/// The XML file is parsed into an array of dictionaries and strings. XML nodes +/// can be elements or strings. Elements are represented as dictionaries with +/// the following keys: +/// +/// - `tag`: The name of the element as a string. +/// - `attrs`: A dictionary of the element's attributes as strings. +/// - `children`: An array of the element's child nodes. +/// +/// The XML file in the example contains a root `news` tag with multiple +/// `article` tags. Each article has a `title`, `author`, and `content` tag. The +/// `content` tag contains one or more paragraphs, which are represented as `p` +/// tags. +/// +/// # Example +/// ```example +/// #let find-child(elem, tag) = { +/// elem.children +/// .find(e => "tag" in e and e.tag == tag) +/// } +/// +/// #let article(elem) = { +/// let title = find-child(elem, "title") +/// let author = find-child(elem, "author") +/// let pars = find-child(elem, "content") +/// +/// heading(title.children.first()) +/// text(10pt, weight: "medium")[ +/// Published by +/// #author.children.first() +/// ] +/// +/// for p in pars.children { +/// if (type(p) == "dictionary") { +/// parbreak() +/// p.children.first() +/// } +/// } +/// } +/// +/// #let data = xml("example.xml") +/// #for elem in data.first().children { +/// if (type(elem) == "dictionary") { +/// article(elem) +/// } +/// } +/// ``` +#[func(scope, title = "XML")] +pub fn xml( + /// The engine. + engine: &mut Engine, + /// Path to an XML file. + /// + /// For more details, see the [Paths section]($syntax/#paths). + path: Spanned<EcoString>, +) -> SourceResult<Value> { + let Spanned { v: path, span } = path; + let id = span.resolve_path(&path).at(span)?; + let data = engine.world.file(id).at(span)?; + xml::decode(Spanned::new(Readable::Bytes(data), span)) +} + +#[scope] +impl xml { + /// Reads structured data from an XML string/bytes. + #[func(title = "Decode XML")] + pub fn decode( + /// XML data. + data: Spanned<Readable>, + ) -> SourceResult<Value> { + let Spanned { v: data, span } = data; + let text = std::str::from_utf8(data.as_slice()) + .map_err(FileError::from) + .at(span)?; + let document = roxmltree::Document::parse_with_options( + text, + ParsingOptions { allow_dtd: true, ..Default::default() }, + ) + .map_err(format_xml_error) + .at(span)?; + Ok(convert_xml(document.root())) + } +} + +/// Convert an XML node to a Typst value. +fn convert_xml(node: roxmltree::Node) -> Value { + if node.is_text() { + return node.text().unwrap_or_default().into_value(); + } + + let children: Array = node.children().map(convert_xml).collect(); + if node.is_root() { + return Value::Array(children); + } + + let tag: Str = node.tag_name().name().into(); + let attrs: Dict = node + .attributes() + .map(|attr| (attr.name().into(), attr.value().into_value())) + .collect(); + + Value::Dict(dict! { + "tag" => tag, + "attrs" => attrs, + "children" => children, + }) +} + +/// Format the user-facing XML error message. +fn format_xml_error(error: roxmltree::Error) -> EcoString { + format_xml_like_error("XML", error) +} diff --git a/crates/typst-library/src/loading/yaml.rs b/crates/typst-library/src/loading/yaml.rs new file mode 100644 index 00000000..0e8ca3fb --- /dev/null +++ b/crates/typst-library/src/loading/yaml.rs @@ -0,0 +1,83 @@ +use ecow::{eco_format, EcoString}; +use typst_syntax::Spanned; + +use crate::diag::{At, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{func, scope, Str, Value}; +use crate::loading::Readable; +use crate::World; + +/// Reads structured data from a YAML file. +/// +/// The file must contain a valid YAML object or array. YAML mappings will be +/// converted into Typst dictionaries, and YAML sequences will be converted into +/// Typst arrays. Strings and booleans will be converted into the Typst +/// equivalents, null-values (`null`, `~` or empty ``) will be converted into +/// `{none}`, and numbers will be converted to floats or integers depending on +/// whether they are whole numbers. Custom YAML tags are ignored, though the +/// loaded value will still be present. +/// +/// Be aware that integers larger than 2<sup>63</sup>-1 will be converted to +/// floating point numbers, which may give an approximative value. +/// +/// The YAML files in the example contain objects with authors as keys, +/// each with a sequence of their own submapping with the keys +/// "title" and "published" +/// +/// # Example +/// ```example +/// #let bookshelf(contents) = { +/// for (author, works) in contents { +/// author +/// for work in works [ +/// - #work.title (#work.published) +/// ] +/// } +/// } +/// +/// #bookshelf( +/// yaml("scifi-authors.yaml") +/// ) +/// ``` +#[func(scope, title = "YAML")] +pub fn yaml( + /// The engine. + engine: &mut Engine, + /// Path to a YAML file. + /// + /// For more details, see the [Paths section]($syntax/#paths). + path: Spanned<EcoString>, +) -> SourceResult<Value> { + let Spanned { v: path, span } = path; + let id = span.resolve_path(&path).at(span)?; + let data = engine.world.file(id).at(span)?; + yaml::decode(Spanned::new(Readable::Bytes(data), span)) +} + +#[scope] +impl yaml { + /// Reads structured data from a YAML string/bytes. + #[func(title = "Decode YAML")] + pub fn decode( + /// YAML data. + data: Spanned<Readable>, + ) -> SourceResult<Value> { + let Spanned { v: data, span } = data; + serde_yaml::from_slice(data.as_slice()) + .map_err(|err| eco_format!("failed to parse YAML ({err})")) + .at(span) + } + + /// Encode structured data into a YAML string. + #[func(title = "Encode YAML")] + pub fn encode( + /// Value to be encoded. + value: Spanned<Value>, + ) -> SourceResult<Str> { + let Spanned { v: value, span } = value; + serde_yaml::to_string(&value) + .map(|v| v.into()) + .map_err(|err| eco_format!("failed to encode value as YAML ({err})")) + .at(span) + } +} diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs new file mode 100644 index 00000000..a1c70b4a --- /dev/null +++ b/crates/typst-library/src/math/accent.rs @@ -0,0 +1,147 @@ +use crate::diag::bail; +use crate::foundations::{cast, elem, func, Content, NativeElement, Smart, Value}; +use crate::layout::{Length, Rel}; +use crate::math::Mathy; +use crate::text::TextElem; + +/// Attaches an accent to a base. +/// +/// # Example +/// ```example +/// $grave(a) = accent(a, `)$ \ +/// $arrow(a) = accent(a, arrow)$ \ +/// $tilde(a) = accent(a, \u{0303})$ +/// ``` +#[elem(Mathy)] +pub struct AccentElem { + /// The base to which the accent is applied. + /// May consist of multiple letters. + /// + /// ```example + /// $arrow(A B C)$ + /// ``` + #[required] + pub base: Content, + + /// The accent to apply to the base. + /// + /// Supported accents include: + /// + /// | Accent | Name | Codepoint | + /// | ------------- | --------------- | --------- | + /// | Grave | `grave` | <code>`</code> | + /// | Acute | `acute` | `´` | + /// | Circumflex | `hat` | `^` | + /// | Tilde | `tilde` | `~` | + /// | Macron | `macron` | `¯` | + /// | Dash | `dash` | `‾` | + /// | Breve | `breve` | `˘` | + /// | Dot | `dot` | `.` | + /// | Double dot, Diaeresis | `dot.double`, `diaer` | `¨` | + /// | Triple dot | `dot.triple` | <code>⃛</code> | + /// | Quadruple dot | `dot.quad` | <code>⃜</code> | + /// | Circle | `circle` | `∘` | + /// | Double acute | `acute.double` | `˝` | + /// | Caron | `caron` | `ˇ` | + /// | Right arrow | `arrow`, `->` | `→` | + /// | Left arrow | `arrow.l`, `<-` | `←` | + /// | Left/Right arrow | `arrow.l.r` | `↔` | + /// | Right harpoon | `harpoon` | `⇀` | + /// | Left harpoon | `harpoon.lt` | `↼` | + #[required] + pub accent: Accent, + + /// The size of the accent, relative to the width of the base. + pub size: Smart<Rel<Length>>, +} + +/// An accent character. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Accent(pub char); + +impl Accent { + /// Normalize a character into an accent. + pub fn new(c: char) -> Self { + Self(Self::combine(c).unwrap_or(c)) + } +} + +/// This macro generates accent-related functions. +/// +/// ```ignore +/// accents! { +/// '\u{0300}' | '`' => grave, +/// // ^^^^^^^^^ ^^^ ^^^^^ +/// // | | | +/// // | | +-- The name of the function. +/// // | +--------- The alternative characters that represent the accent. +/// // +---------------------- The primary character that represents the accent. +/// } +/// ``` +/// +/// When combined with the `Accent::combine` function, accent characters can be normalized +/// to the primary character. +macro_rules! accents { + ($($primary:literal $(| $alt:literal)* => $name:ident),* $(,)?) => { + impl Accent { + /// Normalize an accent to a combining one. + pub fn combine(c: char) -> Option<char> { + Some(match c { + $($primary $(| $alt)* => $primary,)* + _ => return None, + }) + } + } + + $( + /// The accent function for callable symbol definitions. + #[func] + pub fn $name( + /// The base to which the accent is applied. + base: Content, + /// The size of the accent, relative to the width of the base. + #[named] + size: Option<Smart<Rel<Length>>>, + ) -> Content { + let mut accent = AccentElem::new(base, Accent::new($primary)); + if let Some(size) = size { + accent = accent.with_size(size); + } + accent.pack() + } + )+ + }; +} + +// Keep it synced with the documenting table above. +accents! { + '\u{0300}' | '`' => grave, + '\u{0301}' | '´' => acute, + '\u{0302}' | '^' | 'ˆ' => hat, + '\u{0303}' | '~' | '∼' | '˜' => tilde, + '\u{0304}' | '¯' => macron, + '\u{0305}' | '-' | '‾' | '−' => dash, + '\u{0306}' | '˘' => breve, + '\u{0307}' | '.' | '˙' | '⋅' => dot, + '\u{0308}' | '¨' => dot_double, + '\u{20db}' => dot_triple, + '\u{20dc}' => dot_quad, + '\u{030a}' | '∘' | '○' => circle, + '\u{030b}' | '˝' => acute_double, + '\u{030c}' | 'ˇ' => caron, + '\u{20d6}' | '←' => arrow_l, + '\u{20d7}' | '→' | '⟶' => arrow, + '\u{20e1}' | '↔' | '⟷' => arrow_l_r, + '\u{20d0}' | '↼' => harpoon_lt, + '\u{20d1}' | '⇀' => harpoon, +} + +cast! { + Accent, + self => self.0.into_value(), + v: char => Self::new(v), + v: Content => match v.to_packed::<TextElem>() { + Some(elem) => Value::Str(elem.text().clone().into()).cast()?, + None => bail!("expected text"), + }, +} diff --git a/crates/typst-library/src/math/attach.rs b/crates/typst-library/src/math/attach.rs new file mode 100644 index 00000000..74eabd10 --- /dev/null +++ b/crates/typst-library/src/math/attach.rs @@ -0,0 +1,156 @@ +use crate::foundations::{elem, Content, Packed, Smart}; +use crate::layout::{Length, Rel}; +use crate::math::{EquationElem, Mathy}; + +/// A base with optional attachments. +/// +/// ```example +/// $ attach( +/// Pi, t: alpha, b: beta, +/// tl: 1, tr: 2+3, bl: 4+5, br: 6, +/// ) $ +/// ``` +#[elem(Mathy)] +pub struct AttachElem { + /// The base to which things are attached. + #[required] + pub base: Content, + + /// The top attachment, smartly positioned at top-right or above the base. + /// + /// You can wrap the base in `{limits()}` or `{scripts()}` to override the + /// smart positioning. + pub t: Option<Content>, + + /// The bottom attachment, smartly positioned at the bottom-right or below + /// the base. + /// + /// You can wrap the base in `{limits()}` or `{scripts()}` to override the + /// smart positioning. + pub b: Option<Content>, + + /// The top-left attachment (before the base). + pub tl: Option<Content>, + + /// The bottom-left attachment (before base). + pub bl: Option<Content>, + + /// The top-right attachment (after the base). + pub tr: Option<Content>, + + /// The bottom-right attachment (after the base). + pub br: Option<Content>, +} + +impl Packed<AttachElem> { + /// If an AttachElem's base is also an AttachElem, merge attachments into the + /// base AttachElem where possible. + pub fn merge_base(&self) -> Option<Self> { + // Extract from an EquationElem. + let mut base = self.base(); + if let Some(equation) = base.to_packed::<EquationElem>() { + base = equation.body(); + } + + // Move attachments from elem into base where possible. + if let Some(base) = base.to_packed::<AttachElem>() { + let mut elem = self.clone(); + let mut base = base.clone(); + + macro_rules! merge { + ($content:ident) => { + if base.$content.is_none() && elem.$content.is_some() { + base.$content = elem.$content.clone(); + elem.$content = None; + } + }; + } + + merge!(t); + merge!(b); + merge!(tl); + merge!(tr); + merge!(bl); + merge!(br); + + elem.base = base.pack(); + return Some(elem); + } + + None + } +} + +/// Grouped primes. +/// +/// ```example +/// $ a'''_b = a^'''_b $ +/// ``` +/// +/// # Syntax +/// This function has dedicated syntax: use apostrophes instead of primes. They +/// will automatically attach to the previous element, moving superscripts to +/// the next level. +#[elem(Mathy)] +pub struct PrimesElem { + /// The number of grouped primes. + #[required] + pub count: usize, +} + +/// Forces a base to display attachments as scripts. +/// +/// ```example +/// $ scripts(sum)_1^2 != sum_1^2 $ +/// ``` +#[elem(Mathy)] +pub struct ScriptsElem { + /// The base to attach the scripts to. + #[required] + pub body: Content, +} + +/// Forces a base to display attachments as limits. +/// +/// ```example +/// $ limits(A)_1^2 != A_1^2 $ +/// ``` +#[elem(Mathy)] +pub struct LimitsElem { + /// The base to attach the limits to. + #[required] + pub body: Content, + + /// Whether to also force limits in inline equations. + /// + /// When applying limits globally (e.g., through a show rule), it is + /// typically a good idea to disable this. + #[default(true)] + pub inline: bool, +} + +/// Stretches a glyph. +/// +/// This function can also be used to automatically stretch the base of an +/// attachment, so that it fits the top and bottom attachments. +/// +/// Note that only some glyphs can be stretched, and which ones can depend on +/// the math font being used. However, most math fonts are the same in this +/// regard. +/// +/// ```example +/// $ H stretch(=)^"define" U + p V $ +/// $ f : X stretch(->>, size: #150%)_"surjective" Y $ +/// $ x stretch(harpoons.ltrb, size: #3em) y +/// stretch(\[, size: #150%) z $ +/// ``` +#[elem(Mathy)] +pub struct StretchElem { + /// The glyph to stretch. + #[required] + pub body: Content, + + /// The size to stretch to, relative to the maximum size of the glyph and + /// its attachments. + pub size: Smart<Rel<Length>>, +} diff --git a/crates/typst-library/src/math/cancel.rs b/crates/typst-library/src/math/cancel.rs new file mode 100644 index 00000000..a72505c0 --- /dev/null +++ b/crates/typst-library/src/math/cancel.rs @@ -0,0 +1,116 @@ +use crate::foundations::{cast, elem, Content, Func, Smart}; +use crate::layout::{Abs, Angle, Length, Ratio, Rel}; +use crate::math::Mathy; +use crate::visualize::Stroke; + +/// Displays a diagonal line over a part of an equation. +/// +/// This is commonly used to show the elimination of a term. +/// +/// # Example +/// ```example +/// >>> #set page(width: 140pt) +/// Here, we can simplify: +/// $ (a dot b dot cancel(x)) / +/// cancel(x) $ +/// ``` +#[elem(Mathy)] +pub struct CancelElem { + /// The content over which the line should be placed. + #[required] + pub body: Content, + + /// The length of the line, relative to the length of the diagonal spanning + /// the whole element being "cancelled". A value of `{100%}` would then have + /// the line span precisely the element's diagonal. + /// + /// ```example + /// >>> #set page(width: 140pt) + /// $ a + cancel(x, length: #200%) + /// - cancel(x, length: #200%) $ + /// ``` + #[default(Rel::new(Ratio::one(), Abs::pt(3.0).into()))] + pub length: Rel<Length>, + + /// Whether the cancel line should be inverted (flipped along the y-axis). + /// For the default angle setting, inverted means the cancel line + /// points to the top left instead of top right. + /// + /// ```example + /// >>> #set page(width: 140pt) + /// $ (a cancel((b + c), inverted: #true)) / + /// cancel(b + c, inverted: #true) $ + /// ``` + #[default(false)] + pub inverted: bool, + + /// Whether two opposing cancel lines should be drawn, forming a cross over + /// the element. Overrides `inverted`. + /// + /// ```example + /// >>> #set page(width: 140pt) + /// $ cancel(Pi, cross: #true) $ + /// ``` + #[default(false)] + pub cross: bool, + + /// How much to rotate the cancel line. + /// + /// - If given an angle, the line is rotated by that angle clockwise with + /// respect to the y-axis. + /// - If `{auto}`, the line assumes the default angle; that is, along the + /// rising diagonal of the content box. + /// - If given a function `angle => angle`, the line is rotated, with + /// respect to the y-axis, by the angle returned by that function. The + /// function receives the default angle as its input. + /// + /// ```example + /// >>> #set page(width: 140pt) + /// $ cancel(Pi) + /// cancel(Pi, angle: #0deg) + /// cancel(Pi, angle: #45deg) + /// cancel(Pi, angle: #90deg) + /// cancel(1/(1+x), angle: #(a => a + 45deg)) + /// cancel(1/(1+x), angle: #(a => a + 90deg)) $ + /// ``` + pub angle: Smart<CancelAngle>, + + /// How to [stroke]($stroke) the cancel line. + /// + /// ```example + /// >>> #set page(width: 140pt) + /// $ cancel( + /// sum x, + /// stroke: #( + /// paint: red, + /// thickness: 1.5pt, + /// dash: "dashed", + /// ), + /// ) $ + /// ``` + #[resolve] + #[fold] + #[default(Stroke { + // Default stroke has 0.5pt for better visuals. + thickness: Smart::Custom(Abs::pt(0.5).into()), + ..Default::default() + })] + pub stroke: Stroke, +} + +/// Defines the cancel line. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum CancelAngle { + Angle(Angle), + Func(Func), +} + +cast! { + CancelAngle, + self => match self { + Self::Angle(v) => v.into_value(), + Self::Func(v) => v.into_value() + }, + v: Angle => CancelAngle::Angle(v), + v: Func => CancelAngle::Func(v), +} diff --git a/crates/typst-library/src/math/equation.rs b/crates/typst-library/src/math/equation.rs new file mode 100644 index 00000000..4edafe5e --- /dev/null +++ b/crates/typst-library/src/math/equation.rs @@ -0,0 +1,256 @@ +use std::num::NonZeroUsize; + +use typst_utils::NonZeroExt; +use unicode_math_class::MathClass; + +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{ + elem, Content, NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles, + Synthesize, +}; +use crate::introspection::{Count, Counter, CounterUpdate, Locatable}; +use crate::layout::{ + AlignElem, Alignment, BlockElem, InlineElem, OuterHAlignment, SpecificAlignment, + VAlignment, +}; +use crate::math::{MathSize, MathVariant}; +use crate::model::{Numbering, Outlinable, ParLine, Refable, Supplement}; +use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem}; + +/// A mathematical equation. +/// +/// Can be displayed inline with text or as a separate block. +/// +/// # Example +/// ```example +/// #set text(font: "New Computer Modern") +/// +/// Let $a$, $b$, and $c$ be the side +/// lengths of right-angled triangle. +/// Then, we know that: +/// $ a^2 + b^2 = c^2 $ +/// +/// Prove by induction: +/// $ sum_(k=1)^n k = (n(n+1)) / 2 $ +/// ``` +/// +/// By default, block-level equations will not break across pages. This can be +/// changed through `{show math.equation: set block(breakable: true)}`. +/// +/// # Syntax +/// This function also has dedicated syntax: Write mathematical markup within +/// dollar signs to create an equation. Starting and ending the equation with at +/// least one space lifts it into a separate block that is centered +/// horizontally. For more details about math syntax, see the +/// [main math page]($category/math). +#[elem(Locatable, Synthesize, Show, ShowSet, Count, LocalName, Refable, Outlinable)] +pub struct EquationElem { + /// Whether the equation is displayed as a separate block. + #[default(false)] + pub block: bool, + + /// How to [number]($numbering) block-level equations. + /// + /// ```example + /// #set math.equation(numbering: "(1)") + /// + /// We define: + /// $ phi.alt := (1 + sqrt(5)) / 2 $ <ratio> + /// + /// With @ratio, we get: + /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $ + /// ``` + #[borrowed] + pub numbering: Option<Numbering>, + + /// The alignment of the equation numbering. + /// + /// By default, the alignment is `{end + horizon}`. For the horizontal + /// component, you can use `{right}`, `{left}`, or `{start}` and `{end}` + /// of the text direction; for the vertical component, you can use + /// `{top}`, `{horizon}`, or `{bottom}`. + /// + /// ```example + /// #set math.equation(numbering: "(1)", number-align: bottom) + /// + /// We can calculate: + /// $ E &= sqrt(m_0^2 + p^2) \ + /// &approx 125 "GeV" $ + /// ``` + #[default(SpecificAlignment::Both(OuterHAlignment::End, VAlignment::Horizon))] + pub number_align: SpecificAlignment<OuterHAlignment, VAlignment>, + + /// A supplement for the equation. + /// + /// For references to equations, this is added before the referenced number. + /// + /// If a function is specified, it is passed the referenced equation and + /// should return content. + /// + /// ```example + /// #set math.equation(numbering: "(1)", supplement: [Eq.]) + /// + /// We define: + /// $ phi.alt := (1 + sqrt(5)) / 2 $ <ratio> + /// + /// With @ratio, we get: + /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $ + /// ``` + pub supplement: Smart<Option<Supplement>>, + + /// The contents of the equation. + #[required] + pub body: Content, + + /// The size of the glyphs. + #[internal] + #[default(MathSize::Text)] + #[ghost] + pub size: MathSize, + + /// The style variant to select. + #[internal] + #[ghost] + pub variant: MathVariant, + + /// Affects the height of exponents. + #[internal] + #[default(false)] + #[ghost] + pub cramped: bool, + + /// Whether to use bold glyphs. + #[internal] + #[default(false)] + #[ghost] + pub bold: bool, + + /// Whether to use italic glyphs. + #[internal] + #[ghost] + pub italic: Smart<bool>, + + /// A forced class to use for all fragment. + #[internal] + #[ghost] + pub class: Option<MathClass>, +} + +impl Synthesize for Packed<EquationElem> { + fn synthesize( + &mut self, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult<()> { + let supplement = match self.as_ref().supplement(styles) { + Smart::Auto => TextElem::packed(Self::local_name_in(styles)), + Smart::Custom(None) => Content::empty(), + Smart::Custom(Some(supplement)) => { + supplement.resolve(engine, styles, [self.clone().pack()])? + } + }; + + self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement)))); + Ok(()) + } +} + +impl Show for Packed<EquationElem> { + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + if self.block(styles) { + Ok(BlockElem::multi_layouter( + self.clone(), + engine.routines.layout_equation_block, + ) + .pack() + .spanned(self.span())) + } else { + Ok(InlineElem::layouter(self.clone(), engine.routines.layout_equation_inline) + .pack() + .spanned(self.span())) + } + } +} + +impl ShowSet for Packed<EquationElem> { + fn show_set(&self, styles: StyleChain) -> Styles { + let mut out = Styles::new(); + if self.block(styles) { + out.set(AlignElem::set_alignment(Alignment::CENTER)); + out.set(BlockElem::set_breakable(false)); + out.set(ParLine::set_numbering(None)); + out.set(EquationElem::set_size(MathSize::Display)); + } else { + out.set(EquationElem::set_size(MathSize::Text)); + } + out.set(TextElem::set_weight(FontWeight::from_number(450))); + out.set(TextElem::set_font(FontList(vec![FontFamily::new( + "New Computer Modern Math", + )]))); + out + } +} + +impl Count for Packed<EquationElem> { + fn update(&self) -> Option<CounterUpdate> { + (self.block(StyleChain::default()) && self.numbering().is_some()) + .then(|| CounterUpdate::Step(NonZeroUsize::ONE)) + } +} + +impl LocalName for Packed<EquationElem> { + const KEY: &'static str = "equation"; +} + +impl Refable for Packed<EquationElem> { + fn supplement(&self) -> Content { + // After synthesis, this should always be custom content. + match (**self).supplement(StyleChain::default()) { + Smart::Custom(Some(Supplement::Content(content))) => content, + _ => Content::empty(), + } + } + + fn counter(&self) -> Counter { + Counter::of(EquationElem::elem()) + } + + fn numbering(&self) -> Option<&Numbering> { + (**self).numbering(StyleChain::default()).as_ref() + } +} + +impl Outlinable for Packed<EquationElem> { + fn outline( + &self, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult<Option<Content>> { + if !self.block(StyleChain::default()) { + return Ok(None); + } + let Some(numbering) = self.numbering() else { + return Ok(None); + }; + + // After synthesis, this should always be custom content. + let mut supplement = match (**self).supplement(StyleChain::default()) { + Smart::Custom(Some(Supplement::Content(content))) => content, + _ => Content::empty(), + }; + + if !supplement.is_empty() { + supplement += TextElem::packed("\u{a0}"); + } + + let numbers = self.counter().display_at_loc( + engine, + self.location().unwrap(), + styles, + numbering, + )?; + + Ok(Some(supplement + numbers)) + } +} diff --git a/crates/typst-library/src/math/frac.rs b/crates/typst-library/src/math/frac.rs new file mode 100644 index 00000000..f5c4514d --- /dev/null +++ b/crates/typst-library/src/math/frac.rs @@ -0,0 +1,56 @@ +use typst_syntax::Spanned; + +use crate::diag::bail; +use crate::foundations::{elem, Content, Value}; +use crate::math::Mathy; + +/// A mathematical fraction. +/// +/// # Example +/// ```example +/// $ 1/2 < (x+1)/2 $ +/// $ ((x+1)) / 2 = frac(a, b) $ +/// ``` +/// +/// # Syntax +/// This function also has dedicated syntax: Use a slash to turn neighbouring +/// expressions into a fraction. Multiple atoms can be grouped into a single +/// expression using round grouping parenthesis. Such parentheses are removed +/// from the output, but you can nest multiple to force them. +#[elem(title = "Fraction", Mathy)] +pub struct FracElem { + /// The fraction's numerator. + #[required] + pub num: Content, + + /// The fraction's denominator. + #[required] + pub denom: Content, +} + +/// A binomial expression. +/// +/// # Example +/// ```example +/// $ binom(n, k) $ +/// $ binom(n, k_1, k_2, k_3, ..., k_m) $ +/// ``` +#[elem(title = "Binomial", Mathy)] +pub struct BinomElem { + /// The binomial's upper index. + #[required] + pub upper: Content, + + /// The binomial's lower index. + #[required] + #[variadic] + #[parse( + let values = args.all::<Spanned<Value>>()?; + if values.is_empty() { + // Prevents one element binomials + bail!(args.span, "missing argument: lower"); + } + values.into_iter().map(|spanned| spanned.v.display()).collect() + )] + pub lower: Vec<Content>, +} diff --git a/crates/typst-library/src/math/lr.rs b/crates/typst-library/src/math/lr.rs new file mode 100644 index 00000000..07ab0dd4 --- /dev/null +++ b/crates/typst-library/src/math/lr.rs @@ -0,0 +1,135 @@ +use crate::foundations::{elem, func, Content, NativeElement, Smart}; +use crate::layout::{Length, Rel}; +use crate::math::Mathy; +use crate::text::TextElem; + +/// Scales delimiters. +/// +/// While matched delimiters scale by default, this can be used to scale +/// unmatched delimiters and to control the delimiter scaling more precisely. +#[elem(title = "Left/Right", Mathy)] +pub struct LrElem { + /// The size of the brackets, relative to the height of the wrapped content. + pub size: Smart<Rel<Length>>, + + /// The delimited content, including the delimiters. + #[required] + #[parse( + let mut arguments = args.all::<Content>()?.into_iter(); + let mut body = arguments.next().unwrap_or_default(); + arguments.for_each(|arg| body += TextElem::packed(',') + arg); + body + )] + pub body: Content, +} + +/// Scales delimiters vertically to the nearest surrounding `{lr()}` group. +/// +/// ```example +/// $ { x mid(|) sum_(i=1)^n w_i|f_i (x)| < 1 } $ +/// ``` +#[elem(Mathy)] +pub struct MidElem { + /// The content to be scaled. + #[required] + pub body: Content, +} + +/// Floors an expression. +/// +/// ```example +/// $ floor(x/2) $ +/// ``` +#[func] +pub fn floor( + /// The size of the brackets, relative to the height of the wrapped content. + #[named] + size: Option<Smart<Rel<Length>>>, + /// The expression to floor. + body: Content, +) -> Content { + delimited(body, '⌊', '⌋', size) +} + +/// Ceils an expression. +/// +/// ```example +/// $ ceil(x/2) $ +/// ``` +#[func] +pub fn ceil( + /// The size of the brackets, relative to the height of the wrapped content. + #[named] + size: Option<Smart<Rel<Length>>>, + /// The expression to ceil. + body: Content, +) -> Content { + delimited(body, '⌈', '⌉', size) +} + +/// Rounds an expression. +/// +/// ```example +/// $ round(x/2) $ +/// ``` +#[func] +pub fn round( + /// The size of the brackets, relative to the height of the wrapped content. + #[named] + size: Option<Smart<Rel<Length>>>, + /// The expression to round. + body: Content, +) -> Content { + delimited(body, '⌊', '⌉', size) +} + +/// Takes the absolute value of an expression. +/// +/// ```example +/// $ abs(x/2) $ +/// ``` +#[func] +pub fn abs( + /// The size of the brackets, relative to the height of the wrapped content. + #[named] + size: Option<Smart<Rel<Length>>>, + /// The expression to take the absolute value of. + body: Content, +) -> Content { + delimited(body, '|', '|', size) +} + +/// Takes the norm of an expression. +/// +/// ```example +/// $ norm(x/2) $ +/// ``` +#[func] +pub fn norm( + /// The size of the brackets, relative to the height of the wrapped content. + #[named] + size: Option<Smart<Rel<Length>>>, + /// The expression to take the norm of. + body: Content, +) -> Content { + delimited(body, '‖', '‖', size) +} + +fn delimited( + body: Content, + left: char, + right: char, + size: Option<Smart<Rel<Length>>>, +) -> Content { + let span = body.span(); + let mut elem = LrElem::new(Content::sequence([ + TextElem::packed(left), + body, + TextElem::packed(right), + ])); + // Push size only if size is provided + if let Some(size) = size { + elem.push_size(size); + } + elem.pack().spanned(span) +} diff --git a/crates/typst-library/src/math/matrix.rs b/crates/typst-library/src/math/matrix.rs new file mode 100644 index 00000000..1c788995 --- /dev/null +++ b/crates/typst-library/src/math/matrix.rs @@ -0,0 +1,448 @@ +use smallvec::{smallvec, SmallVec}; +use typst_syntax::Spanned; +use typst_utils::Numeric; +use unicode_math_class::MathClass; + +use crate::diag::{bail, At, HintedStrResult, StrResult}; +use crate::foundations::{ + array, cast, dict, elem, Array, Content, Dict, Fold, NoneValue, Resolve, Smart, + StyleChain, Symbol, Value, +}; +use crate::layout::{Abs, Em, HAlignment, Length, Rel}; +use crate::math::Mathy; +use crate::visualize::Stroke; + +const DEFAULT_ROW_GAP: Em = Em::new(0.2); +const DEFAULT_COL_GAP: Em = Em::new(0.5); + +/// A column vector. +/// +/// Content in the vector's elements can be aligned with the +/// [`align`]($math.vec.align) parameter, or the `&` symbol. +/// +/// # Example +/// ```example +/// $ vec(a, b, c) dot vec(1, 2, 3) +/// = a + 2b + 3c $ +/// ``` +#[elem(title = "Vector", Mathy)] +pub struct VecElem { + /// The delimiter to use. + /// + /// Can be a single character specifying the left delimiter, in which case + /// the right delimiter is inferred. Otherwise, can be an array containing a + /// left and a right delimiter. + /// + /// ```example + /// #set math.vec(delim: "[") + /// $ vec(1, 2) $ + /// ``` + #[default(DelimiterPair::PAREN)] + pub delim: DelimiterPair, + + /// The horizontal alignment that each element should have. + /// + /// ```example + /// #set math.vec(align: right) + /// $ vec(-1, 1, -1) $ + /// ``` + #[resolve] + #[default(HAlignment::Center)] + pub align: HAlignment, + + /// The gap between elements. + /// + /// ```example + /// #set math.vec(gap: 1em) + /// $ vec(1, 2) $ + /// ``` + #[default(DEFAULT_ROW_GAP.into())] + pub gap: Rel<Length>, + + /// The elements of the vector. + #[variadic] + pub children: Vec<Content>, +} + +/// A matrix. +/// +/// The elements of a row should be separated by commas, while the rows +/// themselves should be separated by semicolons. The semicolon syntax merges +/// preceding arguments separated by commas into an array. You can also use this +/// special syntax of math function calls to define custom functions that take +/// 2D data. +/// +/// Content in cells can be aligned with the [`align`]($math.mat.align) +/// parameter, or content in cells that are in the same row can be aligned with +/// the `&` symbol. +/// +/// # Example +/// ```example +/// $ mat( +/// 1, 2, ..., 10; +/// 2, 2, ..., 10; +/// dots.v, dots.v, dots.down, dots.v; +/// 10, 10, ..., 10; +/// ) $ +/// ``` +#[elem(title = "Matrix", Mathy)] +pub struct MatElem { + /// The delimiter to use. + /// + /// Can be a single character specifying the left delimiter, in which case + /// the right delimiter is inferred. Otherwise, can be an array containing a + /// left and a right delimiter. + /// + /// ```example + /// #set math.mat(delim: "[") + /// $ mat(1, 2; 3, 4) $ + /// ``` + #[default(DelimiterPair::PAREN)] + pub delim: DelimiterPair, + + /// The horizontal alignment that each cell should have. + /// + /// ```example + /// #set math.mat(align: right) + /// $ mat(-1, 1, 1; 1, -1, 1; 1, 1, -1) $ + /// ``` + #[resolve] + #[default(HAlignment::Center)] + pub align: HAlignment, + + /// Draws augmentation lines in a matrix. + /// + /// - `{none}`: No lines are drawn. + /// - A single number: A vertical augmentation line is drawn + /// after the specified column number. Negative numbers start from the end. + /// - A dictionary: With a dictionary, multiple augmentation lines can be + /// drawn both horizontally and vertically. Additionally, the style of the + /// lines can be set. The dictionary can contain the following keys: + /// - `hline`: The offsets at which horizontal lines should be drawn. + /// For example, an offset of `2` would result in a horizontal line + /// being drawn after the second row of the matrix. Accepts either an + /// integer for a single line, or an array of integers + /// for multiple lines. Like for a single number, negative numbers start from the end. + /// - `vline`: The offsets at which vertical lines should be drawn. + /// For example, an offset of `2` would result in a vertical line being + /// drawn after the second column of the matrix. Accepts either an + /// integer for a single line, or an array of integers + /// for multiple lines. Like for a single number, negative numbers start from the end. + /// - `stroke`: How to [stroke]($stroke) the line. If set to `{auto}`, + /// takes on a thickness of 0.05em and square line caps. + /// + /// ```example + /// $ mat(1, 0, 1; 0, 1, 2; augment: #2) $ + /// // Equivalent to: + /// $ mat(1, 0, 1; 0, 1, 2; augment: #(-1)) $ + /// ``` + /// + /// ```example + /// $ mat(0, 0, 0; 1, 1, 1; augment: #(hline: 1, stroke: 2pt + green)) $ + /// ``` + #[resolve] + #[fold] + pub augment: Option<Augment>, + + /// The gap between rows and columns. + /// + /// This is a shorthand to set `row-gap` and `column-gap` to the same value. + /// + /// ```example + /// #set math.mat(gap: 1em) + /// $ mat(1, 2; 3, 4) $ + /// ``` + #[external] + pub gap: Rel<Length>, + + /// The gap between rows. + /// + /// ```example + /// #set math.mat(row-gap: 1em) + /// $ mat(1, 2; 3, 4) $ + /// ``` + #[parse( + let gap = args.named("gap")?; + args.named("row-gap")?.or(gap) + )] + #[default(DEFAULT_ROW_GAP.into())] + pub row_gap: Rel<Length>, + + /// The gap between columns. + /// + /// ```example + /// #set math.mat(column-gap: 1em) + /// $ mat(1, 2; 3, 4) $ + /// ``` + #[parse(args.named("column-gap")?.or(gap))] + #[default(DEFAULT_COL_GAP.into())] + pub column_gap: Rel<Length>, + + /// An array of arrays with the rows of the matrix. + /// + /// ```example + /// #let data = ((1, 2, 3), (4, 5, 6)) + /// #let matrix = math.mat(..data) + /// $ v := matrix $ + /// ``` + #[variadic] + #[parse( + let mut rows = vec![]; + let mut width = 0; + + let values = args.all::<Spanned<Value>>()?; + if values.iter().any(|spanned| matches!(spanned.v, Value::Array(_))) { + for Spanned { v, span } in values { + let array = v.cast::<Array>().at(span)?; + let row: Vec<_> = array.into_iter().map(Value::display).collect(); + width = width.max(row.len()); + rows.push(row); + } + } else { + rows = vec![values.into_iter().map(|spanned| spanned.v.display()).collect()]; + } + + for row in &mut rows { + if row.len() < width { + row.resize(width, Content::empty()); + } + } + + rows + )] + pub rows: Vec<Vec<Content>>, +} + +/// A case distinction. +/// +/// Content across different branches can be aligned with the `&` symbol. +/// +/// # Example +/// ```example +/// $ f(x, y) := cases( +/// 1 "if" (x dot y)/2 <= 0, +/// 2 "if" x "is even", +/// 3 "if" x in NN, +/// 4 "else", +/// ) $ +/// ``` +#[elem(Mathy)] +pub struct CasesElem { + /// The delimiter to use. + /// + /// Can be a single character specifying the left delimiter, in which case + /// the right delimiter is inferred. Otherwise, can be an array containing a + /// left and a right delimiter. + /// + /// ```example + /// #set math.cases(delim: "[") + /// $ x = cases(1, 2) $ + /// ``` + #[default(DelimiterPair::BRACE)] + pub delim: DelimiterPair, + + /// Whether the direction of cases should be reversed. + /// + /// ```example + /// #set math.cases(reverse: true) + /// $ cases(1, 2) = x $ + /// ``` + #[default(false)] + pub reverse: bool, + + /// The gap between branches. + /// + /// ```example + /// #set math.cases(gap: 1em) + /// $ x = cases(1, 2) $ + /// ``` + #[default(DEFAULT_ROW_GAP.into())] + pub gap: Rel<Length>, + + /// The branches of the case distinction. + #[variadic] + pub children: Vec<Content>, +} + +/// A delimiter is a single character that is used to delimit a matrix, vector +/// or cases. The character has to be a Unicode codepoint tagged as a math +/// "opening", "closing" or "fence". +/// +/// Typically, the delimiter is stretched to fit the height of whatever it +/// delimits. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Delimiter(Option<char>); + +cast! { + Delimiter, + self => self.0.into_value(), + _: NoneValue => Self::none(), + v: Symbol => Self::char(v.get())?, + v: char => Self::char(v)?, +} + +impl Delimiter { + pub fn none() -> Self { + Self(None) + } + + pub fn char(c: char) -> StrResult<Self> { + if !matches!( + unicode_math_class::class(c), + Some(MathClass::Opening | MathClass::Closing | MathClass::Fence), + ) { + bail!("invalid delimiter: \"{}\"", c) + } + Ok(Self(Some(c))) + } + + pub fn get(self) -> Option<char> { + self.0 + } + + pub fn find_matching(self) -> Self { + match self.0 { + None => Self::none(), + Some('[') => Self(Some(']')), + Some(']') => Self(Some('[')), + Some('{') => Self(Some('}')), + Some('}') => Self(Some('{')), + Some(c) => match unicode_math_class::class(c) { + Some(MathClass::Opening) => Self(char::from_u32(c as u32 + 1)), + Some(MathClass::Closing) => Self(char::from_u32(c as u32 - 1)), + _ => Self(Some(c)), + }, + } + } +} + +/// A pair of delimiters (one closing, one opening) used for matrices, vectors +/// and cases. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct DelimiterPair { + open: Delimiter, + close: Delimiter, +} + +cast! { + DelimiterPair, + + self => array![self.open, self.close].into_value(), + + v: Array => match v.as_slice() { + [open, close] => Self { + open: open.clone().cast()?, + close: close.clone().cast()?, + }, + _ => bail!("expected 2 delimiters, found {}", v.len()) + }, + v: Delimiter => Self { open: v, close: v.find_matching() } +} + +impl DelimiterPair { + const PAREN: Self = Self { + open: Delimiter(Some('(')), + close: Delimiter(Some(')')), + }; + const BRACE: Self = Self { + open: Delimiter(Some('{')), + close: Delimiter(Some('}')), + }; + + /// The delimiter's opening character. + pub fn open(self) -> Option<char> { + self.open.get() + } + + /// The delimiter's closing character. + pub fn close(self) -> Option<char> { + self.close.get() + } +} + +/// Parameters specifying how augmentation lines +/// should be drawn on a matrix. +#[derive(Debug, Default, Clone, PartialEq, Hash)] +pub struct Augment<T: Numeric = Length> { + pub hline: AugmentOffsets, + pub vline: AugmentOffsets, + pub stroke: Smart<Stroke<T>>, +} + +impl<T: Numeric + Fold> Fold for Augment<T> { + fn fold(self, outer: Self) -> Self { + Self { + stroke: match (self.stroke, outer.stroke) { + (Smart::Custom(inner), Smart::Custom(outer)) => { + Smart::Custom(inner.fold(outer)) + } + // Usually, folding an inner `auto` with an `outer` prefers + // the explicit `auto`. However, here `auto` means unspecified + // and thus we want `outer`. + (inner, outer) => inner.or(outer), + }, + ..self + } + } +} + +impl Resolve for Augment { + type Output = Augment<Abs>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + Augment { + hline: self.hline, + vline: self.vline, + stroke: self.stroke.resolve(styles), + } + } +} + +cast! { + Augment, + self => { + // if the stroke is auto and there is only one vertical line, + if self.stroke.is_auto() && self.hline.0.is_empty() && self.vline.0.len() == 1 { + return self.vline.0[0].into_value(); + } + + dict! { + "hline" => self.hline, + "vline" => self.vline, + "stroke" => self.stroke, + }.into_value() + }, + v: isize => Augment { + hline: AugmentOffsets::default(), + vline: AugmentOffsets(smallvec![v]), + stroke: Smart::Auto, + }, + mut dict: Dict => { + let mut take = |key| dict.take(key).ok().map(AugmentOffsets::from_value).transpose(); + let hline = take("hline")?.unwrap_or_default(); + let vline = take("vline")?.unwrap_or_default(); + let stroke = dict.take("stroke") + .ok() + .map(Stroke::from_value) + .transpose()? + .map(Smart::Custom) + .unwrap_or(Smart::Auto); + Augment { hline, vline, stroke } + }, +} + +cast! { + Augment<Abs>, + self => self.into_value(), +} + +/// The offsets at which augmentation lines should be drawn on a matrix. +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct AugmentOffsets(pub SmallVec<[isize; 1]>); + +cast! { + AugmentOffsets, + self => self.0.into_value(), + v: isize => Self(smallvec![v]), + v: Array => Self(v.into_iter().map(Value::cast).collect::<HintedStrResult<_>>()?), +} diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs new file mode 100644 index 00000000..53b1f072 --- /dev/null +++ b/crates/typst-library/src/math/mod.rs @@ -0,0 +1,257 @@ +//! Mathematical formulas. + +pub mod accent; +mod attach; +mod cancel; +mod equation; +mod frac; +mod lr; +mod matrix; +mod op; +mod root; +mod style; +mod underover; + +pub use self::accent::{Accent, AccentElem}; +pub use self::attach::*; +pub use self::cancel::*; +pub use self::equation::*; +pub use self::frac::*; +pub use self::lr::*; +pub use self::matrix::*; +pub use self::op::*; +pub use self::root::*; +pub use self::style::*; +pub use self::underover::*; + +use typst_utils::singleton; +use unicode_math_class::MathClass; + +use crate::foundations::{ + category, elem, Category, Content, Module, NativeElement, Scope, +}; +use crate::layout::{Em, HElem}; +use crate::text::TextElem; + +/// Typst has special [syntax]($syntax/#math) and library functions to typeset +/// mathematical formulas. Math formulas can be displayed inline with text or as +/// separate blocks. They will be typeset into their own block if they start and +/// end with at least one space (e.g. `[$ x^2 $]`). +/// +/// # Variables +/// In math, single letters are always displayed as is. Multiple letters, +/// however, are interpreted as variables and functions. To display multiple +/// letters verbatim, you can place them into quotes and to access single letter +/// variables, you can use the [hash syntax]($scripting/#expressions). +/// +/// ```example +/// $ A = pi r^2 $ +/// $ "area" = pi dot "radius"^2 $ +/// $ cal(A) := +/// { x in RR | x "is natural" } $ +/// #let x = 5 +/// $ #x < 17 $ +/// ``` +/// +/// # Symbols +/// Math mode makes a wide selection of [symbols]($category/symbols/sym) like +/// `pi`, `dot`, or `RR` available. Many mathematical symbols are available in +/// different variants. You can select between different variants by applying +/// [modifiers]($symbol) to the symbol. Typst further recognizes a number of +/// shorthand sequences like `=>` that approximate a symbol. When such a +/// shorthand exists, the symbol's documentation lists it. +/// +/// ```example +/// $ x < y => x gt.eq.not y $ +/// ``` +/// +/// # Line Breaks +/// Formulas can also contain line breaks. Each line can contain one or multiple +/// _alignment points_ (`&`) which are then aligned. +/// +/// ```example +/// $ sum_(k=0)^n k +/// &= 1 + ... + n \ +/// &= (n(n+1)) / 2 $ +/// ``` +/// +/// # Function calls +/// Math mode supports special function calls without the hash prefix. In these +/// "math calls", the argument list works a little differently than in code: +/// +/// - Within them, Typst is still in "math mode". Thus, you can write math +/// directly into them, but need to use hash syntax to pass code expressions +/// (except for strings, which are available in the math syntax). +/// - They support positional and named arguments, but don't support trailing +/// content blocks and argument spreading. +/// - They provide additional syntax for 2-dimensional argument lists. The +/// semicolon (`;`) merges preceding arguments separated by commas into an +/// array argument. +/// +/// ```example +/// $ frac(a^2, 2) $ +/// $ vec(1, 2, delim: "[") $ +/// $ mat(1, 2; 3, 4) $ +/// $ lim_x = +/// op("lim", limits: #true)_x $ +/// ``` +/// +/// To write a verbatim comma or semicolon in a math call, escape it with a +/// backslash. The colon on the other hand is only recognized in a special way +/// if directly preceded by an identifier, so to display it verbatim in those +/// cases, you can just insert a space before it. +/// +/// Functions calls preceded by a hash are normal code function calls and not +/// affected by these rules. +/// +/// # Alignment +/// When equations include multiple _alignment points_ (`&`), this creates +/// blocks of alternatingly right- and left-aligned columns. In the example +/// below, the expression `(3x + y) / 7` is right-aligned and `= 9` is +/// left-aligned. The word "given" is also left-aligned because `&&` creates two +/// alignment points in a row, alternating the alignment twice. `& &` and `&&` +/// behave exactly the same way. Meanwhile, "multiply by 7" is right-aligned +/// because just one `&` precedes it. Each alignment point simply alternates +/// between right-aligned/left-aligned. +/// +/// ```example +/// $ (3x + y) / 7 &= 9 && "given" \ +/// 3x + y &= 63 & "multiply by 7" \ +/// 3x &= 63 - y && "subtract y" \ +/// x &= 21 - y/3 & "divide by 3" $ +/// ``` +/// +/// # Math fonts +/// You can set the math font by with a [show-set rule]($styling/#show-rules) as +/// demonstrated below. Note that only special OpenType math fonts are suitable +/// for typesetting maths. +/// +/// ```example +/// #show math.equation: set text(font: "Fira Math") +/// $ sum_(i in NN) 1 + i $ +/// ``` +/// +/// # Math module +/// All math functions are part of the `math` [module]($scripting/#modules), +/// which is available by default in equations. Outside of equations, they can +/// be accessed with the `math.` prefix. +#[category] +pub static MATH: Category; + +// Spacings. +pub const THIN: Em = Em::new(1.0 / 6.0); +pub const MEDIUM: Em = Em::new(2.0 / 9.0); +pub const THICK: Em = Em::new(5.0 / 18.0); +pub const QUAD: Em = Em::new(1.0); +pub const WIDE: Em = Em::new(2.0); + +/// Create a module with all math definitions. +pub fn module() -> Module { + let mut math = Scope::deduplicating(); + math.category(MATH); + math.define_elem::<EquationElem>(); + math.define_elem::<TextElem>(); + math.define_elem::<LrElem>(); + math.define_elem::<MidElem>(); + math.define_elem::<AttachElem>(); + math.define_elem::<StretchElem>(); + math.define_elem::<ScriptsElem>(); + math.define_elem::<LimitsElem>(); + math.define_elem::<AccentElem>(); + math.define_elem::<UnderlineElem>(); + math.define_elem::<OverlineElem>(); + math.define_elem::<UnderbraceElem>(); + math.define_elem::<OverbraceElem>(); + math.define_elem::<UnderbracketElem>(); + math.define_elem::<OverbracketElem>(); + math.define_elem::<UnderparenElem>(); + math.define_elem::<OverparenElem>(); + math.define_elem::<UndershellElem>(); + math.define_elem::<OvershellElem>(); + math.define_elem::<CancelElem>(); + math.define_elem::<FracElem>(); + math.define_elem::<BinomElem>(); + math.define_elem::<VecElem>(); + math.define_elem::<MatElem>(); + math.define_elem::<CasesElem>(); + math.define_elem::<RootElem>(); + math.define_elem::<ClassElem>(); + math.define_elem::<OpElem>(); + math.define_elem::<PrimesElem>(); + math.define_func::<abs>(); + math.define_func::<norm>(); + math.define_func::<round>(); + math.define_func::<sqrt>(); + math.define_func::<upright>(); + math.define_func::<bold>(); + math.define_func::<italic>(); + math.define_func::<serif>(); + math.define_func::<sans>(); + math.define_func::<cal>(); + math.define_func::<frak>(); + math.define_func::<mono>(); + math.define_func::<bb>(); + math.define_func::<display>(); + math.define_func::<inline>(); + math.define_func::<script>(); + math.define_func::<sscript>(); + + // Text operators. + op::define(&mut math); + + // Spacings. + math.define("thin", HElem::new(THIN.into()).pack()); + math.define("med", HElem::new(MEDIUM.into()).pack()); + math.define("thick", HElem::new(THICK.into()).pack()); + math.define("quad", HElem::new(QUAD.into()).pack()); + math.define("wide", HElem::new(WIDE.into()).pack()); + + // Symbols. + for (name, symbol) in crate::symbols::SYM { + math.define(*name, symbol.clone()); + } + + Module::new("math", math) +} + +/// Trait for recognizing math elements and auto-wrapping them in equations. +pub trait Mathy {} + +/// A math alignment point: `&`, `&&`. +#[elem(title = "Alignment Point", Mathy)] +pub struct AlignPointElem {} + +impl AlignPointElem { + /// Get the globally shared alignment point element. + pub fn shared() -> &'static Content { + singleton!(Content, AlignPointElem::new().pack()) + } +} + +/// Forced use of a certain math class. +/// +/// This is useful to treat certain symbols as if they were of a different +/// class, e.g. to make a symbol behave like a relation. The class of a symbol +/// defines the way it is laid out, including spacing around it, and how its +/// scripts are attached by default. Note that the latter can always be +/// overridden using [`{limits}`](math.limits) and [`{scripts}`](math.scripts). +/// +/// # Example +/// ```example +/// #let loves = math.class( +/// "relation", +/// sym.suit.heart, +/// ) +/// +/// $x loves y and y loves 5$ +/// ``` +#[elem(Mathy)] +pub struct ClassElem { + /// The class to apply to the content. + #[required] + pub class: MathClass, + + /// The content to which the class is applied. + #[required] + pub body: Content, +} diff --git a/crates/typst-library/src/math/op.rs b/crates/typst-library/src/math/op.rs new file mode 100644 index 00000000..ef24705a --- /dev/null +++ b/crates/typst-library/src/math/op.rs @@ -0,0 +1,103 @@ +use ecow::EcoString; + +use crate::foundations::{elem, Content, NativeElement, Scope}; +use crate::layout::HElem; +use crate::math::{upright, Mathy, THIN}; +use crate::text::TextElem; + +/// A text operator in an equation. +/// +/// # Example +/// ```example +/// $ tan x = (sin x)/(cos x) $ +/// $ op("custom", +/// limits: #true)_(n->oo) n $ +/// ``` +/// +/// # Predefined Operators { #predefined } +/// Typst predefines the operators `arccos`, `arcsin`, `arctan`, `arg`, `cos`, +/// `cosh`, `cot`, `coth`, `csc`, `csch`, `ctg`, `deg`, `det`, `dim`, `exp`, +/// `gcd`, `hom`, `id`, `im`, `inf`, `ker`, `lg`, `lim`, `liminf`, `limsup`, +/// `ln`, `log`, `max`, `min`, `mod`, `Pr`, `sec`, `sech`, `sin`, `sinc`, +/// `sinh`, `sup`, `tan`, `tanh`, `tg` and `tr`. +#[elem(title = "Text Operator", Mathy)] +pub struct OpElem { + /// The operator's text. + #[required] + pub text: Content, + + /// Whether the operator should show attachments as limits in display mode. + #[default(false)] + pub limits: bool, +} + +macro_rules! ops { + ($($name:ident $(: $value:literal)? $(($tts:tt))?),* $(,)?) => { + pub(super) fn define(math: &mut Scope) { + $({ + let operator = EcoString::from(ops!(@name $name $(: $value)?)); + math.define( + stringify!($name), + OpElem::new(TextElem::new(operator).into()) + .with_limits(ops!(@limit $($tts)*)) + .pack() + ); + })* + + let dif = |d| { + HElem::new(THIN.into()).with_weak(true).pack() + + upright(TextElem::packed(d)) + }; + math.define("dif", dif('d')); + math.define("Dif", dif('D')); + } + }; + (@name $name:ident) => { stringify!($name) }; + (@name $name:ident: $value:literal) => { $value }; + (@limit limits) => { true }; + (@limit) => { false }; +} + +ops! { + arccos, + arcsin, + arctan, + arg, + cos, + cosh, + cot, + coth, + csc, + csch, + ctg, + deg, + det (limits), + dim, + exp, + gcd (limits), + hom, + id, + im, + inf (limits), + ker, + lg, + lim (limits), + liminf: "lim inf" (limits), + limsup: "lim sup" (limits), + ln, + log, + max (limits), + min (limits), + mod, + Pr (limits), + sec, + sech, + sin, + sinc, + sinh, + sup (limits), + tan, + tanh, + tg, + tr, +} diff --git a/crates/typst-library/src/math/root.rs b/crates/typst-library/src/math/root.rs new file mode 100644 index 00000000..e25c6d42 --- /dev/null +++ b/crates/typst-library/src/math/root.rs @@ -0,0 +1,35 @@ +use typst_syntax::Span; + +use crate::foundations::{elem, func, Content, NativeElement}; +use crate::math::Mathy; + +/// A square root. +/// +/// ```example +/// $ sqrt(3 - 2 sqrt(2)) = sqrt(2) - 1 $ +/// ``` +#[func(title = "Square Root")] +pub fn sqrt( + /// The call span of this function. + span: Span, + /// The expression to take the square root of. + radicand: Content, +) -> Content { + RootElem::new(radicand).pack().spanned(span) +} + +/// A general root. +/// +/// ```example +/// $ root(3, x) $ +/// ``` +#[elem(Mathy)] +pub struct RootElem { + /// Which root of the radicand to take. + #[positional] + pub index: Option<Content>, + + /// The expression to take the root of. + #[required] + pub radicand: Content, +} diff --git a/crates/typst-library/src/math/style.rs b/crates/typst-library/src/math/style.rs new file mode 100644 index 00000000..f3d28f2a --- /dev/null +++ b/crates/typst-library/src/math/style.rs @@ -0,0 +1,254 @@ +use crate::foundations::{func, Cast, Content, Smart}; +use crate::math::EquationElem; + +/// Bold font style in math. +/// +/// ```example +/// $ bold(A) := B^+ $ +/// ``` +#[func(keywords = ["mathbf"])] +pub fn bold( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_bold(true)) +} + +/// Upright (non-italic) font style in math. +/// +/// ```example +/// $ upright(A) != A $ +/// ``` +#[func(keywords = ["mathup"])] +pub fn upright( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_italic(Smart::Custom(false))) +} + +/// Italic font style in math. +/// +/// For roman letters and greek lowercase letters, this is already the default. +#[func(keywords = ["mathit"])] +pub fn italic( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_italic(Smart::Custom(true))) +} + +/// Serif (roman) font style in math. +/// +/// This is already the default. +#[func(keywords = ["mathrm"])] +pub fn serif( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_variant(MathVariant::Serif)) +} + +/// Sans-serif font style in math. +/// +/// ```example +/// $ sans(A B C) $ +/// ``` +#[func(title = "Sans Serif", keywords = ["mathsf"])] +pub fn sans( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_variant(MathVariant::Sans)) +} + +/// Calligraphic font style in math. +/// +/// ```example +/// Let $cal(P)$ be the set of ... +/// ``` +/// +/// This corresponds both to LaTeX's `\mathcal` and `\mathscr` as both of these +/// styles share the same Unicode codepoints. Switching between the styles is +/// thus only possible if supported by the font via +/// [font features]($text.features). +/// +/// For the default math font, the roundhand style is available through the +/// `ss01` feature. Therefore, you could define your own version of `\mathscr` +/// like this: +/// +/// ```example +/// #let scr(it) = text( +/// features: ("ss01",), +/// box($cal(it)$), +/// ) +/// +/// We establish $cal(P) != scr(P)$. +/// ``` +/// +/// (The box is not conceptually necessary, but unfortunately currently needed +/// due to limitations in Typst's text style handling in math.) +#[func(title = "Calligraphic", keywords = ["mathcal", "mathscr"])] +pub fn cal( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_variant(MathVariant::Cal)) +} + +/// Fraktur font style in math. +/// +/// ```example +/// $ frak(P) $ +/// ``` +#[func(title = "Fraktur", keywords = ["mathfrak"])] +pub fn frak( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_variant(MathVariant::Frak)) +} + +/// Monospace font style in math. +/// +/// ```example +/// $ mono(x + y = z) $ +/// ``` +#[func(title = "Monospace", keywords = ["mathtt"])] +pub fn mono( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_variant(MathVariant::Mono)) +} + +/// Blackboard bold (double-struck) font style in math. +/// +/// For uppercase latin letters, blackboard bold is additionally available +/// through [symbols]($category/symbols/sym) of the form `NN` and `RR`. +/// +/// ```example +/// $ bb(b) $ +/// $ bb(N) = NN $ +/// $ f: NN -> RR $ +/// ``` +#[func(title = "Blackboard Bold", keywords = ["mathbb"])] +pub fn bb( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_variant(MathVariant::Bb)) +} + +/// Forced display style in math. +/// +/// This is the normal size for block equations. +/// +/// ```example +/// $sum_i x_i/2 = display(sum_i x_i/2)$ +/// ``` +#[func(title = "Display Size", keywords = ["displaystyle"])] +pub fn display( + /// The content to size. + body: Content, + /// Whether to impose a height restriction for exponents, like regular sub- + /// and superscripts do. + #[named] + #[default(false)] + cramped: bool, +) -> Content { + body.styled(EquationElem::set_size(MathSize::Display)) + .styled(EquationElem::set_cramped(cramped)) +} + +/// Forced inline (text) style in math. +/// +/// This is the normal size for inline equations. +/// +/// ```example +/// $ sum_i x_i/2 +/// = inline(sum_i x_i/2) $ +/// ``` +#[func(title = "Inline Size", keywords = ["textstyle"])] +pub fn inline( + /// The content to size. + body: Content, + /// Whether to impose a height restriction for exponents, like regular sub- + /// and superscripts do. + #[named] + #[default(false)] + cramped: bool, +) -> Content { + body.styled(EquationElem::set_size(MathSize::Text)) + .styled(EquationElem::set_cramped(cramped)) +} + +/// Forced script style in math. +/// +/// This is the smaller size used in powers or sub- or superscripts. +/// +/// ```example +/// $sum_i x_i/2 = script(sum_i x_i/2)$ +/// ``` +#[func(title = "Script Size", keywords = ["scriptstyle"])] +pub fn script( + /// The content to size. + body: Content, + /// Whether to impose a height restriction for exponents, like regular sub- + /// and superscripts do. + #[named] + #[default(true)] + cramped: bool, +) -> Content { + body.styled(EquationElem::set_size(MathSize::Script)) + .styled(EquationElem::set_cramped(cramped)) +} + +/// Forced second script style in math. +/// +/// This is the smallest size, used in second-level sub- and superscripts +/// (script of the script). +/// +/// ```example +/// $sum_i x_i/2 = sscript(sum_i x_i/2)$ +/// ``` +#[func(title = "Script-Script Size", keywords = ["scriptscriptstyle"])] +pub fn sscript( + /// The content to size. + body: Content, + /// Whether to impose a height restriction for exponents, like regular sub- + /// and superscripts do. + #[named] + #[default(true)] + cramped: bool, +) -> Content { + body.styled(EquationElem::set_size(MathSize::ScriptScript)) + .styled(EquationElem::set_cramped(cramped)) +} + +/// The size of elements in an equation. +/// +/// See the TeXbook p. 141. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Cast, Hash)] +pub enum MathSize { + /// Second-level sub- and superscripts. + ScriptScript, + /// Sub- and superscripts. + Script, + /// Math in text. + Text, + /// Math on its own line. + Display, +} + +/// A mathematical style variant, as defined by Unicode. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Cast, Hash)] +pub enum MathVariant { + #[default] + Serif, + Sans, + Cal, + Frak, + Mono, + Bb, +} diff --git a/crates/typst-library/src/math/underover.rs b/crates/typst-library/src/math/underover.rs new file mode 100644 index 00000000..302c51af --- /dev/null +++ b/crates/typst-library/src/math/underover.rs @@ -0,0 +1,156 @@ +use crate::foundations::{elem, Content}; +use crate::math::Mathy; + +/// A horizontal line under content. +/// +/// ```example +/// $ underline(1 + 2 + ... + 5) $ +/// ``` +#[elem(Mathy)] +pub struct UnderlineElem { + /// The content above the line. + #[required] + pub body: Content, +} + +/// A horizontal line over content. +/// +/// ```example +/// $ overline(1 + 2 + ... + 5) $ +/// ``` +#[elem(Mathy)] +pub struct OverlineElem { + /// The content below the line. + #[required] + pub body: Content, +} + +/// A horizontal brace under content, with an optional annotation below. +/// +/// ```example +/// $ underbrace(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct UnderbraceElem { + /// The content above the brace. + #[required] + pub body: Content, + + /// The optional content below the brace. + #[positional] + pub annotation: Option<Content>, +} + +/// A horizontal brace over content, with an optional annotation above. +/// +/// ```example +/// $ overbrace(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct OverbraceElem { + /// The content below the brace. + #[required] + pub body: Content, + + /// The optional content above the brace. + #[positional] + pub annotation: Option<Content>, +} + +/// A horizontal bracket under content, with an optional annotation below. +/// +/// ```example +/// $ underbracket(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct UnderbracketElem { + /// The content above the bracket. + #[required] + pub body: Content, + + /// The optional content below the bracket. + #[positional] + pub annotation: Option<Content>, +} + +/// A horizontal bracket over content, with an optional annotation above. +/// +/// ```example +/// $ overbracket(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct OverbracketElem { + /// The content below the bracket. + #[required] + pub body: Content, + + /// The optional content above the bracket. + #[positional] + pub annotation: Option<Content>, +} + +/// A horizontal parenthesis under content, with an optional annotation below. +/// +/// ```example +/// $ underparen(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct UnderparenElem { + /// The content above the parenthesis. + #[required] + pub body: Content, + + /// The optional content below the parenthesis. + #[positional] + pub annotation: Option<Content>, +} + +/// A horizontal parenthesis over content, with an optional annotation above. +/// +/// ```example +/// $ overparen(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct OverparenElem { + /// The content below the parenthesis. + #[required] + pub body: Content, + + /// The optional content above the parenthesis. + #[positional] + pub annotation: Option<Content>, +} + +/// A horizontal tortoise shell bracket under content, with an optional +/// annotation below. +/// +/// ```example +/// $ undershell(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct UndershellElem { + /// The content above the tortoise shell bracket. + #[required] + pub body: Content, + + /// The optional content below the tortoise shell bracket. + #[positional] + pub annotation: Option<Content>, +} + +/// A horizontal tortoise shell bracket over content, with an optional +/// annotation above. +/// +/// ```example +/// $ overshell(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct OvershellElem { + /// The content below the tortoise shell bracket. + #[required] + pub body: Content, + + /// The optional content above the tortoise shell bracket. + #[positional] + pub annotation: Option<Content>, +} diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs new file mode 100644 index 00000000..d11055b9 --- /dev/null +++ b/crates/typst-library/src/model/bibliography.rs @@ -0,0 +1,1101 @@ +use std::collections::HashMap; +use std::ffi::OsStr; +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::num::NonZeroUsize; +use std::path::Path; +use std::sync::Arc; + +use comemo::Tracked; +use ecow::{eco_format, EcoString, EcoVec}; +use hayagriva::archive::ArchivedStyle; +use hayagriva::io::BibLaTeXError; +use hayagriva::{ + citationberg, BibliographyDriver, BibliographyRequest, CitationItem, CitationRequest, + SpecificLocator, +}; +use indexmap::IndexMap; +use once_cell::sync::Lazy; +use smallvec::{smallvec, SmallVec}; +use typed_arena::Arena; +use typst_syntax::{Span, Spanned}; +use typst_utils::{LazyHash, NonZeroExt, PicoStr}; + +use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, ty, Args, Array, Bytes, CastInfo, Content, FromValue, IntoValue, Label, + NativeElement, Packed, Reflect, Repr, Scope, Show, ShowSet, Smart, Str, StyleChain, + Styles, Synthesize, Type, Value, +}; +use crate::introspection::{Introspector, Locatable, Location}; +use crate::layout::{ + BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem, + Sizing, TrackSizings, VElem, +}; +use crate::model::{ + CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem, + Url, +}; +use crate::routines::{EvalMode, Routines}; +use crate::text::{ + FontStyle, Lang, LocalName, Region, SubElem, SuperElem, TextElem, WeightDelta, +}; +use crate::World; + +/// A bibliography / reference listing. +/// +/// You can create a new bibliography by calling this function with a path +/// to a bibliography file in either one of two formats: +/// +/// - A Hayagriva `.yml` file. Hayagriva is a new bibliography file format +/// designed for use with Typst. Visit its +/// [documentation](https://github.com/typst/hayagriva/blob/main/docs/file-format.md) +/// for more details. +/// - A BibLaTeX `.bib` file. +/// +/// As soon as you add a bibliography somewhere in your document, you can start +/// citing things with reference syntax (`[@key]`) or explicit calls to the +/// [citation]($cite) function (`[#cite(<key>)]`). The bibliography will only +/// show entries for works that were referenced in the document. +/// +/// # Styles +/// Typst offers a wide selection of built-in +/// [citation and bibliography styles]($bibliography.style). Beyond those, you +/// can add and use custom [CSL](https://citationstyles.org/) (Citation Style +/// Language) files. Wondering which style to use? Here are some good defaults +/// based on what discipline you're working in: +/// +/// | Fields | Typical Styles | +/// |-----------------|--------------------------------------------------------| +/// | Engineering, IT | `{"ieee"}` | +/// | Psychology, Life Sciences | `{"apa"}` | +/// | Social sciences | `{"chicago-author-date"}` | +/// | Humanities | `{"mla"}`, `{"chicago-notes"}`, `{"harvard-cite-them-right"}` | +/// | Economics | `{"harvard-cite-them-right"}` | +/// | Physics | `{"american-physics-society"}` | +/// +/// # Example +/// ```example +/// This was already noted by +/// pirates long ago. @arrgh +/// +/// Multiple sources say ... +/// @arrgh @netwok. +/// +/// #bibliography("works.bib") +/// ``` +#[elem(Locatable, Synthesize, Show, ShowSet, LocalName)] +pub struct BibliographyElem { + /// Path(s) to Hayagriva `.yml` and/or BibLaTeX `.bib` files. + #[required] + #[parse( + let (paths, bibliography) = Bibliography::parse(engine, args)?; + paths + )] + pub path: BibliographyPaths, + + /// The title of the bibliography. + /// + /// - When set to `{auto}`, an appropriate title for the + /// [text language]($text.lang) will be used. This is the default. + /// - When set to `{none}`, the bibliography will not have a title. + /// - A custom title can be set by passing content. + /// + /// The bibliography's heading will not be numbered by default, but you can + /// force it to be with a show-set rule: + /// `{show bibliography: set heading(numbering: "1.")}` + pub title: Smart<Option<Content>>, + + /// Whether to include all works from the given bibliography files, even + /// those that weren't cited in the document. + /// + /// To selectively add individual cited works without showing them, you can + /// also use the `cite` function with [`form`]($cite.form) set to `{none}`. + #[default(false)] + pub full: bool, + + /// The bibliography style. + /// + /// Should be either one of the built-in styles (see below) or a path to + /// a [CSL file](https://citationstyles.org/). Some of the styles listed + /// below appear twice, once with their full name and once with a short + /// alias. + #[parse(CslStyle::parse(engine, args)?)] + #[default(CslStyle::from_name("ieee").unwrap())] + pub style: CslStyle, + + /// The loaded bibliography. + #[internal] + #[required] + #[parse(bibliography)] + pub bibliography: Bibliography, + + /// The language setting where the bibliography is. + #[internal] + #[synthesized] + pub lang: Lang, + + /// The region setting where the bibliography is. + #[internal] + #[synthesized] + pub region: Option<Region>, +} + +/// A list of bibliography file paths. +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct BibliographyPaths(Vec<EcoString>); + +cast! { + BibliographyPaths, + self => self.0.into_value(), + v: EcoString => Self(vec![v]), + v: Array => Self(v.into_iter().map(Value::cast).collect::<HintedStrResult<_>>()?), +} + +impl BibliographyElem { + /// Find the document's bibliography. + pub fn find(introspector: Tracked<Introspector>) -> StrResult<Packed<Self>> { + let query = introspector.query(&Self::elem().select()); + let mut iter = query.iter(); + let Some(elem) = iter.next() else { + bail!("the document does not contain a bibliography"); + }; + + if iter.next().is_some() { + bail!("multiple bibliographies are not yet supported"); + } + + Ok(elem.to_packed::<Self>().unwrap().clone()) + } + + /// Whether the bibliography contains the given key. + pub fn has(engine: &Engine, key: impl Into<PicoStr>) -> bool { + let key = key.into(); + engine + .introspector + .query(&Self::elem().select()) + .iter() + .any(|elem| elem.to_packed::<Self>().unwrap().bibliography().has(key)) + } + + /// Find all bibliography keys. + pub fn keys( + introspector: Tracked<Introspector>, + ) -> Vec<(EcoString, Option<EcoString>)> { + let mut vec = vec![]; + for elem in introspector.query(&Self::elem().select()).iter() { + let this = elem.to_packed::<Self>().unwrap(); + for entry in this.bibliography().entries() { + let key = entry.key().into(); + let detail = entry.title().map(|title| title.value.to_str().into()); + vec.push((key, detail)) + } + } + vec + } +} + +impl Synthesize for Packed<BibliographyElem> { + fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> { + let elem = self.as_mut(); + elem.push_lang(TextElem::lang_in(styles)); + elem.push_region(TextElem::region_in(styles)); + Ok(()) + } +} + +impl Show for Packed<BibliographyElem> { + #[typst_macros::time(name = "bibliography", span = self.span())] + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + const COLUMN_GUTTER: Em = Em::new(0.65); + const INDENT: Em = Em::new(1.5); + + let mut seq = vec![]; + if let Some(title) = self.title(styles).unwrap_or_else(|| { + Some(TextElem::packed(Self::local_name_in(styles)).spanned(self.span())) + }) { + seq.push( + HeadingElem::new(title) + .with_depth(NonZeroUsize::ONE) + .pack() + .spanned(self.span()), + ); + } + + let span = self.span(); + let works = Works::generate(engine).at(span)?; + let references = works + .references + .as_ref() + .ok_or("CSL style is not suitable for bibliographies") + .at(span)?; + + let row_gutter = ParElem::spacing_in(styles); + let row_gutter_elem = VElem::new(row_gutter.into()).with_weak(true).pack(); + + if references.iter().any(|(prefix, _)| prefix.is_some()) { + let mut cells = vec![]; + for (prefix, reference) in references { + cells.push(GridChild::Item(GridItem::Cell( + Packed::new(GridCell::new(prefix.clone().unwrap_or_default())) + .spanned(span), + ))); + cells.push(GridChild::Item(GridItem::Cell( + Packed::new(GridCell::new(reference.clone())).spanned(span), + ))); + } + seq.push( + GridElem::new(cells) + .with_columns(TrackSizings(smallvec![Sizing::Auto; 2])) + .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()])) + .with_row_gutter(TrackSizings(smallvec![row_gutter.into()])) + .pack() + .spanned(self.span()), + ); + } else { + for (i, (_, reference)) in references.iter().enumerate() { + if i > 0 { + seq.push(row_gutter_elem.clone()); + } + seq.push(reference.clone()); + } + } + + let mut content = Content::sequence(seq); + if works.hanging_indent { + content = content.styled(ParElem::set_hanging_indent(INDENT.into())); + } + + Ok(content) + } +} + +impl ShowSet for Packed<BibliographyElem> { + fn show_set(&self, _: StyleChain) -> Styles { + const INDENT: Em = Em::new(1.0); + let mut out = Styles::new(); + out.set(HeadingElem::set_numbering(None)); + out.set(PadElem::set_left(INDENT.into())); + out + } +} + +impl LocalName for Packed<BibliographyElem> { + const KEY: &'static str = "bibliography"; +} + +/// A loaded bibliography. +#[derive(Clone, PartialEq)] +pub struct Bibliography { + map: Arc<IndexMap<PicoStr, hayagriva::Entry>>, + hash: u128, +} + +impl Bibliography { + /// Parse the bibliography argument. + fn parse( + engine: &mut Engine, + args: &mut Args, + ) -> SourceResult<(BibliographyPaths, Bibliography)> { + let Spanned { v: paths, span } = + args.expect::<Spanned<BibliographyPaths>>("path to bibliography file")?; + + // Load bibliography files. + let data = paths + .0 + .iter() + .map(|path| { + let id = span.resolve_path(path).at(span)?; + engine.world.file(id).at(span) + }) + .collect::<SourceResult<Vec<Bytes>>>()?; + + // Parse. + let bibliography = Self::load(&paths, &data).at(span)?; + + Ok((paths, bibliography)) + } + + /// Load bibliography entries from paths. + #[comemo::memoize] + #[typst_macros::time(name = "load bibliography")] + fn load(paths: &BibliographyPaths, data: &[Bytes]) -> StrResult<Bibliography> { + let mut map = IndexMap::new(); + let mut duplicates = Vec::<EcoString>::new(); + + // We might have multiple bib/yaml files + for (path, bytes) in paths.0.iter().zip(data) { + let src = std::str::from_utf8(bytes).map_err(FileError::from)?; + + let ext = Path::new(path.as_str()) + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default(); + + let library = match ext.to_lowercase().as_str() { + "yml" | "yaml" => hayagriva::io::from_yaml_str(src) + .map_err(|err| eco_format!("failed to parse YAML ({err})"))?, + "bib" => hayagriva::io::from_biblatex_str(src) + .map_err(|errors| format_biblatex_error(path, src, errors))?, + _ => bail!("unknown bibliography format (must be .yml/.yaml or .bib)"), + }; + + for entry in library { + match map.entry(entry.key().into()) { + indexmap::map::Entry::Vacant(vacant) => { + vacant.insert(entry); + } + indexmap::map::Entry::Occupied(_) => { + duplicates.push(entry.key().into()); + } + } + } + } + + if !duplicates.is_empty() { + bail!("duplicate bibliography keys: {}", duplicates.join(", ")); + } + + Ok(Bibliography { + map: Arc::new(map), + hash: typst_utils::hash128(data), + }) + } + + fn has(&self, key: impl Into<PicoStr>) -> bool { + self.map.contains_key(&key.into()) + } + + fn entries(&self) -> impl Iterator<Item = &hayagriva::Entry> { + self.map.values() + } +} + +impl Debug for Bibliography { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.debug_set().entries(self.map.keys()).finish() + } +} + +impl Hash for Bibliography { + fn hash<H: Hasher>(&self, state: &mut H) { + self.hash.hash(state); + } +} + +/// Format a BibLaTeX loading error. +fn format_biblatex_error(path: &str, src: &str, errors: Vec<BibLaTeXError>) -> EcoString { + let Some(error) = errors.first() else { + return eco_format!("failed to parse BibLaTeX file ({path})"); + }; + + let (span, msg) = match error { + BibLaTeXError::Parse(error) => (&error.span, error.kind.to_string()), + BibLaTeXError::Type(error) => (&error.span, error.kind.to_string()), + }; + let line = src.get(..span.start).unwrap_or_default().lines().count(); + eco_format!("failed to parse BibLaTeX file ({path}:{line}: {msg})") +} + +/// A loaded CSL style. +#[ty(cast)] +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct CslStyle { + name: Option<EcoString>, + style: Arc<LazyHash<citationberg::IndependentStyle>>, +} + +impl CslStyle { + /// Parse the style argument. + pub fn parse(engine: &mut Engine, args: &mut Args) -> SourceResult<Option<CslStyle>> { + let Some(Spanned { v: string, span }) = + args.named::<Spanned<EcoString>>("style")? + else { + return Ok(None); + }; + + Ok(Some(Self::parse_impl(engine, &string, span).at(span)?)) + } + + /// Parse the style argument with `Smart`. + pub fn parse_smart( + engine: &mut Engine, + args: &mut Args, + ) -> SourceResult<Option<Smart<CslStyle>>> { + let Some(Spanned { v: smart, span }) = + args.named::<Spanned<Smart<EcoString>>>("style")? + else { + return Ok(None); + }; + + Ok(Some(match smart { + Smart::Auto => Smart::Auto, + Smart::Custom(string) => { + Smart::Custom(Self::parse_impl(engine, &string, span).at(span)?) + } + })) + } + + /// Parse internally. + fn parse_impl(engine: &mut Engine, string: &str, span: Span) -> StrResult<CslStyle> { + let ext = Path::new(string) + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default() + .to_lowercase(); + + if ext == "csl" { + let id = span.resolve_path(string)?; + let data = engine.world.file(id)?; + CslStyle::from_data(&data) + } else { + CslStyle::from_name(string) + } + } + + /// Load a built-in CSL style. + #[comemo::memoize] + pub fn from_name(name: &str) -> StrResult<CslStyle> { + match hayagriva::archive::ArchivedStyle::by_name(name).map(ArchivedStyle::get) { + Some(citationberg::Style::Independent(style)) => Ok(Self { + name: Some(name.into()), + style: Arc::new(LazyHash::new(style)), + }), + _ => bail!("unknown style: `{name}`"), + } + } + + /// Load a CSL style from file contents. + #[comemo::memoize] + pub fn from_data(data: &Bytes) -> StrResult<CslStyle> { + let text = std::str::from_utf8(data.as_slice()).map_err(FileError::from)?; + citationberg::IndependentStyle::from_xml(text) + .map(|style| Self { name: None, style: Arc::new(LazyHash::new(style)) }) + .map_err(|err| eco_format!("failed to load CSL style ({err})")) + } + + /// Get the underlying independent style. + pub fn get(&self) -> &citationberg::IndependentStyle { + self.style.as_ref() + } +} + +// This Reflect impl is technically a bit wrong because it doesn't say what +// FromValue and IntoValue really do. Instead, it says what the `style` argument +// on `bibliography` and `cite` expect (through manual parsing). +impl Reflect for CslStyle { + #[comemo::memoize] + fn input() -> CastInfo { + let ty = std::iter::once(CastInfo::Type(Type::of::<Str>())); + let options = hayagriva::archive::ArchivedStyle::all().iter().map(|name| { + CastInfo::Value(name.names()[0].into_value(), name.display_name()) + }); + CastInfo::Union(ty.chain(options).collect()) + } + + fn output() -> CastInfo { + EcoString::output() + } + + fn castable(value: &Value) -> bool { + if let Value::Dyn(dynamic) = &value { + if dynamic.is::<Self>() { + return true; + } + } + + false + } +} + +impl FromValue for CslStyle { + fn from_value(value: Value) -> HintedStrResult<Self> { + if let Value::Dyn(dynamic) = &value { + if let Some(concrete) = dynamic.downcast::<Self>() { + return Ok(concrete.clone()); + } + } + + Err(<Self as Reflect>::error(&value)) + } +} + +impl IntoValue for CslStyle { + fn into_value(self) -> Value { + Value::dynamic(self) + } +} + +impl Repr for CslStyle { + fn repr(&self) -> EcoString { + self.name + .as_ref() + .map(|name| name.repr()) + .unwrap_or_else(|| "..".into()) + } +} + +/// Fully formatted citations and references, generated once (through +/// memoization) for the whole document. This setup is necessary because +/// citation formatting is inherently stateful and we need access to all +/// citations to do it. +pub(super) struct Works { + /// Maps from the location of a citation group to its rendered content. + pub citations: HashMap<Location, SourceResult<Content>>, + /// Lists all references in the bibliography, with optional prefix, or + /// `None` if the citation style can't be used for bibliographies. + pub references: Option<Vec<(Option<Content>, Content)>>, + /// Whether the bibliography should have hanging indent. + pub hanging_indent: bool, +} + +impl Works { + /// Generate all citations and the whole bibliography. + pub fn generate(engine: &Engine) -> StrResult<Arc<Works>> { + Self::generate_impl(engine.routines, engine.world, engine.introspector) + } + + /// The internal implementation of [`Works::generate`]. + #[comemo::memoize] + fn generate_impl( + routines: &Routines, + world: Tracked<dyn World + '_>, + introspector: Tracked<Introspector>, + ) -> StrResult<Arc<Works>> { + let mut generator = Generator::new(routines, world, introspector)?; + let rendered = generator.drive(); + let works = generator.display(&rendered)?; + Ok(Arc::new(works)) + } +} + +/// Context for generating the bibliography. +struct Generator<'a> { + /// The routines that is used to evaluate mathematical material in citations. + routines: &'a Routines, + /// The world that is used to evaluate mathematical material in citations. + world: Tracked<'a, dyn World + 'a>, + /// The document's bibliography. + bibliography: Packed<BibliographyElem>, + /// The document's citation groups. + groups: EcoVec<Content>, + /// Details about each group that are accumulated while driving hayagriva's + /// bibliography driver and needed when processing hayagriva's output. + infos: Vec<GroupInfo>, + /// Citations with unresolved keys. + failures: HashMap<Location, SourceResult<Content>>, +} + +/// Details about a group of merged citations. All citations are put into groups +/// of adjacent ones (e.g., `@foo @bar` will merge into a group of length two). +/// Even single citations will be put into groups of length ones. +struct GroupInfo { + /// The group's location. + location: Location, + /// The group's span. + span: Span, + /// Whether the group should be displayed in a footnote. + footnote: bool, + /// Details about the groups citations. + subinfos: SmallVec<[CiteInfo; 1]>, +} + +/// Details about a citation item in a request. +struct CiteInfo { + /// The citation's key. + key: Label, + /// The citation's supplement. + supplement: Option<Content>, + /// Whether this citation was hidden. + hidden: bool, +} + +impl<'a> Generator<'a> { + /// Create a new generator. + fn new( + routines: &'a Routines, + world: Tracked<'a, dyn World + 'a>, + introspector: Tracked<Introspector>, + ) -> StrResult<Self> { + let bibliography = BibliographyElem::find(introspector)?; + let groups = introspector.query(&CiteGroup::elem().select()); + let infos = Vec::with_capacity(groups.len()); + Ok(Self { + routines, + world, + bibliography, + groups, + infos, + failures: HashMap::new(), + }) + } + + /// Drives hayagriva's citation driver. + fn drive(&mut self) -> hayagriva::Rendered { + static LOCALES: Lazy<Vec<citationberg::Locale>> = + Lazy::new(hayagriva::archive::locales); + + let database = self.bibliography.bibliography(); + let bibliography_style = self.bibliography.style(StyleChain::default()); + let styles = Arena::new(); + + // Process all citation groups. + let mut driver = BibliographyDriver::new(); + for elem in &self.groups { + let group = elem.to_packed::<CiteGroup>().unwrap(); + let location = elem.location().unwrap(); + let children = group.children(); + + // Groups should never be empty. + let Some(first) = children.first() else { continue }; + + let mut subinfos = SmallVec::with_capacity(children.len()); + let mut items = Vec::with_capacity(children.len()); + let mut errors = EcoVec::new(); + let mut normal = true; + + // Create infos and items for each child in the group. + for child in children { + let key = *child.key(); + let Some(entry) = database.map.get(&key.into_inner()) else { + errors.push(error!( + child.span(), + "key `{}` does not exist in the bibliography", + key.as_str() + )); + continue; + }; + + let supplement = child.supplement(StyleChain::default()); + let locator = supplement.as_ref().map(|_| { + SpecificLocator( + citationberg::taxonomy::Locator::Custom, + hayagriva::LocatorPayload::Transparent, + ) + }); + + let mut hidden = false; + let special_form = match child.form(StyleChain::default()) { + None => { + hidden = true; + None + } + Some(CitationForm::Normal) => None, + Some(CitationForm::Prose) => Some(hayagriva::CitePurpose::Prose), + Some(CitationForm::Full) => Some(hayagriva::CitePurpose::Full), + Some(CitationForm::Author) => Some(hayagriva::CitePurpose::Author), + Some(CitationForm::Year) => Some(hayagriva::CitePurpose::Year), + }; + + normal &= special_form.is_none(); + subinfos.push(CiteInfo { key, supplement, hidden }); + items.push(CitationItem::new(entry, locator, None, hidden, special_form)); + } + + if !errors.is_empty() { + self.failures.insert(location, Err(errors)); + continue; + } + + let style = match first.style(StyleChain::default()) { + Smart::Auto => &bibliography_style.style, + Smart::Custom(style) => styles.alloc(style.style), + }; + + self.infos.push(GroupInfo { + location, + subinfos, + span: first.span(), + footnote: normal + && style.settings.class == citationberg::StyleClass::Note, + }); + + driver.citation(CitationRequest::new( + items, + style, + Some(locale( + first.lang().copied().unwrap_or(Lang::ENGLISH), + first.region().copied().flatten(), + )), + &LOCALES, + None, + )); + } + + let locale = locale( + self.bibliography.lang().copied().unwrap_or(Lang::ENGLISH), + self.bibliography.region().copied().flatten(), + ); + + // Add hidden items for everything if we should print the whole + // bibliography. + if self.bibliography.full(StyleChain::default()) { + for entry in database.map.values() { + driver.citation(CitationRequest::new( + vec![CitationItem::new(entry, None, None, true, None)], + bibliography_style.get(), + Some(locale.clone()), + &LOCALES, + None, + )); + } + } + + driver.finish(BibliographyRequest { + style: bibliography_style.get(), + locale: Some(locale), + locale_files: &LOCALES, + }) + } + + /// Displays hayagriva's output as content for the citations and references. + fn display(&mut self, rendered: &hayagriva::Rendered) -> StrResult<Works> { + let citations = self.display_citations(rendered)?; + let references = self.display_references(rendered)?; + let hanging_indent = + rendered.bibliography.as_ref().is_some_and(|b| b.hanging_indent); + Ok(Works { citations, references, hanging_indent }) + } + + /// Display the citation groups. + fn display_citations( + &mut self, + rendered: &hayagriva::Rendered, + ) -> StrResult<HashMap<Location, SourceResult<Content>>> { + // Determine for each citation key where in the bibliography it is, + // so that we can link there. + let mut links = HashMap::new(); + if let Some(bibliography) = &rendered.bibliography { + let location = self.bibliography.location().unwrap(); + for (k, item) in bibliography.items.iter().enumerate() { + links.insert(item.key.as_str(), location.variant(k + 1)); + } + } + + let mut output = std::mem::take(&mut self.failures); + for (info, citation) in self.infos.iter().zip(&rendered.citations) { + let supplement = |i: usize| info.subinfos.get(i)?.supplement.clone(); + let link = |i: usize| links.get(info.subinfos.get(i)?.key.as_str()).copied(); + + let renderer = ElemRenderer { + routines: self.routines, + world: self.world, + span: info.span, + supplement: &supplement, + link: &link, + }; + + let content = if info.subinfos.iter().all(|sub| sub.hidden) { + Content::empty() + } else { + let mut content = renderer.display_elem_children( + &citation.citation, + &mut None, + true, + )?; + + if info.footnote { + content = FootnoteElem::with_content(content).pack(); + } + + content + }; + + output.insert(info.location, Ok(content)); + } + + Ok(output) + } + + /// Display the bibliography references. + #[allow(clippy::type_complexity)] + fn display_references( + &self, + rendered: &hayagriva::Rendered, + ) -> StrResult<Option<Vec<(Option<Content>, Content)>>> { + let Some(rendered) = &rendered.bibliography else { return Ok(None) }; + + // Determine for each citation key where it first occurred, so that we + // can link there. + let mut first_occurrences = HashMap::new(); + for info in &self.infos { + for subinfo in &info.subinfos { + let key = subinfo.key.as_str(); + first_occurrences.entry(key).or_insert(info.location); + } + } + + // The location of the bibliography. + let location = self.bibliography.location().unwrap(); + + let mut output = vec![]; + for (k, item) in rendered.items.iter().enumerate() { + let renderer = ElemRenderer { + routines: self.routines, + world: self.world, + span: self.bibliography.span(), + supplement: &|_| None, + link: &|_| None, + }; + + // Each reference is assigned a manually created well-known location + // that is derived from the bibliography's location. This way, + // citations can link to them. + let backlink = location.variant(k + 1); + + // Render the first field. + let mut prefix = item + .first_field + .as_ref() + .map(|elem| { + let mut content = + renderer.display_elem_child(elem, &mut None, false)?; + if let Some(location) = first_occurrences.get(item.key.as_str()) { + let dest = Destination::Location(*location); + content = content.linked(dest); + } + StrResult::Ok(content) + }) + .transpose()?; + + // Render the main reference content. + let mut reference = + renderer.display_elem_children(&item.content, &mut prefix, false)?; + + // Attach a backlink to either the prefix or the reference so that + // we can link to the bibliography entry. + prefix.as_mut().unwrap_or(&mut reference).set_location(backlink); + + output.push((prefix, reference)); + } + + Ok(Some(output)) + } +} + +/// Renders hayagriva elements into content. +struct ElemRenderer<'a> { + /// The routines that is used to evaluate mathematical material in citations. + routines: &'a Routines, + /// The world that is used to evaluate mathematical material. + world: Tracked<'a, dyn World + 'a>, + /// The span that is attached to all of the resulting content. + span: Span, + /// Resolves the supplement of i-th citation in the request. + supplement: &'a dyn Fn(usize) -> Option<Content>, + /// Resolves where the i-th citation in the request should link to. + link: &'a dyn Fn(usize) -> Option<Location>, +} + +impl ElemRenderer<'_> { + /// Display rendered hayagriva elements. + /// + /// The `prefix` can be a separate content storage where `left-margin` + /// elements will be accumulated into. + /// + /// `is_citation` dictates whether whitespace at the start of the citation + /// will be eliminated. Some CSL styles yield whitespace at the start of + /// their citations, which should instead be handled by Typst. + fn display_elem_children( + &self, + elems: &hayagriva::ElemChildren, + prefix: &mut Option<Content>, + is_citation: bool, + ) -> StrResult<Content> { + Ok(Content::sequence( + elems + .0 + .iter() + .enumerate() + .map(|(i, elem)| { + self.display_elem_child(elem, prefix, is_citation && i == 0) + }) + .collect::<StrResult<Vec<_>>>()?, + )) + } + + /// Display a rendered hayagriva element. + fn display_elem_child( + &self, + elem: &hayagriva::ElemChild, + prefix: &mut Option<Content>, + trim_start: bool, + ) -> StrResult<Content> { + Ok(match elem { + hayagriva::ElemChild::Text(formatted) => { + self.display_formatted(formatted, trim_start) + } + hayagriva::ElemChild::Elem(elem) => self.display_elem(elem, prefix)?, + hayagriva::ElemChild::Markup(markup) => self.display_math(markup), + hayagriva::ElemChild::Link { text, url } => self.display_link(text, url)?, + hayagriva::ElemChild::Transparent { cite_idx, format } => { + self.display_transparent(*cite_idx, format) + } + }) + } + + /// Display a block-level element. + fn display_elem( + &self, + elem: &hayagriva::Elem, + prefix: &mut Option<Content>, + ) -> StrResult<Content> { + use citationberg::Display; + + let block_level = matches!(elem.display, Some(Display::Block | Display::Indent)); + + let mut suf_prefix = None; + let mut content = self.display_elem_children( + &elem.children, + if block_level { &mut suf_prefix } else { prefix }, + false, + )?; + + if let Some(prefix) = suf_prefix { + const COLUMN_GUTTER: Em = Em::new(0.65); + content = GridElem::new(vec![ + GridChild::Item(GridItem::Cell( + Packed::new(GridCell::new(prefix)).spanned(self.span), + )), + GridChild::Item(GridItem::Cell( + Packed::new(GridCell::new(content)).spanned(self.span), + )), + ]) + .with_columns(TrackSizings(smallvec![Sizing::Auto; 2])) + .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()])) + .pack() + .spanned(self.span); + } + + match elem.display { + Some(Display::Block) => { + content = BlockElem::new() + .with_body(Some(BlockBody::Content(content))) + .pack() + .spanned(self.span); + } + Some(Display::Indent) => { + content = PadElem::new(content).pack().spanned(self.span); + } + Some(Display::LeftMargin) => { + *prefix.get_or_insert_with(Default::default) += content; + return Ok(Content::empty()); + } + _ => {} + } + + if let Some(hayagriva::ElemMeta::Entry(i)) = elem.meta { + if let Some(location) = (self.link)(i) { + let dest = Destination::Location(location); + content = content.linked(dest); + } + } + + Ok(content) + } + + /// Display math. + fn display_math(&self, math: &str) -> Content { + (self.routines.eval_string)( + self.routines, + self.world, + math, + self.span, + EvalMode::Math, + Scope::new(), + ) + .map(Value::display) + .unwrap_or_else(|_| TextElem::packed(math).spanned(self.span)) + } + + /// Display a link. + fn display_link(&self, text: &hayagriva::Formatted, url: &str) -> StrResult<Content> { + let dest = Destination::Url(Url::new(url)?); + Ok(LinkElem::new(dest.into(), self.display_formatted(text, false)) + .pack() + .spanned(self.span)) + } + + /// Display transparent pass-through content. + fn display_transparent(&self, i: usize, format: &hayagriva::Formatting) -> Content { + let content = (self.supplement)(i).unwrap_or_default(); + apply_formatting(content, format) + } + + /// Display formatted hayagriva text as content. + fn display_formatted( + &self, + formatted: &hayagriva::Formatted, + trim_start: bool, + ) -> Content { + let formatted_text = if trim_start { + formatted.text.trim_start() + } else { + formatted.text.as_str() + }; + + let content = TextElem::packed(formatted_text).spanned(self.span); + apply_formatting(content, &formatted.formatting) + } +} + +/// Applies formatting to content. +fn apply_formatting(mut content: Content, format: &hayagriva::Formatting) -> Content { + match format.font_style { + citationberg::FontStyle::Normal => {} + citationberg::FontStyle::Italic => { + content = content.styled(TextElem::set_style(FontStyle::Italic)); + } + } + + match format.font_variant { + citationberg::FontVariant::Normal => {} + citationberg::FontVariant::SmallCaps => { + content = content.styled(TextElem::set_smallcaps(true)); + } + } + + match format.font_weight { + citationberg::FontWeight::Normal => {} + citationberg::FontWeight::Bold => { + content = content.styled(TextElem::set_delta(WeightDelta(300))); + } + citationberg::FontWeight::Light => { + content = content.styled(TextElem::set_delta(WeightDelta(-100))); + } + } + + match format.text_decoration { + citationberg::TextDecoration::None => {} + citationberg::TextDecoration::Underline => { + content = content.underlined(); + } + } + + let span = content.span(); + match format.vertical_align { + citationberg::VerticalAlign::None => {} + citationberg::VerticalAlign::Baseline => {} + citationberg::VerticalAlign::Sup => { + // Add zero-width weak spacing to make the superscript "sticky". + content = HElem::hole().pack() + SuperElem::new(content).pack().spanned(span); + } + citationberg::VerticalAlign::Sub => { + content = HElem::hole().pack() + SubElem::new(content).pack().spanned(span); + } + } + + content +} + +/// Create a locale code from language and optionally region. +fn locale(lang: Lang, region: Option<Region>) -> citationberg::LocaleCode { + let mut value = String::with_capacity(5); + value.push_str(lang.as_str()); + if let Some(region) = region { + value.push('-'); + value.push_str(region.as_str()) + } + citationberg::LocaleCode(value) +} diff --git a/crates/typst-library/src/model/cite.rs b/crates/typst-library/src/model/cite.rs new file mode 100644 index 00000000..ac0cfa79 --- /dev/null +++ b/crates/typst-library/src/model/cite.rs @@ -0,0 +1,175 @@ +use crate::diag::{error, At, HintedString, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, Cast, Content, Label, Packed, Show, Smart, StyleChain, Synthesize, +}; +use crate::introspection::Locatable; +use crate::model::bibliography::Works; +use crate::model::CslStyle; +use crate::text::{Lang, Region, TextElem}; + +/// Cite a work from the bibliography. +/// +/// Before you starting citing, you need to add a [bibliography] somewhere in +/// your document. +/// +/// # Example +/// ```example +/// This was already noted by +/// pirates long ago. @arrgh +/// +/// Multiple sources say ... +/// @arrgh @netwok. +/// +/// You can also call `cite` +/// explicitly. #cite(<arrgh>) +/// +/// #bibliography("works.bib") +/// ``` +/// +/// If your source name contains certain characters such as slashes, which are +/// not recognized by the `<>` syntax, you can explicitly call `label` instead. +/// +/// ```typ +/// Computer Modern is an example of a modernist serif typeface. +/// #cite(label("DBLP:books/lib/Knuth86a")). +/// >>> #bibliography("works.bib") +/// ``` +/// +/// # Syntax +/// This function indirectly has dedicated syntax. [References]($ref) can be +/// used to cite works from the bibliography. The label then corresponds to the +/// citation key. +#[elem(Synthesize)] +pub struct CiteElem { + /// The citation key that identifies the entry in the bibliography that + /// shall be cited, as a label. + /// + /// ```example + /// // All the same + /// @netwok \ + /// #cite(<netwok>) \ + /// #cite(label("netwok")) + /// >>> #set text(0pt) + /// >>> #bibliography("works.bib", style: "apa") + /// ``` + #[required] + pub key: Label, + + /// A supplement for the citation such as page or chapter number. + /// + /// In reference syntax, the supplement can be added in square brackets: + /// + /// ```example + /// This has been proven. @distress[p.~7] + /// + /// #bibliography("works.bib") + /// ``` + pub supplement: Option<Content>, + + /// The kind of citation to produce. Different forms are useful in different + /// scenarios: A normal citation is useful as a source at the end of a + /// sentence, while a "prose" citation is more suitable for inclusion in the + /// flow of text. + /// + /// If set to `{none}`, the cited work is included in the bibliography, but + /// nothing will be displayed. + /// + /// ```example + /// #cite(<netwok>, form: "prose") + /// show the outsized effects of + /// pirate life on the human psyche. + /// >>> #set text(0pt) + /// >>> #bibliography("works.bib", style: "apa") + /// ``` + #[default(Some(CitationForm::Normal))] + pub form: Option<CitationForm>, + + /// The citation style. + /// + /// Should be either `{auto}`, one of the built-in styles (see below) or a + /// path to a [CSL file](https://citationstyles.org/). Some of the styles + /// listed below appear twice, once with their full name and once with a + /// short alias. + /// + /// When set to `{auto}`, automatically use the + /// [bibliography's style]($bibliography.style) for the citations. + #[parse(CslStyle::parse_smart(engine, args)?)] + pub style: Smart<CslStyle>, + + /// The text language setting where the citation is. + #[internal] + #[synthesized] + pub lang: Lang, + + /// The text region setting where the citation is. + #[internal] + #[synthesized] + pub region: Option<Region>, +} + +impl Synthesize for Packed<CiteElem> { + fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> { + let elem = self.as_mut(); + elem.push_lang(TextElem::lang_in(styles)); + elem.push_region(TextElem::region_in(styles)); + Ok(()) + } +} + +cast! { + CiteElem, + v: Content => v.unpack::<Self>().map_err(|_| "expected citation")?, +} + +/// The form of the citation. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum CitationForm { + /// Display in the standard way for the active style. + #[default] + Normal, + /// Produces a citation that is suitable for inclusion in a sentence. + Prose, + /// Mimics a bibliography entry, with full information about the cited work. + Full, + /// Shows only the cited work's author(s). + Author, + /// Shows only the cited work's year. + Year, +} + +/// A group of citations. +/// +/// This is automatically created from adjacent citations during show rule +/// application. +#[elem(Locatable, Show)] +pub struct CiteGroup { + /// The citations. + #[required] + pub children: Vec<Packed<CiteElem>>, +} + +impl Show for Packed<CiteGroup> { + #[typst_macros::time(name = "cite", span = self.span())] + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + let location = self.location().unwrap(); + let span = self.span(); + Works::generate(engine) + .at(span)? + .citations + .get(&location) + .cloned() + .ok_or_else(failed_to_format_citation) + .at(span)? + } +} + +/// The error message when a citation wasn't found in the pre-formatted list. +#[cold] +fn failed_to_format_citation() -> HintedString { + error!( + "cannot format citation in isolation"; + hint: "check whether this citation is measured \ + without being inserted into the document" + ) +} diff --git a/crates/typst-library/src/model/document.rs b/crates/typst-library/src/model/document.rs new file mode 100644 index 00000000..b693d785 --- /dev/null +++ b/crates/typst-library/src/model/document.rs @@ -0,0 +1,145 @@ +use ecow::EcoString; + +use crate::diag::{bail, HintedStrResult, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, Args, Array, Construct, Content, Datetime, Fields, Smart, StyleChain, + Styles, Value, +}; +use crate::introspection::Introspector; +use crate::layout::Page; + +/// The root element of a document and its metadata. +/// +/// All documents are automatically wrapped in a `document` element. You cannot +/// create a document element yourself. This function is only used with +/// [set rules]($styling/#set-rules) to specify document metadata. Such a set +/// rule must not occur inside of any layout container. +/// +/// ```example +/// #set document(title: [Hello]) +/// +/// This has no visible output, but +/// embeds metadata into the PDF! +/// ``` +/// +/// Note that metadata set with this function is not rendered within the +/// document. Instead, it is embedded in the compiled PDF file. +#[elem(Construct)] +pub struct DocumentElem { + /// The document's title. This is often rendered as the title of the + /// PDF viewer window. + /// + /// While this can be arbitrary content, PDF viewers only support plain text + /// titles, so the conversion might be lossy. + #[ghost] + pub title: Option<Content>, + + /// The document's authors. + #[ghost] + pub author: Author, + + /// The document's keywords. + #[ghost] + pub keywords: Keywords, + + /// The document's creation date. + /// + /// If this is `{auto}` (default), Typst uses the current date and time. + /// Setting it to `{none}` prevents Typst from embedding any creation date + /// into the PDF metadata. + /// + /// The year component must be at least zero in order to be embedded into a + /// PDF. + /// + /// If you want to create byte-by-byte reproducible PDFs, set this to + /// something other than `{auto}`. + #[ghost] + pub date: Smart<Option<Datetime>>, +} + +impl Construct for DocumentElem { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> { + bail!(args.span, "can only be used in set rules") + } +} + +/// A list of authors. +#[derive(Debug, Default, Clone, PartialEq, Hash)] +pub struct Author(Vec<EcoString>); + +cast! { + Author, + self => self.0.into_value(), + v: EcoString => Self(vec![v]), + v: Array => Self(v.into_iter().map(Value::cast).collect::<HintedStrResult<_>>()?), +} + +/// A list of keywords. +#[derive(Debug, Default, Clone, PartialEq, Hash)] +pub struct Keywords(Vec<EcoString>); + +cast! { + Keywords, + self => self.0.into_value(), + v: EcoString => Self(vec![v]), + v: Array => Self(v.into_iter().map(Value::cast).collect::<HintedStrResult<_>>()?), +} + +/// A finished document with metadata and page frames. +#[derive(Debug, Default, Clone)] +pub struct Document { + /// The document's finished pages. + pub pages: Vec<Page>, + /// Details about the document. + pub info: DocumentInfo, + /// Provides the ability to execute queries on the document. + pub introspector: Introspector, +} + +/// Details about the document. +#[derive(Debug, Default, Clone, PartialEq, Hash)] +pub struct DocumentInfo { + /// The document's title. + pub title: Option<EcoString>, + /// The document's author. + pub author: Vec<EcoString>, + /// The document's keywords. + pub keywords: Vec<EcoString>, + /// The document's creation date. + pub date: Smart<Option<Datetime>>, +} + +impl DocumentInfo { + /// Populate this document info with details from the given styles. + /// + /// Document set rules are a bit special, so we need to do this manually. + pub fn populate(&mut self, styles: &Styles) { + let chain = StyleChain::new(styles); + let has = |field| styles.has::<DocumentElem>(field as _); + if has(<DocumentElem as Fields>::Enum::Title) { + self.title = + DocumentElem::title_in(chain).map(|content| content.plain_text()); + } + if has(<DocumentElem as Fields>::Enum::Author) { + self.author = DocumentElem::author_in(chain).0; + } + if has(<DocumentElem as Fields>::Enum::Keywords) { + self.keywords = DocumentElem::keywords_in(chain).0; + } + if has(<DocumentElem as Fields>::Enum::Date) { + self.date = DocumentElem::date_in(chain); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_document_is_send_and_sync() { + fn ensure_send_and_sync<T: Send + Sync>() {} + ensure_send_and_sync::<Document>(); + } +} diff --git a/crates/typst-library/src/model/emph.rs b/crates/typst-library/src/model/emph.rs new file mode 100644 index 00000000..e36e5ef7 --- /dev/null +++ b/crates/typst-library/src/model/emph.rs @@ -0,0 +1,41 @@ +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{elem, Content, Packed, Show, StyleChain}; +use crate::text::{ItalicToggle, TextElem}; + +/// Emphasizes content by toggling italics. +/// +/// - If the current [text style]($text.style) is `{"normal"}`, this turns it +/// into `{"italic"}`. +/// - If it is already `{"italic"}` or `{"oblique"}`, it turns it back to +/// `{"normal"}`. +/// +/// # Example +/// ```example +/// This is _emphasized._ \ +/// This is #emph[too.] +/// +/// #show emph: it => { +/// text(blue, it.body) +/// } +/// +/// This is _emphasized_ differently. +/// ``` +/// +/// # Syntax +/// This function also has dedicated syntax: To emphasize content, simply +/// enclose it in underscores (`_`). Note that this only works at word +/// boundaries. To emphasize part of a word, you have to use the function. +#[elem(title = "Emphasis", keywords = ["italic"], Show)] +pub struct EmphElem { + /// The content to emphasize. + #[required] + pub body: Content, +} + +impl Show for Packed<EmphElem> { + #[typst_macros::time(name = "emph", span = self.span())] + fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(self.body().clone().styled(TextElem::set_emph(ItalicToggle(true)))) + } +} diff --git a/crates/typst-library/src/model/enum.rs b/crates/typst-library/src/model/enum.rs new file mode 100644 index 00000000..bac792d3 --- /dev/null +++ b/crates/typst-library/src/model/enum.rs @@ -0,0 +1,271 @@ +use std::str::FromStr; + +use smallvec::SmallVec; + +use crate::diag::{bail, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain, + Styles, +}; +use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem}; +use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern, ParElem}; + +/// A numbered list. +/// +/// Displays a sequence of items vertically and numbers them consecutively. +/// +/// # Example +/// ```example +/// Automatically numbered: +/// + Preparations +/// + Analysis +/// + Conclusions +/// +/// Manually numbered: +/// 2. What is the first step? +/// 5. I am confused. +/// + Moving on ... +/// +/// Multiple lines: +/// + This enum item has multiple +/// lines because the next line +/// is indented. +/// +/// Function call. +/// #enum[First][Second] +/// ``` +/// +/// You can easily switch all your enumerations to a different numbering style +/// with a set rule. +/// ```example +/// #set enum(numbering: "a)") +/// +/// + Starting off ... +/// + Don't forget step two +/// ``` +/// +/// You can also use [`enum.item`]($enum.item) to programmatically customize the +/// number of each item in the enumeration: +/// +/// ```example +/// #enum( +/// enum.item(1)[First step], +/// enum.item(5)[Fifth step], +/// enum.item(10)[Tenth step] +/// ) +/// ``` +/// +/// # Syntax +/// This functions also has dedicated syntax: +/// +/// - Starting a line with a plus sign creates an automatically numbered +/// enumeration item. +/// - Starting a line with a number followed by a dot creates an explicitly +/// numbered enumeration item. +/// +/// Enumeration items can contain multiple paragraphs and other block-level +/// content. All content that is indented more than an item's marker becomes +/// part of that item. +#[elem(scope, title = "Numbered List", Show)] +pub struct EnumElem { + /// Defines the default [spacing]($enum.spacing) of the enumeration. If it + /// is `{false}`, the items are spaced apart with + /// [paragraph spacing]($par.spacing). If it is `{true}`, they use + /// [paragraph leading]($par.leading) instead. This makes the list more + /// compact, which can look better if the items are short. + /// + /// In markup mode, the value of this parameter is determined based on + /// whether items are separated with a blank line. If items directly follow + /// each other, this is set to `{true}`; if items are separated by a blank + /// line, this is set to `{false}`. The markup-defined tightness cannot be + /// overridden with set rules. + /// + /// ```example + /// + If an enum has a lot of text, and + /// maybe other inline content, it + /// should not be tight anymore. + /// + /// + To make an enum wide, simply + /// insert a blank line between the + /// items. + /// ``` + #[default(true)] + pub tight: bool, + + /// How to number the enumeration. Accepts a + /// [numbering pattern or function]($numbering). + /// + /// If the numbering pattern contains multiple counting symbols, they apply + /// to nested enums. If given a function, the function receives one argument + /// if `full` is `{false}` and multiple arguments if `full` is `{true}`. + /// + /// ```example + /// #set enum(numbering: "1.a)") + /// + Different + /// + Numbering + /// + Nested + /// + Items + /// + Style + /// + /// #set enum(numbering: n => super[#n]) + /// + Superscript + /// + Numbering! + /// ``` + #[default(Numbering::Pattern(NumberingPattern::from_str("1.").unwrap()))] + #[borrowed] + pub numbering: Numbering, + + /// Which number to start the enumeration with. + /// + /// ```example + /// #enum( + /// start: 3, + /// [Skipping], + /// [Ahead], + /// ) + /// ``` + #[default(1)] + pub start: usize, + + /// Whether to display the full numbering, including the numbers of + /// all parent enumerations. + /// + /// + /// ```example + /// #set enum(numbering: "1.a)", full: true) + /// + Cook + /// + Heat water + /// + Add ingredients + /// + Eat + /// ``` + #[default(false)] + pub full: bool, + + /// The indentation of each item. + #[resolve] + pub indent: Length, + + /// The space between the numbering and the body of each item. + #[resolve] + #[default(Em::new(0.5).into())] + pub body_indent: Length, + + /// The spacing between the items of the enumeration. + /// + /// If set to `{auto}`, uses paragraph [`leading`]($par.leading) for tight + /// enumerations and paragraph [`spacing`]($par.spacing) for wide + /// (non-tight) enumerations. + pub spacing: Smart<Length>, + + /// The alignment that enum numbers should have. + /// + /// By default, this is set to `{end + top}`, which aligns enum numbers + /// towards end of the current text direction (in left-to-right script, + /// for example, this is the same as `{right}`) and at the top of the line. + /// The choice of `{end}` for horizontal alignment of enum numbers is + /// usually preferred over `{start}`, as numbers then grow away from the + /// text instead of towards it, avoiding certain visual issues. This option + /// lets you override this behaviour, however. (Also to note is that the + /// [unordered list]($list) uses a different method for this, by giving the + /// `marker` content an alignment directly.). + /// + /// ````example + /// #set enum(number-align: start + bottom) + /// + /// Here are some powers of two: + /// 1. One + /// 2. Two + /// 4. Four + /// 8. Eight + /// 16. Sixteen + /// 32. Thirty two + /// ```` + #[default(HAlignment::End + VAlignment::Top)] + pub number_align: Alignment, + + /// The numbered list's items. + /// + /// When using the enum syntax, adjacent items are automatically collected + /// into enumerations, even through constructs like for loops. + /// + /// ```example + /// #for phase in ( + /// "Launch", + /// "Orbit", + /// "Descent", + /// ) [+ #phase] + /// ``` + #[variadic] + pub children: Vec<Packed<EnumItem>>, + + /// The numbers of parent items. + #[internal] + #[fold] + #[ghost] + pub parents: SmallVec<[usize; 4]>, +} + +#[scope] +impl EnumElem { + #[elem] + type EnumItem; +} + +impl Show for Packed<EnumElem> { + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let mut realized = + BlockElem::multi_layouter(self.clone(), engine.routines.layout_enum) + .pack() + .spanned(self.span()); + + if self.tight(styles) { + let leading = ParElem::leading_in(styles); + let spacing = + VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); + realized = spacing + realized; + } + + Ok(realized) + } +} + +/// An enumeration item. +#[elem(name = "item", title = "Numbered List Item")] +pub struct EnumItem { + /// The item's number. + #[positional] + pub number: Option<usize>, + + /// The item's body. + #[required] + pub body: Content, +} + +cast! { + EnumItem, + array: Array => { + let mut iter = array.into_iter(); + let (number, body) = match (iter.next(), iter.next(), iter.next()) { + (Some(a), Some(b), None) => (a.cast()?, b.cast()?), + _ => bail!("array must contain exactly two entries"), + }; + Self::new(body).with_number(number) + }, + v: Content => v.unpack::<Self>().unwrap_or_else(Self::new), +} + +impl ListLike for EnumElem { + type Item = EnumItem; + + fn create(children: Vec<Packed<Self::Item>>, tight: bool) -> Self { + Self::new(children).with_tight(tight) + } +} + +impl ListItemLike for EnumItem { + fn styled(mut item: Packed<Self>, styles: Styles) -> Packed<Self> { + item.body.style_in_place(styles); + item + } +} diff --git a/crates/typst-library/src/model/figure.rs b/crates/typst-library/src/model/figure.rs new file mode 100644 index 00000000..abdf2a4e --- /dev/null +++ b/crates/typst-library/src/model/figure.rs @@ -0,0 +1,641 @@ +use std::borrow::Cow; +use std::num::NonZeroUsize; +use std::str::FromStr; + +use ecow::EcoString; +use typst_utils::NonZeroExt; + +use crate::diag::{bail, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, scope, select_where, Content, Element, NativeElement, Packed, Selector, + Show, ShowSet, Smart, StyleChain, Styles, Synthesize, +}; +use crate::introspection::{ + Count, Counter, CounterKey, CounterUpdate, Locatable, Location, +}; +use crate::layout::{ + AlignElem, Alignment, BlockBody, BlockElem, Em, HAlignment, Length, OuterVAlignment, + PlaceElem, PlacementScope, VAlignment, VElem, +}; +use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement}; +use crate::text::{Lang, Region, TextElem}; +use crate::visualize::ImageElem; + +/// A figure with an optional caption. +/// +/// Automatically detects its kind to select the correct counting track. For +/// example, figures containing images will be numbered separately from figures +/// containing tables. +/// +/// # Examples +/// The example below shows a basic figure with an image: +/// ```example +/// @glacier shows a glacier. Glaciers +/// are complex systems. +/// +/// #figure( +/// image("glacier.jpg", width: 80%), +/// caption: [A curious figure.], +/// ) <glacier> +/// ``` +/// +/// You can also insert [tables]($table) into figures to give them a caption. +/// The figure will detect this and automatically use a separate counter. +/// +/// ```example +/// #figure( +/// table( +/// columns: 4, +/// [t], [1], [2], [3], +/// [y], [0.3s], [0.4s], [0.8s], +/// ), +/// caption: [Timing results], +/// ) +/// ``` +/// +/// This behaviour can be overridden by explicitly specifying the figure's +/// `kind`. All figures of the same kind share a common counter. +/// +/// # Figure behaviour +/// By default, figures are placed within the flow of content. To make them +/// float to the top or bottom of the page, you can use the +/// [`placement`]($figure.placement) argument. +/// +/// If your figure is too large and its contents are breakable across pages +/// (e.g. if it contains a large table), then you can make the figure itself +/// breakable across pages as well with this show rule: +/// ```typ +/// #show figure: set block(breakable: true) +/// ``` +/// +/// See the [block]($block.breakable) documentation for more information about +/// breakable and non-breakable blocks. +/// +/// # Caption customization +/// You can modify the appearance of the figure's caption with its associated +/// [`caption`]($figure.caption) function. In the example below, we emphasize +/// all captions: +/// +/// ```example +/// #show figure.caption: emph +/// +/// #figure( +/// rect[Hello], +/// caption: [I am emphasized!], +/// ) +/// ``` +/// +/// By using a [`where`]($function.where) selector, we can scope such rules to +/// specific kinds of figures. For example, to position the caption above +/// tables, but keep it below for all other kinds of figures, we could write the +/// following show-set rule: +/// +/// ```example +/// #show figure.where( +/// kind: table +/// ): set figure.caption(position: top) +/// +/// #figure( +/// table(columns: 2)[A][B][C][D], +/// caption: [I'm up here], +/// ) +/// ``` +#[elem(scope, Locatable, Synthesize, Count, Show, ShowSet, Refable, Outlinable)] +pub struct FigureElem { + /// The content of the figure. Often, an [image]. + #[required] + pub body: Content, + + /// The figure's placement on the page. + /// + /// - `{none}`: The figure stays in-flow exactly where it was specified + /// like other content. + /// - `{auto}`: The figure picks `{top}` or `{bottom}` depending on which + /// is closer. + /// - `{top}`: The figure floats to the top of the page. + /// - `{bottom}`: The figure floats to the bottom of the page. + /// + /// The gap between the main flow content and the floating figure is + /// controlled by the [`clearance`]($place.clearance) argument on the + /// `place` function. + /// + /// ```example + /// #set page(height: 200pt) + /// + /// = Introduction + /// #figure( + /// placement: bottom, + /// caption: [A glacier], + /// image("glacier.jpg", width: 60%), + /// ) + /// #lorem(60) + /// ``` + pub placement: Option<Smart<VAlignment>>, + + /// Relative to which containing scope the figure is placed. + /// + /// Set this to `{"parent"}` to create a full-width figure in a two-column + /// document. + /// + /// Has no effect if `placement` is `{none}`. + /// + /// ```example + /// #set page(height: 250pt, columns: 2) + /// + /// = Introduction + /// #figure( + /// placement: bottom, + /// scope: "parent", + /// caption: [A glacier], + /// image("glacier.jpg", width: 60%), + /// ) + /// #lorem(60) + /// ``` + pub scope: PlacementScope, + + /// The figure's caption. + pub caption: Option<Packed<FigureCaption>>, + + /// The kind of figure this is. + /// + /// All figures of the same kind share a common counter. + /// + /// If set to `{auto}`, the figure will try to automatically determine its + /// kind based on the type of its body. Automatically detected kinds are + /// [tables]($table) and [code]($raw). In other cases, the inferred kind is + /// that of an [image]. + /// + /// Setting this to something other than `{auto}` will override the + /// automatic detection. This can be useful if + /// - you wish to create a custom figure type that is not an + /// [image], a [table] or [code]($raw), + /// - you want to force the figure to use a specific counter regardless of + /// its content. + /// + /// You can set the kind to be an element function or a string. If you set + /// it to an element function other than [`{table}`]($table), [`{raw}`](raw) + /// or [`{image}`](image), you will need to manually specify the figure's + /// supplement. + /// + /// ```example + /// #figure( + /// circle(radius: 10pt), + /// caption: [A curious atom.], + /// kind: "atom", + /// supplement: [Atom], + /// ) + /// ``` + pub kind: Smart<FigureKind>, + + /// The figure's supplement. + /// + /// If set to `{auto}`, the figure will try to automatically determine the + /// correct supplement based on the `kind` and the active + /// [text language]($text.lang). If you are using a custom figure type, you + /// will need to manually specify the supplement. + /// + /// If a function is specified, it is passed the first descendant of the + /// specified `kind` (typically, the figure's body) and should return + /// content. + /// + /// ```example + /// #figure( + /// [The contents of my figure!], + /// caption: [My custom figure], + /// supplement: [Bar], + /// kind: "foo", + /// ) + /// ``` + #[borrowed] + pub supplement: Smart<Option<Supplement>>, + + /// How to number the figure. Accepts a + /// [numbering pattern or function]($numbering). + #[default(Some(NumberingPattern::from_str("1").unwrap().into()))] + #[borrowed] + pub numbering: Option<Numbering>, + + /// The vertical gap between the body and caption. + #[default(Em::new(0.65).into())] + pub gap: Length, + + /// Whether the figure should appear in an [`outline`] of figures. + #[default(true)] + pub outlined: bool, + + /// Convenience field to get access to the counter for this figure. + /// + /// The counter only depends on the `kind`: + /// - For (tables)[@table]: `{counter(figure.where(kind: table))}` + /// - For (images)[@image]: `{counter(figure.where(kind: image))}` + /// - For a custom kind: `{counter(figure.where(kind: kind))}` + /// + /// These are the counters you'll need to modify if you want to skip a + /// number or reset the counter. + #[synthesized] + pub counter: Option<Counter>, +} + +#[scope] +impl FigureElem { + #[elem] + type FigureCaption; +} + +impl Synthesize for Packed<FigureElem> { + fn synthesize( + &mut self, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult<()> { + let span = self.span(); + let location = self.location(); + let elem = self.as_mut(); + let numbering = elem.numbering(styles); + + // Determine the figure's kind. + let kind = elem.kind(styles).unwrap_or_else(|| { + elem.body() + .query_first(Selector::can::<dyn Figurable>()) + .map(|elem| FigureKind::Elem(elem.func())) + .unwrap_or_else(|| FigureKind::Elem(ImageElem::elem())) + }); + + // Resolve the supplement. + let supplement = match elem.supplement(styles).as_ref() { + Smart::Auto => { + // Default to the local name for the kind, if available. + let name = match &kind { + FigureKind::Elem(func) => func + .local_name( + TextElem::lang_in(styles), + TextElem::region_in(styles), + ) + .map(TextElem::packed), + FigureKind::Name(_) => None, + }; + + if numbering.is_some() && name.is_none() { + bail!(span, "please specify the figure's supplement") + } + + Some(name.unwrap_or_default()) + } + Smart::Custom(None) => None, + Smart::Custom(Some(supplement)) => { + // Resolve the supplement with the first descendant of the kind or + // just the body, if none was found. + let descendant = match kind { + FigureKind::Elem(func) => elem + .body() + .query_first(Selector::Elem(func, None)) + .map(Cow::Owned), + FigureKind::Name(_) => None, + }; + + let target = descendant.unwrap_or_else(|| Cow::Borrowed(elem.body())); + Some(supplement.resolve(engine, styles, [target])?) + } + }; + + // Construct the figure's counter. + let counter = Counter::new(CounterKey::Selector( + select_where!(FigureElem, Kind => kind.clone()), + )); + + // Fill the figure's caption. + let mut caption = elem.caption(styles); + if let Some(caption) = &mut caption { + caption.push_kind(kind.clone()); + caption.push_supplement(supplement.clone()); + caption.push_numbering(numbering.clone()); + caption.push_counter(Some(counter.clone())); + caption.push_figure_location(location); + } + + elem.push_kind(Smart::Custom(kind)); + elem.push_supplement(Smart::Custom(supplement.map(Supplement::Content))); + elem.push_counter(Some(counter)); + elem.push_caption(caption); + + Ok(()) + } +} + +impl Show for Packed<FigureElem> { + #[typst_macros::time(name = "figure", span = self.span())] + fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let mut realized = self.body().clone(); + + // Build the caption, if any. + if let Some(caption) = self.caption(styles) { + let v = VElem::new(self.gap(styles).into()).with_weak(true).pack(); + realized = match caption.position(styles) { + OuterVAlignment::Top => caption.pack() + v + realized, + OuterVAlignment::Bottom => realized + v + caption.pack(), + }; + } + + // Wrap the contents in a block. + realized = BlockElem::new() + .with_body(Some(BlockBody::Content(realized))) + .pack() + .spanned(self.span()); + + // Wrap in a float. + if let Some(align) = self.placement(styles) { + realized = PlaceElem::new(realized) + .with_alignment(align.map(|align| HAlignment::Center + align)) + .with_scope(self.scope(styles)) + .with_float(true) + .pack() + .spanned(self.span()); + } else if self.scope(styles) == PlacementScope::Parent { + bail!( + self.span(), + "parent-scoped placement is only available for floating figures"; + hint: "you can enable floating placement with `figure(placement: auto, ..)`" + ); + } + + Ok(realized) + } +} + +impl ShowSet for Packed<FigureElem> { + fn show_set(&self, _: StyleChain) -> Styles { + // Still allows breakable figures with + // `show figure: set block(breakable: true)`. + let mut map = Styles::new(); + map.set(BlockElem::set_breakable(false)); + map.set(AlignElem::set_alignment(Alignment::CENTER)); + map + } +} + +impl Count for Packed<FigureElem> { + fn update(&self) -> Option<CounterUpdate> { + // If the figure is numbered, step the counter by one. + // This steps the `counter(figure)` which is global to all numbered figures. + self.numbering() + .is_some() + .then(|| CounterUpdate::Step(NonZeroUsize::ONE)) + } +} + +impl Refable for Packed<FigureElem> { + fn supplement(&self) -> Content { + // After synthesis, this should always be custom content. + match (**self).supplement(StyleChain::default()).as_ref() { + Smart::Custom(Some(Supplement::Content(content))) => content.clone(), + _ => Content::empty(), + } + } + + fn counter(&self) -> Counter { + (**self) + .counter() + .cloned() + .flatten() + .unwrap_or_else(|| Counter::of(FigureElem::elem())) + } + + fn numbering(&self) -> Option<&Numbering> { + (**self).numbering(StyleChain::default()).as_ref() + } +} + +impl Outlinable for Packed<FigureElem> { + fn outline( + &self, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult<Option<Content>> { + if !self.outlined(StyleChain::default()) { + return Ok(None); + } + + let Some(caption) = self.caption(StyleChain::default()) else { + return Ok(None); + }; + + let mut realized = caption.body().clone(); + if let ( + Smart::Custom(Some(Supplement::Content(mut supplement))), + Some(Some(counter)), + Some(numbering), + ) = ( + (**self).supplement(StyleChain::default()).clone(), + (**self).counter(), + self.numbering(), + ) { + let numbers = counter.display_at_loc( + engine, + self.location().unwrap(), + styles, + numbering, + )?; + + if !supplement.is_empty() { + supplement += TextElem::packed('\u{a0}'); + } + + let separator = caption.get_separator(StyleChain::default()); + + realized = supplement + numbers + separator + caption.body(); + } + + Ok(Some(realized)) + } +} + +/// The caption of a figure. This element can be used in set and show rules to +/// customize the appearance of captions for all figures or figures of a +/// specific kind. +/// +/// In addition to its `pos` and `body`, the `caption` also provides the +/// figure's `kind`, `supplement`, `counter`, and `numbering` as fields. These +/// parts can be used in [`where`]($function.where) selectors and show rules to +/// build a completely custom caption. +/// +/// ```example +/// #show figure.caption: emph +/// +/// #figure( +/// rect[Hello], +/// caption: [A rectangle], +/// ) +/// ``` +#[elem(name = "caption", Synthesize, Show)] +pub struct FigureCaption { + /// The caption's position in the figure. Either `{top}` or `{bottom}`. + /// + /// ```example + /// #show figure.where( + /// kind: table + /// ): set figure.caption(position: top) + /// + /// #figure( + /// table(columns: 2)[A][B], + /// caption: [I'm up here], + /// ) + /// + /// #figure( + /// rect[Hi], + /// caption: [I'm down here], + /// ) + /// + /// #figure( + /// table(columns: 2)[A][B], + /// caption: figure.caption( + /// position: bottom, + /// [I'm down here too!] + /// ) + /// ) + /// ``` + #[default(OuterVAlignment::Bottom)] + pub position: OuterVAlignment, + + /// The separator which will appear between the number and body. + /// + /// If set to `{auto}`, the separator will be adapted to the current + /// [language]($text.lang) and [region]($text.region). + /// + /// ```example + /// #set figure.caption(separator: [ --- ]) + /// + /// #figure( + /// rect[Hello], + /// caption: [A rectangle], + /// ) + /// ``` + pub separator: Smart<Content>, + + /// The caption's body. + /// + /// Can be used alongside `kind`, `supplement`, `counter`, `numbering`, and + /// `location` to completely customize the caption. + /// + /// ```example + /// #show figure.caption: it => [ + /// #underline(it.body) | + /// #it.supplement + /// #context it.counter.display(it.numbering) + /// ] + /// + /// #figure( + /// rect[Hello], + /// caption: [A rectangle], + /// ) + /// ``` + #[required] + pub body: Content, + + /// The figure's supplement. + #[synthesized] + pub kind: FigureKind, + + /// The figure's supplement. + #[synthesized] + pub supplement: Option<Content>, + + /// How to number the figure. + #[synthesized] + pub numbering: Option<Numbering>, + + /// The counter for the figure. + #[synthesized] + pub counter: Option<Counter>, + + /// The figure's location. + #[internal] + #[synthesized] + pub figure_location: Option<Location>, +} + +impl FigureCaption { + /// Gets the default separator in the given language and (optionally) + /// region. + fn local_separator(lang: Lang, _: Option<Region>) -> &'static str { + match lang { + Lang::CHINESE => "\u{2003}", + Lang::FRENCH => ".\u{a0}– ", + Lang::RUSSIAN => ". ", + Lang::ENGLISH | _ => ": ", + } + } + + fn get_separator(&self, styles: StyleChain) -> Content { + self.separator(styles).unwrap_or_else(|| { + TextElem::packed(Self::local_separator( + TextElem::lang_in(styles), + TextElem::region_in(styles), + )) + }) + } +} + +impl Synthesize for Packed<FigureCaption> { + fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> { + let elem = self.as_mut(); + elem.push_separator(Smart::Custom(elem.get_separator(styles))); + Ok(()) + } +} + +impl Show for Packed<FigureCaption> { + #[typst_macros::time(name = "figure.caption", span = self.span())] + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let mut realized = self.body().clone(); + + if let ( + Some(Some(mut supplement)), + Some(Some(numbering)), + Some(Some(counter)), + Some(Some(location)), + ) = ( + self.supplement().cloned(), + self.numbering(), + self.counter(), + self.figure_location(), + ) { + let numbers = counter.display_at_loc(engine, *location, styles, numbering)?; + if !supplement.is_empty() { + supplement += TextElem::packed('\u{a0}'); + } + realized = supplement + numbers + self.get_separator(styles) + realized; + } + + Ok(realized) + } +} + +cast! { + FigureCaption, + v: Content => v.unpack::<Self>().unwrap_or_else(Self::new), +} + +/// The `kind` parameter of a [`FigureElem`]. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum FigureKind { + /// The kind is an element function. + Elem(Element), + /// The kind is a name. + Name(EcoString), +} + +cast! { + FigureKind, + self => match self { + Self::Elem(v) => v.into_value(), + Self::Name(v) => v.into_value(), + }, + v: Element => Self::Elem(v), + v: EcoString => Self::Name(v), +} + +/// An element that can be auto-detected in a figure. +/// +/// This trait is used to determine the type of a figure. +pub trait Figurable {} diff --git a/crates/typst-library/src/model/footnote.rs b/crates/typst-library/src/model/footnote.rs new file mode 100644 index 00000000..d9971dd1 --- /dev/null +++ b/crates/typst-library/src/model/footnote.rs @@ -0,0 +1,326 @@ +use std::num::NonZeroUsize; +use std::str::FromStr; + +use typst_utils::NonZeroExt; + +use crate::diag::{bail, At, SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, scope, Content, Label, NativeElement, Packed, Show, ShowSet, Smart, + StyleChain, Styles, +}; +use crate::introspection::{Count, Counter, CounterUpdate, Locatable, Location}; +use crate::layout::{Abs, Em, HElem, Length, Ratio}; +use crate::model::{Destination, Numbering, NumberingPattern, ParElem}; +use crate::text::{SuperElem, TextElem, TextSize}; +use crate::visualize::{LineElem, Stroke}; + +/// A footnote. +/// +/// Includes additional remarks and references on the same page with footnotes. +/// A footnote will insert a superscript number that links to the note at the +/// bottom of the page. Notes are numbered sequentially throughout your document +/// and can break across multiple pages. +/// +/// To customize the appearance of the entry in the footnote listing, see +/// [`footnote.entry`]($footnote.entry). The footnote itself is realized as a +/// normal superscript, so you can use a set rule on the [`super`] function to +/// customize it. You can also apply a show rule to customize only the footnote +/// marker (superscript number) in the running text. +/// +/// # Example +/// ```example +/// Check the docs for more details. +/// #footnote[https://typst.app/docs] +/// ``` +/// +/// The footnote automatically attaches itself to the preceding word, even if +/// there is a space before it in the markup. To force space, you can use the +/// string `[#" "]` or explicit [horizontal spacing]($h). +/// +/// By giving a label to a footnote, you can have multiple references to it. +/// +/// ```example +/// You can edit Typst documents online. +/// #footnote[https://typst.app/app] <fn> +/// Checkout Typst's website. @fn +/// And the online app. #footnote(<fn>) +/// ``` +/// +/// _Note:_ Set and show rules in the scope where `footnote` is called may not +/// apply to the footnote's content. See [here][issue] for more information. +/// +/// [issue]: https://github.com/typst/typst/issues/1467#issuecomment-1588799440 +#[elem(scope, Locatable, Show, Count)] +pub struct FootnoteElem { + /// How to number footnotes. + /// + /// By default, the footnote numbering continues throughout your document. + /// If you prefer per-page footnote numbering, you can reset the footnote + /// [counter] in the page [header]($page.header). In the future, there might + /// be a simpler way to achieve this. + /// + /// ```example + /// #set footnote(numbering: "*") + /// + /// Footnotes: + /// #footnote[Star], + /// #footnote[Dagger] + /// ``` + #[borrowed] + #[default(Numbering::Pattern(NumberingPattern::from_str("1").unwrap()))] + pub numbering: Numbering, + + /// The content to put into the footnote. Can also be the label of another + /// footnote this one should point to. + #[required] + pub body: FootnoteBody, +} + +#[scope] +impl FootnoteElem { + #[elem] + type FootnoteEntry; +} + +impl FootnoteElem { + /// Creates a new footnote that the passed content as its body. + pub fn with_content(content: Content) -> Self { + Self::new(FootnoteBody::Content(content)) + } + + /// Creates a new footnote referencing the footnote with the specified label. + pub fn with_label(label: Label) -> Self { + Self::new(FootnoteBody::Reference(label)) + } + + /// Creates a new footnote referencing the footnote with the specified label, + /// with the other fields from the current footnote cloned. + pub fn into_ref(&self, label: Label) -> Self { + Self { + body: FootnoteBody::Reference(label), + ..self.clone() + } + } + + /// Tests if this footnote is a reference to another footnote. + pub fn is_ref(&self) -> bool { + matches!(self.body(), FootnoteBody::Reference(_)) + } + + /// Returns the content of the body of this footnote if it is not a ref. + pub fn body_content(&self) -> Option<&Content> { + match self.body() { + FootnoteBody::Content(content) => Some(content), + _ => None, + } + } +} + +impl Packed<FootnoteElem> { + /// Returns the location of the definition of this footnote. + pub fn declaration_location(&self, engine: &Engine) -> StrResult<Location> { + match self.body() { + FootnoteBody::Reference(label) => { + let element = engine.introspector.query_label(*label)?; + let footnote = element + .to_packed::<FootnoteElem>() + .ok_or("referenced element should be a footnote")?; + if self.location() == footnote.location() { + bail!("footnote cannot reference itself"); + } + footnote.declaration_location(engine) + } + _ => Ok(self.location().unwrap()), + } + } +} + +impl Show for Packed<FootnoteElem> { + #[typst_macros::time(name = "footnote", span = self.span())] + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let span = self.span(); + let loc = self.declaration_location(engine).at(span)?; + let numbering = self.numbering(styles); + let counter = Counter::of(FootnoteElem::elem()); + let num = counter.display_at_loc(engine, loc, styles, numbering)?; + let sup = SuperElem::new(num).pack().spanned(span); + let loc = loc.variant(1); + // Add zero-width weak spacing to make the footnote "sticky". + Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc))) + } +} + +impl Count for Packed<FootnoteElem> { + fn update(&self) -> Option<CounterUpdate> { + (!self.is_ref()).then(|| CounterUpdate::Step(NonZeroUsize::ONE)) + } +} + +/// The body of a footnote can be either some content or a label referencing +/// another footnote. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum FootnoteBody { + Content(Content), + Reference(Label), +} + +cast! { + FootnoteBody, + self => match self { + Self::Content(v) => v.into_value(), + Self::Reference(v) => v.into_value(), + }, + v: Content => Self::Content(v), + v: Label => Self::Reference(v), +} + +/// An entry in a footnote list. +/// +/// This function is not intended to be called directly. Instead, it is used in +/// set and show rules to customize footnote listings. +/// +/// ```example +/// #show footnote.entry: set text(red) +/// +/// My footnote listing +/// #footnote[It's down here] +/// has red text! +/// ``` +/// +/// _Note:_ Footnote entry properties must be uniform across each page run (a +/// page run is a sequence of pages without an explicit pagebreak in between). +/// For this reason, set and show rules for footnote entries should be defined +/// before any page content, typically at the very start of the document. +#[elem(name = "entry", title = "Footnote Entry", Show, ShowSet)] +pub struct FootnoteEntry { + /// The footnote for this entry. It's location can be used to determine + /// the footnote counter state. + /// + /// ```example + /// #show footnote.entry: it => { + /// let loc = it.note.location() + /// numbering( + /// "1: ", + /// ..counter(footnote).at(loc), + /// ) + /// it.note.body + /// } + /// + /// Customized #footnote[Hello] + /// listing #footnote[World! 🌏] + /// ``` + #[required] + pub note: Packed<FootnoteElem>, + + /// The separator between the document body and the footnote listing. + /// + /// ```example + /// #set footnote.entry( + /// separator: repeat[.] + /// ) + /// + /// Testing a different separator. + /// #footnote[ + /// Unconventional, but maybe + /// not that bad? + /// ] + /// ``` + #[default( + LineElem::new() + .with_length(Ratio::new(0.3).into()) + .with_stroke(Stroke { + thickness: Smart::Custom(Abs::pt(0.5).into()), + ..Default::default() + }) + .pack() + )] + pub separator: Content, + + /// The amount of clearance between the document body and the separator. + /// + /// ```example + /// #set footnote.entry(clearance: 3em) + /// + /// Footnotes also need ... + /// #footnote[ + /// ... some space to breathe. + /// ] + /// ``` + #[default(Em::new(1.0).into())] + #[resolve] + pub clearance: Length, + + /// The gap between footnote entries. + /// + /// ```example + /// #set footnote.entry(gap: 0.8em) + /// + /// Footnotes: + /// #footnote[Spaced], + /// #footnote[Apart] + /// ``` + #[default(Em::new(0.5).into())] + #[resolve] + pub gap: Length, + + /// The indent of each footnote entry. + /// + /// ```example + /// #set footnote.entry(indent: 0em) + /// + /// Footnotes: + /// #footnote[No], + /// #footnote[Indent] + /// ``` + #[default(Em::new(1.0).into())] + pub indent: Length, +} + +impl Show for Packed<FootnoteEntry> { + #[typst_macros::time(name = "footnote.entry", span = self.span())] + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let span = self.span(); + let note = self.note(); + let number_gap = Em::new(0.05); + let default = StyleChain::default(); + let numbering = note.numbering(default); + let counter = Counter::of(FootnoteElem::elem()); + let Some(loc) = note.location() else { + bail!( + span, "footnote entry must have a location"; + hint: "try using a query or a show rule to customize the footnote instead" + ); + }; + + let num = counter.display_at_loc(engine, loc, styles, numbering)?; + let sup = SuperElem::new(num) + .pack() + .spanned(span) + .linked(Destination::Location(loc)) + .located(loc.variant(1)); + + Ok(Content::sequence([ + HElem::new(self.indent(styles).into()).pack(), + sup, + HElem::new(number_gap.into()).with_weak(true).pack(), + note.body_content().unwrap().clone(), + ])) + } +} + +impl ShowSet for Packed<FootnoteEntry> { + fn show_set(&self, _: StyleChain) -> Styles { + let text_size = Em::new(0.85); + let leading = Em::new(0.5); + let mut out = Styles::new(); + out.set(ParElem::set_leading(leading.into())); + out.set(TextElem::set_size(TextSize(text_size.into()))); + out + } +} + +cast! { + FootnoteElem, + v: Content => v.unpack::<Self>().unwrap_or_else(Self::with_content) +} diff --git a/crates/typst-library/src/model/heading.rs b/crates/typst-library/src/model/heading.rs new file mode 100644 index 00000000..269e95eb --- /dev/null +++ b/crates/typst-library/src/model/heading.rs @@ -0,0 +1,351 @@ +use std::num::NonZeroUsize; + +use typst_utils::NonZeroExt; + +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{ + elem, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain, + Styles, Synthesize, +}; +use crate::introspection::{ + Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink, +}; +use crate::layout::{Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region}; +use crate::model::{Numbering, Outlinable, ParElem, Refable, Supplement}; +use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize}; + +/// A section heading. +/// +/// With headings, you can structure your document into sections. Each heading +/// has a _level,_ which starts at one and is unbounded upwards. This level +/// indicates the logical role of the following content (section, subsection, +/// etc.) A top-level heading indicates a top-level section of the document +/// (not the document's title). +/// +/// Typst can automatically number your headings for you. To enable numbering, +/// specify how you want your headings to be numbered with a +/// [numbering pattern or function]($numbering). +/// +/// Independently of the numbering, Typst can also automatically generate an +/// [outline] of all headings for you. To exclude one or more headings from this +/// outline, you can set the `outlined` parameter to `{false}`. +/// +/// # Example +/// ```example +/// #set heading(numbering: "1.a)") +/// +/// = Introduction +/// In recent years, ... +/// +/// == Preliminaries +/// To start, ... +/// ``` +/// +/// # Syntax +/// Headings have dedicated syntax: They can be created by starting a line with +/// one or multiple equals signs, followed by a space. The number of equals +/// signs determines the heading's logical nesting depth. The `{offset}` field +/// can be set to configure the starting depth. +#[elem(Locatable, Synthesize, Count, Show, ShowSet, LocalName, Refable, Outlinable)] +pub struct HeadingElem { + /// The absolute nesting depth of the heading, starting from one. If set + /// to `{auto}`, it is computed from `{offset + depth}`. + /// + /// This is primarily useful for usage in [show rules]($styling/#show-rules) + /// (either with [`where`]($function.where) selectors or by accessing the + /// level directly on a shown heading). + /// + /// ```example + /// #show heading.where(level: 2): set text(red) + /// + /// = Level 1 + /// == Level 2 + /// + /// #set heading(offset: 1) + /// = Also level 2 + /// == Level 3 + /// ``` + pub level: Smart<NonZeroUsize>, + + /// The relative nesting depth of the heading, starting from one. This is + /// combined with `{offset}` to compute the actual `{level}`. + /// + /// This is set by the heading syntax, such that `[== Heading]` creates a + /// heading with logical depth of 2, but actual level `{offset + 2}`. If you + /// construct a heading manually, you should typically prefer this over + /// setting the absolute level. + #[default(NonZeroUsize::ONE)] + pub depth: NonZeroUsize, + + /// The starting offset of each heading's `{level}`, used to turn its + /// relative `{depth}` into its absolute `{level}`. + /// + /// ```example + /// = Level 1 + /// + /// #set heading(offset: 1, numbering: "1.1") + /// = Level 2 + /// + /// #heading(offset: 2, depth: 2)[ + /// I'm level 4 + /// ] + /// ``` + #[default(0)] + pub offset: usize, + + /// How to number the heading. Accepts a + /// [numbering pattern or function]($numbering). + /// + /// ```example + /// #set heading(numbering: "1.a.") + /// + /// = A section + /// == A subsection + /// === A sub-subsection + /// ``` + #[borrowed] + pub numbering: Option<Numbering>, + + /// A supplement for the heading. + /// + /// For references to headings, this is added before the referenced number. + /// + /// If a function is specified, it is passed the referenced heading and + /// should return content. + /// + /// ```example + /// #set heading(numbering: "1.", supplement: [Chapter]) + /// + /// = Introduction <intro> + /// In @intro, we see how to turn + /// Sections into Chapters. And + /// in @intro[Part], it is done + /// manually. + /// ``` + pub supplement: Smart<Option<Supplement>>, + + /// Whether the heading should appear in the [outline]. + /// + /// Note that this property, if set to `{true}`, ensures the heading is also + /// shown as a bookmark in the exported PDF's outline (when exporting to + /// PDF). To change that behavior, use the `bookmarked` property. + /// + /// ```example + /// #outline() + /// + /// #heading[Normal] + /// This is a normal heading. + /// + /// #heading(outlined: false)[Hidden] + /// This heading does not appear + /// in the outline. + /// ``` + #[default(true)] + pub outlined: bool, + + /// Whether the heading should appear as a bookmark in the exported PDF's + /// outline. Doesn't affect other export formats, such as PNG. + /// + /// The default value of `{auto}` indicates that the heading will only + /// appear in the exported PDF's outline if its `outlined` property is set + /// to `{true}`, that is, if it would also be listed in Typst's [outline]. + /// Setting this property to either `{true}` (bookmark) or `{false}` (don't + /// bookmark) bypasses that behavior. + /// + /// ```example + /// #heading[Normal heading] + /// This heading will be shown in + /// the PDF's bookmark outline. + /// + /// #heading(bookmarked: false)[Not bookmarked] + /// This heading won't be + /// bookmarked in the resulting + /// PDF. + /// ``` + #[default(Smart::Auto)] + pub bookmarked: Smart<bool>, + + /// The indent all but the first line of a heading should have. + /// + /// The default value of `{auto}` indicates that the subsequent heading + /// lines will be indented based on the width of the numbering. + /// + /// ```example + /// #set heading(numbering: "1.") + /// #heading[A very, very, very, very, very, very long heading] + /// ``` + #[default(Smart::Auto)] + pub hanging_indent: Smart<Length>, + + /// The heading's title. + #[required] + pub body: Content, +} + +impl HeadingElem { + pub fn resolve_level(&self, styles: StyleChain) -> NonZeroUsize { + self.level(styles).unwrap_or_else(|| { + NonZeroUsize::new(self.offset(styles) + self.depth(styles).get()) + .expect("overflow to 0 on NoneZeroUsize + usize") + }) + } +} + +impl Synthesize for Packed<HeadingElem> { + fn synthesize( + &mut self, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult<()> { + let supplement = match (**self).supplement(styles) { + Smart::Auto => TextElem::packed(Self::local_name_in(styles)), + Smart::Custom(None) => Content::empty(), + Smart::Custom(Some(supplement)) => { + supplement.resolve(engine, styles, [self.clone().pack()])? + } + }; + + let elem = self.as_mut(); + elem.push_level(Smart::Custom(elem.resolve_level(styles))); + elem.push_supplement(Smart::Custom(Some(Supplement::Content(supplement)))); + Ok(()) + } +} + +impl Show for Packed<HeadingElem> { + #[typst_macros::time(name = "heading", span = self.span())] + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + const SPACING_TO_NUMBERING: Em = Em::new(0.3); + + let span = self.span(); + let mut realized = self.body().clone(); + + let hanging_indent = self.hanging_indent(styles); + let mut indent = match hanging_indent { + Smart::Custom(length) => length.resolve(styles), + Smart::Auto => Abs::zero(), + }; + + if let Some(numbering) = (**self).numbering(styles).as_ref() { + let location = self.location().unwrap(); + let numbering = Counter::of(HeadingElem::elem()) + .display_at_loc(engine, location, styles, numbering)? + .spanned(span); + + if hanging_indent.is_auto() { + let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false)); + + // We don't have a locator for the numbering here, so we just + // use the measurement infrastructure for now. + let link = LocatorLink::measure(location); + let size = (engine.routines.layout_frame)( + engine, + &numbering, + Locator::link(&link), + styles, + pod, + )? + .size(); + + indent = size.x + SPACING_TO_NUMBERING.resolve(styles); + } + + realized = numbering + + HElem::new(SPACING_TO_NUMBERING.into()).with_weak(true).pack() + + realized; + } + + if indent != Abs::zero() { + realized = realized.styled(ParElem::set_hanging_indent(indent.into())); + } + + Ok(BlockElem::new() + .with_body(Some(BlockBody::Content(realized))) + .pack() + .spanned(span)) + } +} + +impl ShowSet for Packed<HeadingElem> { + fn show_set(&self, styles: StyleChain) -> Styles { + let level = (**self).resolve_level(styles).get(); + let scale = match level { + 1 => 1.4, + 2 => 1.2, + _ => 1.0, + }; + + let size = Em::new(scale); + let above = Em::new(if level == 1 { 1.8 } else { 1.44 }) / scale; + let below = Em::new(0.75) / scale; + + let mut out = Styles::new(); + out.set(TextElem::set_size(TextSize(size.into()))); + out.set(TextElem::set_weight(FontWeight::BOLD)); + out.set(BlockElem::set_above(Smart::Custom(above.into()))); + out.set(BlockElem::set_below(Smart::Custom(below.into()))); + out.set(BlockElem::set_sticky(true)); + out + } +} + +impl Count for Packed<HeadingElem> { + fn update(&self) -> Option<CounterUpdate> { + (**self) + .numbering(StyleChain::default()) + .is_some() + .then(|| CounterUpdate::Step((**self).resolve_level(StyleChain::default()))) + } +} + +impl Refable for Packed<HeadingElem> { + fn supplement(&self) -> Content { + // After synthesis, this should always be custom content. + match (**self).supplement(StyleChain::default()) { + Smart::Custom(Some(Supplement::Content(content))) => content, + _ => Content::empty(), + } + } + + fn counter(&self) -> Counter { + Counter::of(HeadingElem::elem()) + } + + fn numbering(&self) -> Option<&Numbering> { + (**self).numbering(StyleChain::default()).as_ref() + } +} + +impl Outlinable for Packed<HeadingElem> { + fn outline( + &self, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult<Option<Content>> { + if !self.outlined(StyleChain::default()) { + return Ok(None); + } + + let mut content = self.body().clone(); + if let Some(numbering) = (**self).numbering(StyleChain::default()).as_ref() { + let numbers = Counter::of(HeadingElem::elem()).display_at_loc( + engine, + self.location().unwrap(), + styles, + numbering, + )?; + content = numbers + SpaceElem::shared().clone() + content; + }; + + Ok(Some(content)) + } + + fn level(&self) -> NonZeroUsize { + (**self).resolve_level(StyleChain::default()) + } +} + +impl LocalName for Packed<HeadingElem> { + const KEY: &'static str = "heading"; +} diff --git a/crates/typst-library/src/model/link.rs b/crates/typst-library/src/model/link.rs new file mode 100644 index 00000000..31c65a1d --- /dev/null +++ b/crates/typst-library/src/model/link.rs @@ -0,0 +1,212 @@ +use std::ops::Deref; + +use ecow::{eco_format, EcoString}; +use smallvec::SmallVec; + +use crate::diag::{bail, At, SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, Content, Label, Packed, Repr, Show, Smart, StyleChain, +}; +use crate::introspection::Location; +use crate::layout::Position; +use crate::text::{Hyphenate, TextElem}; + +/// Links to a URL or a location in the document. +/// +/// By default, links are not styled any different from normal text. However, +/// you can easily apply a style of your choice with a show rule. +/// +/// # Example +/// ```example +/// #show link: underline +/// +/// https://example.com \ +/// +/// #link("https://example.com") \ +/// #link("https://example.com")[ +/// See example.com +/// ] +/// ``` +/// +/// # Syntax +/// This function also has dedicated syntax: Text that starts with `http://` or +/// `https://` is automatically turned into a link. +#[elem(Show)] +pub struct LinkElem { + /// The destination the link points to. + /// + /// - To link to web pages, `dest` should be a valid URL string. If the URL + /// is in the `mailto:` or `tel:` scheme and the `body` parameter is + /// omitted, the email address or phone number will be the link's body, + /// without the scheme. + /// + /// - To link to another part of the document, `dest` can take one of three + /// forms: + /// - A [label] attached to an element. If you also want automatic text + /// for the link based on the element, consider using a + /// [reference]($ref) instead. + /// + /// - A [`location`] (typically retrieved from [`here`], [`locate`] or + /// [`query`]). + /// + /// - A dictionary with a `page` key of type [integer]($int) and `x` and + /// `y` coordinates of type [length]. Pages are counted from one, and + /// the coordinates are relative to the page's top left corner. + /// + /// ```example + /// = Introduction <intro> + /// #link("mailto:hello@typst.app") \ + /// #link(<intro>)[Go to intro] \ + /// #link((page: 1, x: 0pt, y: 0pt))[ + /// Go to top + /// ] + /// ``` + #[required] + #[parse( + let dest = args.expect::<LinkTarget>("destination")?; + dest.clone() + )] + pub dest: LinkTarget, + + /// The content that should become a link. + /// + /// If `dest` is an URL string, the parameter can be omitted. In this case, + /// the URL will be shown as the link. + #[required] + #[parse(match &dest { + LinkTarget::Dest(Destination::Url(url)) => match args.eat()? { + Some(body) => body, + None => body_from_url(url), + }, + _ => args.expect("body")?, + })] + pub body: Content, + + /// This style is set on the content contained in the `link` element. + #[internal] + #[ghost] + pub dests: SmallVec<[Destination; 1]>, +} + +impl LinkElem { + /// Create a link element from a URL with its bare text. + pub fn from_url(url: Url) -> Self { + let body = body_from_url(&url); + Self::new(LinkTarget::Dest(Destination::Url(url)), body) + } +} + +impl Show for Packed<LinkElem> { + #[typst_macros::time(name = "link", span = self.span())] + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + let body = self.body().clone(); + let linked = match self.dest() { + LinkTarget::Dest(dest) => body.linked(dest.clone()), + LinkTarget::Label(label) => { + let elem = engine.introspector.query_label(*label).at(self.span())?; + let dest = Destination::Location(elem.location().unwrap()); + body.clone().linked(dest) + } + }; + + Ok(linked.styled(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false))))) + } +} + +fn body_from_url(url: &Url) -> Content { + let mut text = url.as_str(); + for prefix in ["mailto:", "tel:"] { + text = text.trim_start_matches(prefix); + } + let shorter = text.len() < url.len(); + TextElem::packed(if shorter { text.into() } else { (**url).clone() }) +} + +/// A target where a link can go. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum LinkTarget { + Dest(Destination), + Label(Label), +} + +cast! { + LinkTarget, + self => match self { + Self::Dest(v) => v.into_value(), + Self::Label(v) => v.into_value(), + }, + v: Destination => Self::Dest(v), + v: Label => Self::Label(v), +} + +impl From<Destination> for LinkTarget { + fn from(dest: Destination) -> Self { + Self::Dest(dest) + } +} + +/// A link destination. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum Destination { + /// A link to a URL. + Url(Url), + /// A link to a point on a page. + Position(Position), + /// An unresolved link to a location in the document. + Location(Location), +} + +impl Destination {} + +impl Repr for Destination { + fn repr(&self) -> EcoString { + eco_format!("{self:?}") + } +} + +cast! { + Destination, + self => match self { + Self::Url(v) => v.into_value(), + Self::Position(v) => v.into_value(), + Self::Location(v) => v.into_value(), + }, + v: Url => Self::Url(v), + v: Position => Self::Position(v), + v: Location => Self::Location(v), +} + +/// A uniform resource locator with a maximum length. +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Url(EcoString); + +impl Url { + /// Create a URL from a string, checking the maximum length. + pub fn new(url: impl Into<EcoString>) -> StrResult<Self> { + let url = url.into(); + if url.len() > 8000 { + bail!("URL is too long") + } + Ok(Self(url)) + } + + /// Extract the underlying [`EcoString`]. + pub fn into_inner(self) -> EcoString { + self.0 + } +} + +impl Deref for Url { + type Target = EcoString; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +cast! { + Url, + self => self.0.into_value(), + v: EcoString => Self::new(v)?, +} diff --git a/crates/typst-library/src/model/list.rs b/crates/typst-library/src/model/list.rs new file mode 100644 index 00000000..18bddd10 --- /dev/null +++ b/crates/typst-library/src/model/list.rs @@ -0,0 +1,246 @@ +use comemo::Track; + +use crate::diag::{bail, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, scope, Array, Content, Context, Depth, Func, NativeElement, Packed, Show, + Smart, StyleChain, Styles, Value, +}; +use crate::layout::{BlockElem, Em, Length, VElem}; +use crate::model::ParElem; +use crate::text::TextElem; + +/// A bullet list. +/// +/// Displays a sequence of items vertically, with each item introduced by a +/// marker. +/// +/// # Example +/// ```example +/// Normal list. +/// - Text +/// - Math +/// - Layout +/// - ... +/// +/// Multiple lines. +/// - This list item spans multiple +/// lines because it is indented. +/// +/// Function call. +/// #list( +/// [Foundations], +/// [Calculate], +/// [Construct], +/// [Data Loading], +/// ) +/// ``` +/// +/// # Syntax +/// This functions also has dedicated syntax: Start a line with a hyphen, +/// followed by a space to create a list item. A list item can contain multiple +/// paragraphs and other block-level content. All content that is indented +/// more than an item's marker becomes part of that item. +#[elem(scope, title = "Bullet List", Show)] +pub struct ListElem { + /// Defines the default [spacing]($list.spacing) of the list. If it is + /// `{false}`, the items are spaced apart with + /// [paragraph spacing]($par.spacing). If it is `{true}`, they use + /// [paragraph leading]($par.leading) instead. This makes the list more + /// compact, which can look better if the items are short. + /// + /// In markup mode, the value of this parameter is determined based on + /// whether items are separated with a blank line. If items directly follow + /// each other, this is set to `{true}`; if items are separated by a blank + /// line, this is set to `{false}`. The markup-defined tightness cannot be + /// overridden with set rules. + /// + /// ```example + /// - If a list has a lot of text, and + /// maybe other inline content, it + /// should not be tight anymore. + /// + /// - To make a list wide, simply insert + /// a blank line between the items. + /// ``` + #[default(true)] + pub tight: bool, + + /// The marker which introduces each item. + /// + /// Instead of plain content, you can also pass an array with multiple + /// markers that should be used for nested lists. If the list nesting depth + /// exceeds the number of markers, the markers are cycled. For total + /// control, you may pass a function that maps the list's nesting depth + /// (starting from `{0}`) to a desired marker. + /// + /// ```example + /// #set list(marker: [--]) + /// - A more classic list + /// - With en-dashes + /// + /// #set list(marker: ([•], [--])) + /// - Top-level + /// - Nested + /// - Items + /// - Items + /// ``` + #[borrowed] + #[default(ListMarker::Content(vec![ + // These are all available in the default font, vertically centered, and + // roughly of the same size (with the last one having slightly lower + // weight because it is not filled). + TextElem::packed('\u{2022}'), // Bullet + TextElem::packed('\u{2023}'), // Triangular Bullet + TextElem::packed('\u{2013}'), // En-dash + ]))] + pub marker: ListMarker, + + /// The indent of each item. + #[resolve] + pub indent: Length, + + /// The spacing between the marker and the body of each item. + #[resolve] + #[default(Em::new(0.5).into())] + pub body_indent: Length, + + /// The spacing between the items of the list. + /// + /// If set to `{auto}`, uses paragraph [`leading`]($par.leading) for tight + /// lists and paragraph [`spacing`]($par.spacing) for wide (non-tight) + /// lists. + pub spacing: Smart<Length>, + + /// The bullet list's children. + /// + /// When using the list syntax, adjacent items are automatically collected + /// into lists, even through constructs like for loops. + /// + /// ```example + /// #for letter in "ABC" [ + /// - Letter #letter + /// ] + /// ``` + #[variadic] + pub children: Vec<Packed<ListItem>>, + + /// The nesting depth. + #[internal] + #[fold] + #[ghost] + pub depth: Depth, +} + +#[scope] +impl ListElem { + #[elem] + type ListItem; +} + +impl Show for Packed<ListElem> { + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let mut realized = + BlockElem::multi_layouter(self.clone(), engine.routines.layout_list) + .pack() + .spanned(self.span()); + + if self.tight(styles) { + let leading = ParElem::leading_in(styles); + let spacing = + VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); + realized = spacing + realized; + } + + Ok(realized) + } +} + +/// A bullet list item. +#[elem(name = "item", title = "Bullet List Item")] +pub struct ListItem { + /// The item's body. + #[required] + pub body: Content, +} + +cast! { + ListItem, + v: Content => v.unpack::<Self>().unwrap_or_else(Self::new) +} + +/// A list's marker. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum ListMarker { + Content(Vec<Content>), + Func(Func), +} + +impl ListMarker { + /// Resolve the marker for the given depth. + pub fn resolve( + &self, + engine: &mut Engine, + styles: StyleChain, + depth: usize, + ) -> SourceResult<Content> { + Ok(match self { + Self::Content(list) => { + list.get(depth % list.len()).cloned().unwrap_or_default() + } + Self::Func(func) => func + .call(engine, Context::new(None, Some(styles)).track(), [depth])? + .display(), + }) + } +} + +cast! { + ListMarker, + self => match self { + Self::Content(vec) => if vec.len() == 1 { + vec.into_iter().next().unwrap().into_value() + } else { + vec.into_value() + }, + Self::Func(func) => func.into_value(), + }, + v: Content => Self::Content(vec![v]), + array: Array => { + if array.is_empty() { + bail!("array must contain at least one marker"); + } + Self::Content(array.into_iter().map(Value::display).collect()) + }, + v: Func => Self::Func(v), +} + +/// A list, enum, or term list. +pub trait ListLike: NativeElement { + /// The kind of list item this list is composed of. + type Item: ListItemLike; + + /// Create this kind of list from its children and tightness. + fn create(children: Vec<Packed<Self::Item>>, tight: bool) -> Self; +} + +/// A list item, enum item, or term list item. +pub trait ListItemLike: NativeElement { + /// Apply styles to the element's body. + fn styled(item: Packed<Self>, styles: Styles) -> Packed<Self>; +} + +impl ListLike for ListElem { + type Item = ListItem; + + fn create(children: Vec<Packed<Self::Item>>, tight: bool) -> Self { + Self::new(children).with_tight(tight) + } +} + +impl ListItemLike for ListItem { + fn styled(mut item: Packed<Self>, styles: Styles) -> Packed<Self> { + item.body.style_in_place(styles); + item + } +} diff --git a/crates/typst-library/src/model/mod.rs b/crates/typst-library/src/model/mod.rs new file mode 100644 index 00000000..7dad51c3 --- /dev/null +++ b/crates/typst-library/src/model/mod.rs @@ -0,0 +1,75 @@ +//! Structuring elements that define the document model. + +mod bibliography; +mod cite; +mod document; +mod emph; +#[path = "enum.rs"] +mod enum_; +mod figure; +mod footnote; +mod heading; +mod link; +mod list; +#[path = "numbering.rs"] +mod numbering_; +mod outline; +mod par; +mod quote; +mod reference; +mod strong; +mod table; +mod terms; + +pub use self::bibliography::*; +pub use self::cite::*; +pub use self::document::*; +pub use self::emph::*; +pub use self::enum_::*; +pub use self::figure::*; +pub use self::footnote::*; +pub use self::heading::*; +pub use self::link::*; +pub use self::list::*; +pub use self::numbering_::*; +pub use self::outline::*; +pub use self::par::*; +pub use self::quote::*; +pub use self::reference::*; +pub use self::strong::*; +pub use self::table::*; +pub use self::terms::*; + +use crate::foundations::{category, Category, Scope}; + +/// Document structuring. +/// +/// Here, you can find functions to structure your document and interact with +/// that structure. This includes section headings, figures, bibliography +/// management, cross-referencing and more. +#[category] +pub static MODEL: Category; + +/// Hook up all `model` definitions. +pub fn define(global: &mut Scope) { + global.category(MODEL); + global.define_elem::<DocumentElem>(); + global.define_elem::<RefElem>(); + global.define_elem::<LinkElem>(); + global.define_elem::<OutlineElem>(); + global.define_elem::<HeadingElem>(); + global.define_elem::<FigureElem>(); + global.define_elem::<FootnoteElem>(); + global.define_elem::<QuoteElem>(); + global.define_elem::<CiteElem>(); + global.define_elem::<BibliographyElem>(); + global.define_elem::<EnumElem>(); + global.define_elem::<ListElem>(); + global.define_elem::<ParbreakElem>(); + global.define_elem::<ParElem>(); + global.define_elem::<TableElem>(); + global.define_elem::<TermsElem>(); + global.define_elem::<EmphElem>(); + global.define_elem::<StrongElem>(); + global.define_func::<numbering>(); +} diff --git a/crates/typst-library/src/model/numbering.rs b/crates/typst-library/src/model/numbering.rs new file mode 100644 index 00000000..f0aa06e5 --- /dev/null +++ b/crates/typst-library/src/model/numbering.rs @@ -0,0 +1,658 @@ +use std::str::FromStr; + +use chinese_number::{ + from_usize_to_chinese_ten_thousand as usize_to_chinese, ChineseCase, ChineseVariant, +}; +use comemo::Tracked; +use ecow::{eco_format, EcoString, EcoVec}; + +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{cast, func, Context, Func, Str, Value}; +use crate::text::Case; + +/// Applies a numbering to a sequence of numbers. +/// +/// A numbering defines how a sequence of numbers should be displayed as +/// content. It is defined either through a pattern string or an arbitrary +/// function. +/// +/// A numbering pattern consists of counting symbols, for which the actual +/// number is substituted, their prefixes, and one suffix. The prefixes and the +/// suffix are repeated as-is. +/// +/// # Example +/// ```example +/// #numbering("1.1)", 1, 2, 3) \ +/// #numbering("1.a.i", 1, 2) \ +/// #numbering("I – 1", 12, 2) \ +/// #numbering( +/// (..nums) => nums +/// .pos() +/// .map(str) +/// .join(".") + ")", +/// 1, 2, 3, +/// ) +/// ``` +/// +/// # Numbering patterns and numbering functions +/// There are multiple instances where you can provide a numbering pattern or +/// function in Typst. For example, when defining how to number +/// [headings]($heading) or [figures]($figure). Every time, the expected format +/// is the same as the one described below for the +/// [`numbering`]($numbering.numbering) parameter. +/// +/// The following example illustrates that a numbering function is just a +/// regular [function] that accepts numbers and returns [`content`]. +/// ```example +/// #let unary(.., last) = "|" * last +/// #set heading(numbering: unary) +/// = First heading +/// = Second heading +/// = Third heading +/// ``` +#[func] +pub fn numbering( + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked<Context>, + /// Defines how the numbering works. + /// + /// **Counting symbols** are `1`, `a`, `A`, `i`, `I`, `一`, `壹`, `あ`, `い`, + /// `ア`, `イ`, `א`, `가`, `ㄱ`, `*`, `①`, and `⓵`. They are replaced by the + /// number in the sequence, preserving the original case. + /// + /// The `*` character means that symbols should be used to count, in the + /// order of `*`, `†`, `‡`, `§`, `¶`, `‖`. If there are more than six + /// items, the number is represented using repeated symbols. + /// + /// **Suffixes** are all characters after the last counting symbol. They are + /// repeated as-is at the end of any rendered number. + /// + /// **Prefixes** are all characters that are neither counting symbols nor + /// suffixes. They are repeated as-is at in front of their rendered + /// equivalent of their counting symbol. + /// + /// This parameter can also be an arbitrary function that gets each number + /// as an individual argument. When given a function, the `numbering` + /// function just forwards the arguments to that function. While this is not + /// particularly useful in itself, it means that you can just give arbitrary + /// numberings to the `numbering` function without caring whether they are + /// defined as a pattern or function. + numbering: Numbering, + /// The numbers to apply the numbering to. Must be positive. + /// + /// If `numbering` is a pattern and more numbers than counting symbols are + /// given, the last counting symbol with its prefix is repeated. + #[variadic] + numbers: Vec<usize>, +) -> SourceResult<Value> { + numbering.apply(engine, context, &numbers) +} + +/// How to number a sequence of things. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum Numbering { + /// A pattern with prefix, numbering, lower / upper case and suffix. + Pattern(NumberingPattern), + /// A closure mapping from an item's number to content. + Func(Func), +} + +impl Numbering { + /// Apply the pattern to the given numbers. + pub fn apply( + &self, + engine: &mut Engine, + context: Tracked<Context>, + numbers: &[usize], + ) -> SourceResult<Value> { + Ok(match self { + Self::Pattern(pattern) => Value::Str(pattern.apply(numbers).into()), + Self::Func(func) => func.call(engine, context, numbers.iter().copied())?, + }) + } + + /// Trim the prefix suffix if this is a pattern. + pub fn trimmed(mut self) -> Self { + if let Self::Pattern(pattern) = &mut self { + pattern.trimmed = true; + } + self + } +} + +impl From<NumberingPattern> for Numbering { + fn from(pattern: NumberingPattern) -> Self { + Self::Pattern(pattern) + } +} + +cast! { + Numbering, + self => match self { + Self::Pattern(pattern) => pattern.into_value(), + Self::Func(func) => func.into_value(), + }, + v: NumberingPattern => Self::Pattern(v), + v: Func => Self::Func(v), +} + +/// How to turn a number into text. +/// +/// A pattern consists of a prefix, followed by one of `1`, `a`, `A`, `i`, `I`, +/// `一`, `壹`, `あ`, `い`, `ア`, `イ`, `א`, `가`, `ㄱ`, `*`, `①`, or `⓵`, and then a +/// suffix. +/// +/// Examples of valid patterns: +/// - `1)` +/// - `a.` +/// - `(I)` +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct NumberingPattern { + pub pieces: EcoVec<(EcoString, NumberingKind)>, + pub suffix: EcoString, + trimmed: bool, +} + +impl NumberingPattern { + /// Apply the pattern to the given number. + pub fn apply(&self, numbers: &[usize]) -> EcoString { + let mut fmt = EcoString::new(); + let mut numbers = numbers.iter(); + + for (i, ((prefix, kind), &n)) in self.pieces.iter().zip(&mut numbers).enumerate() + { + if i > 0 || !self.trimmed { + fmt.push_str(prefix); + } + fmt.push_str(&kind.apply(n)); + } + + for ((prefix, kind), &n) in self.pieces.last().into_iter().cycle().zip(numbers) { + if prefix.is_empty() { + fmt.push_str(&self.suffix); + } else { + fmt.push_str(prefix); + } + fmt.push_str(&kind.apply(n)); + } + + if !self.trimmed { + fmt.push_str(&self.suffix); + } + + fmt + } + + /// Apply only the k-th segment of the pattern to a number. + pub fn apply_kth(&self, k: usize, number: usize) -> EcoString { + let mut fmt = EcoString::new(); + if let Some((prefix, _)) = self.pieces.first() { + fmt.push_str(prefix); + } + if let Some((_, kind)) = self + .pieces + .iter() + .chain(self.pieces.last().into_iter().cycle()) + .nth(k) + { + fmt.push_str(&kind.apply(number)); + } + fmt.push_str(&self.suffix); + fmt + } + + /// How many counting symbols this pattern has. + pub fn pieces(&self) -> usize { + self.pieces.len() + } +} + +impl FromStr for NumberingPattern { + type Err = &'static str; + + fn from_str(pattern: &str) -> Result<Self, Self::Err> { + let mut pieces = EcoVec::new(); + let mut handled = 0; + + for (i, c) in pattern.char_indices() { + let Some(kind) = NumberingKind::from_char(c) else { + continue; + }; + + let prefix = pattern[handled..i].into(); + pieces.push((prefix, kind)); + handled = c.len_utf8() + i; + } + + let suffix = pattern[handled..].into(); + if pieces.is_empty() { + return Err("invalid numbering pattern"); + } + + Ok(Self { pieces, suffix, trimmed: false }) + } +} + +cast! { + NumberingPattern, + self => { + let mut pat = EcoString::new(); + for (prefix, kind) in &self.pieces { + pat.push_str(prefix); + pat.push(kind.to_char()); + } + pat.push_str(&self.suffix); + pat.into_value() + }, + v: Str => v.parse()?, +} + +/// Different kinds of numberings. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum NumberingKind { + /// Arabic numerals (1, 2, 3, etc.). + Arabic, + /// Lowercase Latin letters (a, b, c, etc.). Items beyond z use base-26. + LowerLatin, + /// Uppercase Latin letters (A, B, C, etc.). Items beyond Z use base-26. + UpperLatin, + /// Lowercase Roman numerals (i, ii, iii, etc.). + LowerRoman, + /// Uppercase Roman numerals (I, II, III, etc.). + UpperRoman, + /// Paragraph/note-like symbols: *, †, ‡, §, ¶, and ‖. Further items use repeated symbols. + Symbol, + /// Hebrew numerals, including Geresh/Gershayim. + Hebrew, + /// Simplified Chinese standard numerals. This corresponds to the + /// `ChineseCase::Lower` variant. + LowerSimplifiedChinese, + /// Simplified Chinese "banknote" numerals. This corresponds to the + /// `ChineseCase::Upper` variant. + UpperSimplifiedChinese, + // TODO: Pick the numbering pattern based on languages choice. + // As the first character of Simplified and Traditional Chinese numbering + // are the same, we are unable to determine if the context requires + // Simplified or Traditional by only looking at this character. + #[allow(unused)] + /// Traditional Chinese standard numerals. This corresponds to the + /// `ChineseCase::Lower` variant. + LowerTraditionalChinese, + #[allow(unused)] + /// Traditional Chinese "banknote" numerals. This corresponds to the + /// `ChineseCase::Upper` variant. + UpperTraditionalChinese, + /// Hiragana in the gojūon order. Includes n but excludes wi and we. + HiraganaAiueo, + /// Hiragana in the iroha order. Includes wi and we but excludes n. + HiraganaIroha, + /// Katakana in the gojūon order. Includes n but excludes wi and we. + KatakanaAiueo, + /// Katakana in the iroha order. Includes wi and we but excludes n. + KatakanaIroha, + /// Korean jamo (ㄱ, ㄴ, ㄷ, etc.). + KoreanJamo, + /// Korean syllables (가, 나, 다, etc.). + KoreanSyllable, + /// Eastern Arabic numerals, used in some Arabic-speaking countries. + EasternArabic, + /// The variant of Eastern Arabic numerals used in Persian and Urdu. + EasternArabicPersian, + /// Devanagari numerals. + DevanagariNumber, + /// Bengali numerals. + BengaliNumber, + /// Bengali letters (ক, খ, গ, ...কক, কখ etc.). + BengaliLetter, + /// Circled numbers (①, ②, ③, etc.), up to 50. + CircledNumber, + /// Double-circled numbers (⓵, ⓶, ⓷, etc.), up to 10. + DoubleCircledNumber, +} + +impl NumberingKind { + /// Create a numbering kind from a representative character. + pub fn from_char(c: char) -> Option<Self> { + Some(match c { + '1' => NumberingKind::Arabic, + 'a' => NumberingKind::LowerLatin, + 'A' => NumberingKind::UpperLatin, + 'i' => NumberingKind::LowerRoman, + 'I' => NumberingKind::UpperRoman, + '*' => NumberingKind::Symbol, + 'א' => NumberingKind::Hebrew, + '一' => NumberingKind::LowerSimplifiedChinese, + '壹' => NumberingKind::UpperSimplifiedChinese, + 'あ' => NumberingKind::HiraganaAiueo, + 'い' => NumberingKind::HiraganaIroha, + 'ア' => NumberingKind::KatakanaAiueo, + 'イ' => NumberingKind::KatakanaIroha, + 'ㄱ' => NumberingKind::KoreanJamo, + '가' => NumberingKind::KoreanSyllable, + '\u{0661}' => NumberingKind::EasternArabic, + '\u{06F1}' => NumberingKind::EasternArabicPersian, + '\u{0967}' => NumberingKind::DevanagariNumber, + '\u{09E7}' => NumberingKind::BengaliNumber, + '\u{0995}' => NumberingKind::BengaliLetter, + '①' => NumberingKind::CircledNumber, + '⓵' => NumberingKind::DoubleCircledNumber, + _ => return None, + }) + } + + /// The representative character for this numbering kind. + pub fn to_char(self) -> char { + match self { + Self::Arabic => '1', + Self::LowerLatin => 'a', + Self::UpperLatin => 'A', + Self::LowerRoman => 'i', + Self::UpperRoman => 'I', + Self::Symbol => '*', + Self::Hebrew => 'א', + Self::LowerSimplifiedChinese | Self::LowerTraditionalChinese => '一', + Self::UpperSimplifiedChinese | Self::UpperTraditionalChinese => '壹', + Self::HiraganaAiueo => 'あ', + Self::HiraganaIroha => 'い', + Self::KatakanaAiueo => 'ア', + Self::KatakanaIroha => 'イ', + Self::KoreanJamo => 'ㄱ', + Self::KoreanSyllable => '가', + Self::EasternArabic => '\u{0661}', + Self::EasternArabicPersian => '\u{06F1}', + Self::DevanagariNumber => '\u{0967}', + Self::BengaliNumber => '\u{09E7}', + Self::BengaliLetter => '\u{0995}', + Self::CircledNumber => '①', + Self::DoubleCircledNumber => '⓵', + } + } + + /// Apply the numbering to the given number. + pub fn apply(self, n: usize) -> EcoString { + match self { + Self::Arabic => eco_format!("{n}"), + Self::LowerRoman => roman_numeral(n, Case::Lower), + Self::UpperRoman => roman_numeral(n, Case::Upper), + Self::Symbol => { + if n == 0 { + return '-'.into(); + } + + const SYMBOLS: &[char] = &['*', '†', '‡', '§', '¶', '‖']; + let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()]; + let amount = ((n - 1) / SYMBOLS.len()) + 1; + std::iter::repeat(symbol).take(amount).collect() + } + Self::Hebrew => hebrew_numeral(n), + + Self::LowerLatin => zeroless( + [ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + ], + n, + ), + Self::UpperLatin => zeroless( + [ + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', + 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + ], + n, + ), + Self::HiraganaAiueo => zeroless( + [ + 'あ', 'い', 'う', 'え', 'お', 'か', 'き', 'く', 'け', 'こ', 'さ', + 'し', 'す', 'せ', 'そ', 'た', 'ち', 'つ', 'て', 'と', 'な', 'に', + 'ぬ', 'ね', 'の', 'は', 'ひ', 'ふ', 'へ', 'ほ', 'ま', 'み', 'む', + 'め', 'も', 'や', 'ゆ', 'よ', 'ら', 'り', 'る', 'れ', 'ろ', 'わ', + 'を', 'ん', + ], + n, + ), + Self::HiraganaIroha => zeroless( + [ + 'い', 'ろ', 'は', 'に', 'ほ', 'へ', 'と', 'ち', 'り', 'ぬ', 'る', + 'を', 'わ', 'か', 'よ', 'た', 'れ', 'そ', 'つ', 'ね', 'な', 'ら', + 'む', 'う', 'ゐ', 'の', 'お', 'く', 'や', 'ま', 'け', 'ふ', 'こ', + 'え', 'て', 'あ', 'さ', 'き', 'ゆ', 'め', 'み', 'し', 'ゑ', 'ひ', + 'も', 'せ', 'す', + ], + n, + ), + Self::KatakanaAiueo => zeroless( + [ + 'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ', 'コ', 'サ', + 'シ', 'ス', 'セ', 'ソ', 'タ', 'チ', 'ツ', 'テ', 'ト', 'ナ', 'ニ', + 'ヌ', 'ネ', 'ノ', 'ハ', 'ヒ', 'フ', 'ヘ', 'ホ', 'マ', 'ミ', 'ム', + 'メ', 'モ', 'ヤ', 'ユ', 'ヨ', 'ラ', 'リ', 'ル', 'レ', 'ロ', 'ワ', + 'ヲ', 'ン', + ], + n, + ), + Self::KatakanaIroha => zeroless( + [ + 'イ', 'ロ', 'ハ', 'ニ', 'ホ', 'ヘ', 'ト', 'チ', 'リ', 'ヌ', 'ル', + 'ヲ', 'ワ', 'カ', 'ヨ', 'タ', 'レ', 'ソ', 'ツ', 'ネ', 'ナ', 'ラ', + 'ム', 'ウ', 'ヰ', 'ノ', 'オ', 'ク', 'ヤ', 'マ', 'ケ', 'フ', 'コ', + 'エ', 'テ', 'ア', 'サ', 'キ', 'ユ', 'メ', 'ミ', 'シ', 'ヱ', 'ヒ', + 'モ', 'セ', 'ス', + ], + n, + ), + Self::KoreanJamo => zeroless( + [ + 'ㄱ', 'ㄴ', 'ㄷ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅅ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', + 'ㅌ', 'ㅍ', 'ㅎ', + ], + n, + ), + Self::KoreanSyllable => zeroless( + [ + '가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카', + '타', '파', '하', + ], + n, + ), + Self::BengaliLetter => zeroless( + [ + 'ক', 'খ', 'গ', 'ঘ', 'ঙ', 'চ', 'ছ', 'জ', 'ঝ', 'ঞ', 'ট', 'ঠ', 'ড', 'ঢ', + 'ণ', 'ত', 'থ', 'দ', 'ধ', 'ন', 'প', 'ফ', 'ব', 'ভ', 'ম', 'য', 'র', 'ল', + 'শ', 'ষ', 'স', 'হ', + ], + n, + ), + Self::CircledNumber => zeroless( + [ + '①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩', '⑪', '⑫', '⑬', '⑭', + '⑮', '⑯', '⑰', '⑱', '⑲', '⑳', '㉑', '㉒', '㉓', '㉔', '㉕', '㉖', + '㉗', '㉘', '㉙', '㉚', '㉛', '㉜', '㉝', '㉞', '㉟', '㊱', '㊲', + '㊳', '㊴', '㊵', '㊶', '㊷', '㊸', '㊹', '㊺', '㊻', '㊼', '㊽', + '㊾', '㊿', + ], + n, + ), + Self::DoubleCircledNumber => { + zeroless(['⓵', '⓶', '⓷', '⓸', '⓹', '⓺', '⓻', '⓼', '⓽', '⓾'], n) + } + + Self::LowerSimplifiedChinese => { + usize_to_chinese(ChineseVariant::Simple, ChineseCase::Lower, n).into() + } + Self::UpperSimplifiedChinese => { + usize_to_chinese(ChineseVariant::Simple, ChineseCase::Upper, n).into() + } + Self::LowerTraditionalChinese => { + usize_to_chinese(ChineseVariant::Traditional, ChineseCase::Lower, n) + .into() + } + Self::UpperTraditionalChinese => { + usize_to_chinese(ChineseVariant::Traditional, ChineseCase::Upper, n) + .into() + } + + Self::EasternArabic => decimal('\u{0660}', n), + Self::EasternArabicPersian => decimal('\u{06F0}', n), + Self::DevanagariNumber => decimal('\u{0966}', n), + Self::BengaliNumber => decimal('\u{09E6}', n), + } + } +} + +fn hebrew_numeral(mut n: usize) -> EcoString { + if n == 0 { + return '-'.into(); + } + let mut fmt = EcoString::new(); + 'outer: for (name, value) in [ + ('ת', 400), + ('ש', 300), + ('ר', 200), + ('ק', 100), + ('צ', 90), + ('פ', 80), + ('ע', 70), + ('ס', 60), + ('נ', 50), + ('מ', 40), + ('ל', 30), + ('כ', 20), + ('י', 10), + ('ט', 9), + ('ח', 8), + ('ז', 7), + ('ו', 6), + ('ה', 5), + ('ד', 4), + ('ג', 3), + ('ב', 2), + ('א', 1), + ] { + while n >= value { + match n { + 15 => fmt.push_str("ט״ו"), + 16 => fmt.push_str("ט״ז"), + _ => { + let append_geresh = n == value && fmt.is_empty(); + if n == value && !fmt.is_empty() { + fmt.push('״'); + } + fmt.push(name); + if append_geresh { + fmt.push('׳'); + } + + n -= value; + continue; + } + } + break 'outer; + } + } + fmt +} + +fn roman_numeral(mut n: usize, case: Case) -> EcoString { + if n == 0 { + return match case { + Case::Lower => 'n'.into(), + Case::Upper => 'N'.into(), + }; + } + + // Adapted from Yann Villessuzanne's roman.rs under the + // Unlicense, at https://github.com/linfir/roman.rs/ + let mut fmt = EcoString::new(); + for &(name, value) in &[ + ("M̅", 1000000), + ("D̅", 500000), + ("C̅", 100000), + ("L̅", 50000), + ("X̅", 10000), + ("V̅", 5000), + ("I̅V̅", 4000), + ("M", 1000), + ("CM", 900), + ("D", 500), + ("CD", 400), + ("C", 100), + ("XC", 90), + ("L", 50), + ("XL", 40), + ("X", 10), + ("IX", 9), + ("V", 5), + ("IV", 4), + ("I", 1), + ] { + while n >= value { + n -= value; + for c in name.chars() { + match case { + Case::Lower => fmt.extend(c.to_lowercase()), + Case::Upper => fmt.push(c), + } + } + } + } + + fmt +} + +/// Stringify a number using a base-N counting system with no zero digit. +/// +/// This is best explained by example. Suppose our digits are 'A', 'B', and 'C'. +/// We would get the following: +/// +/// ```text +/// 1 => "A" +/// 2 => "B" +/// 3 => "C" +/// 4 => "AA" +/// 5 => "AB" +/// 6 => "AC" +/// 7 => "BA" +/// 8 => "BB" +/// 9 => "BC" +/// 10 => "CA" +/// 11 => "CB" +/// 12 => "CC" +/// 13 => "AAA" +/// etc. +/// ``` +/// +/// You might be familiar with this scheme from the way spreadsheet software +/// tends to label its columns. +fn zeroless<const N_DIGITS: usize>( + alphabet: [char; N_DIGITS], + mut n: usize, +) -> EcoString { + if n == 0 { + return '-'.into(); + } + let mut cs = EcoString::new(); + while n > 0 { + n -= 1; + cs.push(alphabet[n % N_DIGITS]); + n /= N_DIGITS; + } + cs.chars().rev().collect() +} + +/// Stringify a number using a base-10 counting system with a zero digit. +/// +/// This function assumes that the digits occupy contiguous codepoints. +fn decimal(start: char, mut n: usize) -> EcoString { + if n == 0 { + return start.into(); + } + let mut cs = EcoString::new(); + while n > 0 { + cs.push(char::from_u32((start as u32) + ((n % 10) as u32)).unwrap()); + n /= 10; + } + cs.chars().rev().collect() +} diff --git a/crates/typst-library/src/model/outline.rs b/crates/typst-library/src/model/outline.rs new file mode 100644 index 00000000..85257c2c --- /dev/null +++ b/crates/typst-library/src/model/outline.rs @@ -0,0 +1,547 @@ +use std::num::NonZeroUsize; +use std::str::FromStr; + +use comemo::Track; +use typst_syntax::Span; +use typst_utils::NonZeroExt; + +use crate::diag::{bail, At, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, scope, select_where, Content, Context, Func, LocatableSelector, + NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles, +}; +use crate::introspection::{Counter, CounterKey, Locatable}; +use crate::layout::{ + BoxElem, Dir, Em, Fr, HElem, HideElem, Length, Rel, RepeatElem, Spacing, +}; +use crate::model::{ + Destination, HeadingElem, NumberingPattern, ParElem, ParbreakElem, Refable, +}; +use crate::text::{LinebreakElem, LocalName, SpaceElem, TextElem}; + +/// A table of contents, figures, or other elements. +/// +/// This function generates a list of all occurrences of an element in the +/// document, up to a given depth. The element's numbering and page number will +/// be displayed in the outline alongside its title or caption. By default this +/// generates a table of contents. +/// +/// # Example +/// ```example +/// #outline() +/// +/// = Introduction +/// #lorem(5) +/// +/// = Prior work +/// #lorem(10) +/// ``` +/// +/// # Alternative outlines +/// By setting the `target` parameter, the outline can be used to generate a +/// list of other kinds of elements than headings. In the example below, we list +/// all figures containing images by setting `target` to `{figure.where(kind: +/// image)}`. We could have also set it to just `figure`, but then the list +/// would also include figures containing tables or other material. For more +/// details on the `where` selector, [see here]($function.where). +/// +/// ```example +/// #outline( +/// title: [List of Figures], +/// target: figure.where(kind: image), +/// ) +/// +/// #figure( +/// image("tiger.jpg"), +/// caption: [A nice figure!], +/// ) +/// ``` +/// +/// # Styling the outline +/// The outline element has several options for customization, such as its +/// `title` and `indent` parameters. If desired, however, it is possible to have +/// more control over the outline's look and style through the +/// [`outline.entry`]($outline.entry) element. +#[elem(scope, keywords = ["Table of Contents"], Show, ShowSet, LocalName)] +pub struct OutlineElem { + /// The title of the outline. + /// + /// - When set to `{auto}`, an appropriate title for the + /// [text language]($text.lang) will be used. This is the default. + /// - When set to `{none}`, the outline will not have a title. + /// - A custom title can be set by passing content. + /// + /// The outline's heading will not be numbered by default, but you can + /// force it to be with a show-set rule: + /// `{show outline: set heading(numbering: "1.")}` + pub title: Smart<Option<Content>>, + + /// The type of element to include in the outline. + /// + /// To list figures containing a specific kind of element, like a table, you + /// can write `{figure.where(kind: table)}`. + /// + /// ```example + /// #outline( + /// title: [List of Tables], + /// target: figure.where(kind: table), + /// ) + /// + /// #figure( + /// table( + /// columns: 4, + /// [t], [1], [2], [3], + /// [y], [0.3], [0.7], [0.5], + /// ), + /// caption: [Experiment results], + /// ) + /// ``` + #[default(LocatableSelector(select_where!(HeadingElem, Outlined => true)))] + #[borrowed] + pub target: LocatableSelector, + + /// The maximum level up to which elements are included in the outline. When + /// this argument is `{none}`, all elements are included. + /// + /// ```example + /// #set heading(numbering: "1.") + /// #outline(depth: 2) + /// + /// = Yes + /// Top-level section. + /// + /// == Still + /// Subsection. + /// + /// === Nope + /// Not included. + /// ``` + pub depth: Option<NonZeroUsize>, + + /// How to indent the outline's entries. + /// + /// - `{none}`: No indent + /// - `{auto}`: Indents the numbering of the nested entry with the title of + /// its parent entry. This only has an effect if the entries are numbered + /// (e.g., via [heading numbering]($heading.numbering)). + /// - [Relative length]($relative): Indents the item by this length + /// multiplied by its nesting level. Specifying `{2em}`, for instance, + /// would indent top-level headings (not nested) by `{0em}`, second level + /// headings by `{2em}` (nested once), third-level headings by `{4em}` + /// (nested twice) and so on. + /// - [Function]($function): You can completely customize this setting with + /// a function. That function receives the nesting level as a parameter + /// (starting at 0 for top-level headings/elements) and can return a + /// relative length or content making up the indent. For example, + /// `{n => n * 2em}` would be equivalent to just specifying `{2em}`, while + /// `{n => [→ ] * n}` would indent with one arrow per nesting level. + /// + /// *Migration hints:* Specifying `{true}` (equivalent to `{auto}`) or + /// `{false}` (equivalent to `{none}`) for this option is deprecated and + /// will be removed in a future release. + /// + /// ```example + /// #set heading(numbering: "1.a.") + /// + /// #outline( + /// title: [Contents (Automatic)], + /// indent: auto, + /// ) + /// + /// #outline( + /// title: [Contents (Length)], + /// indent: 2em, + /// ) + /// + /// #outline( + /// title: [Contents (Function)], + /// indent: n => [→ ] * n, + /// ) + /// + /// = About ACME Corp. + /// == History + /// === Origins + /// #lorem(10) + /// + /// == Products + /// #lorem(10) + /// ``` + #[default(None)] + #[borrowed] + pub indent: Option<Smart<OutlineIndent>>, + + /// Content to fill the space between the title and the page number. Can be + /// set to `{none}` to disable filling. + /// + /// ```example + /// #outline(fill: line(length: 100%)) + /// + /// = A New Beginning + /// ``` + #[default(Some(RepeatElem::new(TextElem::packed(".")).pack()))] + pub fill: Option<Content>, +} + +#[scope] +impl OutlineElem { + #[elem] + type OutlineEntry; +} + +impl Show for Packed<OutlineElem> { + #[typst_macros::time(name = "outline", span = self.span())] + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let mut seq = vec![ParbreakElem::shared().clone()]; + // Build the outline title. + if let Some(title) = self.title(styles).unwrap_or_else(|| { + Some(TextElem::packed(Self::local_name_in(styles)).spanned(self.span())) + }) { + seq.push( + HeadingElem::new(title) + .with_depth(NonZeroUsize::ONE) + .pack() + .spanned(self.span()), + ); + } + + let indent = self.indent(styles); + let depth = self.depth(styles).unwrap_or(NonZeroUsize::new(usize::MAX).unwrap()); + + let mut ancestors: Vec<&Content> = vec![]; + let elems = engine.introspector.query(&self.target(styles).0); + + for elem in &elems { + let Some(entry) = OutlineEntry::from_outlinable( + engine, + self.span(), + elem.clone(), + self.fill(styles), + styles, + )? + else { + continue; + }; + + let level = entry.level(); + if depth < *level { + continue; + } + + // Deals with the ancestors of the current element. + // This is only applicable for elements with a hierarchy/level. + while ancestors + .last() + .and_then(|ancestor| ancestor.with::<dyn Outlinable>()) + .is_some_and(|last| last.level() >= *level) + { + ancestors.pop(); + } + + OutlineIndent::apply( + indent, + engine, + &ancestors, + &mut seq, + styles, + self.span(), + )?; + + // Add the overridable outline entry, followed by a line break. + seq.push(entry.pack()); + seq.push(LinebreakElem::shared().clone()); + + ancestors.push(elem); + } + + seq.push(ParbreakElem::shared().clone()); + + Ok(Content::sequence(seq)) + } +} + +impl ShowSet for Packed<OutlineElem> { + fn show_set(&self, _: StyleChain) -> Styles { + let mut out = Styles::new(); + out.set(HeadingElem::set_outlined(false)); + out.set(HeadingElem::set_numbering(None)); + out.set(ParElem::set_first_line_indent(Em::new(0.0).into())); + out + } +} + +impl LocalName for Packed<OutlineElem> { + const KEY: &'static str = "outline"; +} + +/// Marks an element as being able to be outlined. This is used to implement the +/// `#outline()` element. +pub trait Outlinable: Refable { + /// Produce an outline item for this element. + fn outline( + &self, + engine: &mut Engine, + + styles: StyleChain, + ) -> SourceResult<Option<Content>>; + + /// Returns the nesting level of this element. + fn level(&self) -> NonZeroUsize { + NonZeroUsize::ONE + } +} + +/// Defines how an outline is indented. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum OutlineIndent { + Bool(bool), + Rel(Rel<Length>), + Func(Func), +} + +impl OutlineIndent { + fn apply( + indent: &Option<Smart<Self>>, + engine: &mut Engine, + ancestors: &Vec<&Content>, + seq: &mut Vec<Content>, + styles: StyleChain, + span: Span, + ) -> SourceResult<()> { + match indent { + // 'none' | 'false' => no indenting + None | Some(Smart::Custom(OutlineIndent::Bool(false))) => {} + + // 'auto' | 'true' => use numbering alignment for indenting + Some(Smart::Auto | Smart::Custom(OutlineIndent::Bool(true))) => { + // Add hidden ancestors numberings to realize the indent. + let mut hidden = Content::empty(); + for ancestor in ancestors { + let ancestor_outlinable = ancestor.with::<dyn Outlinable>().unwrap(); + + if let Some(numbering) = ancestor_outlinable.numbering() { + let numbers = ancestor_outlinable.counter().display_at_loc( + engine, + ancestor.location().unwrap(), + styles, + numbering, + )?; + + hidden += numbers + SpaceElem::shared().clone(); + }; + } + + if !ancestors.is_empty() { + seq.push(HideElem::new(hidden).pack()); + seq.push(SpaceElem::shared().clone()); + } + } + + // Length => indent with some fixed spacing per level + Some(Smart::Custom(OutlineIndent::Rel(length))) => { + seq.push( + HElem::new(Spacing::Rel(*length)).pack().repeat(ancestors.len()), + ); + } + + // Function => call function with the current depth and take + // the returned content + Some(Smart::Custom(OutlineIndent::Func(func))) => { + let depth = ancestors.len(); + let LengthOrContent(content) = func + .call(engine, Context::new(None, Some(styles)).track(), [depth])? + .cast() + .at(span)?; + if !content.is_empty() { + seq.push(content); + } + } + }; + + Ok(()) + } +} + +cast! { + OutlineIndent, + self => match self { + Self::Bool(v) => v.into_value(), + Self::Rel(v) => v.into_value(), + Self::Func(v) => v.into_value() + }, + v: bool => OutlineIndent::Bool(v), + v: Rel<Length> => OutlineIndent::Rel(v), + v: Func => OutlineIndent::Func(v), +} + +struct LengthOrContent(Content); + +cast! { + LengthOrContent, + v: Rel<Length> => Self(HElem::new(Spacing::Rel(v)).pack()), + v: Content => Self(v), +} + +/// Represents each entry line in an outline, including the reference to the +/// outlined element, its page number, and the filler content between both. +/// +/// This element is intended for use with show rules to control the appearance +/// of outlines. To customize an entry's line, you can build it from scratch by +/// accessing the `level`, `element`, `body`, `fill` and `page` fields on the +/// entry. +/// +/// ```example +/// #set heading(numbering: "1.") +/// +/// #show outline.entry.where( +/// level: 1 +/// ): it => { +/// v(12pt, weak: true) +/// strong(it) +/// } +/// +/// #outline(indent: auto) +/// +/// = Introduction +/// = Background +/// == History +/// == State of the Art +/// = Analysis +/// == Setup +/// ``` +#[elem(name = "entry", title = "Outline Entry", Show)] +pub struct OutlineEntry { + /// The nesting level of this outline entry. Starts at `{1}` for top-level + /// entries. + #[required] + pub level: NonZeroUsize, + + /// The element this entry refers to. Its location will be available + /// through the [`location`]($content.location) method on content + /// and can be [linked]($link) to. + #[required] + pub element: Content, + + /// The content which is displayed in place of the referred element at its + /// entry in the outline. For a heading, this would be its number followed + /// by the heading's title, for example. + #[required] + pub body: Content, + + /// The content used to fill the space between the element's outline and + /// its page number, as defined by the outline element this entry is + /// located in. When `{none}`, empty space is inserted in that gap instead. + /// + /// Note that, when using show rules to override outline entries, it is + /// recommended to wrap the filling content in a [`box`] with fractional + /// width. For example, `{box(width: 1fr, repeat[-])}` would show precisely + /// as many `-` characters as necessary to fill a particular gap. + #[required] + pub fill: Option<Content>, + + /// The page number of the element this entry links to, formatted with the + /// numbering set for the referenced page. + #[required] + pub page: Content, +} + +impl OutlineEntry { + /// Generates an OutlineEntry from the given element, if possible (errors if + /// the element does not implement `Outlinable`). If the element should not + /// be outlined (e.g. heading with 'outlined: false'), does not generate an + /// entry instance (returns `Ok(None)`). + fn from_outlinable( + engine: &mut Engine, + span: Span, + elem: Content, + fill: Option<Content>, + styles: StyleChain, + ) -> SourceResult<Option<Self>> { + let Some(outlinable) = elem.with::<dyn Outlinable>() else { + bail!(span, "cannot outline {}", elem.func().name()); + }; + + let Some(body) = outlinable.outline(engine, styles)? else { + return Ok(None); + }; + + let location = elem.location().unwrap(); + let page_numbering = engine + .introspector + .page_numbering(location) + .cloned() + .unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into()); + + let page = Counter::new(CounterKey::Page).display_at_loc( + engine, + location, + styles, + &page_numbering, + )?; + + Ok(Some(Self::new(outlinable.level(), elem, body, fill, page))) + } +} + +impl Show for Packed<OutlineEntry> { + #[typst_macros::time(name = "outline.entry", span = self.span())] + fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let mut seq = vec![]; + let elem = self.element(); + + // In case a user constructs an outline entry with an arbitrary element. + let Some(location) = elem.location() else { + if elem.can::<dyn Locatable>() && elem.can::<dyn Outlinable>() { + bail!( + self.span(), "{} must have a location", elem.func().name(); + hint: "try using a query or a show rule to customize the outline.entry instead", + ) + } else { + bail!(self.span(), "cannot outline {}", elem.func().name()) + } + }; + + // Isolate the entry body in RTL because the page number is typically + // LTR. I'm not sure whether LTR should conceptually also be isolated, + // but in any case we don't do it for now because the text shaping + // pipeline does tend to choke a bit on default ignorables (in + // particular the CJK-Latin spacing). + // + // See also: + // - https://github.com/typst/typst/issues/4476 + // - https://github.com/typst/typst/issues/5176 + let rtl = TextElem::dir_in(styles) == Dir::RTL; + if rtl { + // "Right-to-Left Embedding" + seq.push(TextElem::packed("\u{202B}")); + } + + seq.push(self.body().clone().linked(Destination::Location(location))); + + if rtl { + // "Pop Directional Formatting" + seq.push(TextElem::packed("\u{202C}")); + } + + // Add filler symbols between the section name and page number. + if let Some(filler) = self.fill() { + seq.push(SpaceElem::shared().clone()); + seq.push( + BoxElem::new() + .with_body(Some(filler.clone())) + .with_width(Fr::one().into()) + .pack() + .spanned(self.span()), + ); + seq.push(SpaceElem::shared().clone()); + } else { + seq.push(HElem::new(Fr::one().into()).pack()); + } + + // Add the page number. + let page = self.page().clone().linked(Destination::Location(location)); + seq.push(page); + + Ok(Content::sequence(seq)) + } +} diff --git a/crates/typst-library/src/model/par.rs b/crates/typst-library/src/model/par.rs new file mode 100644 index 00000000..a43499d8 --- /dev/null +++ b/crates/typst-library/src/model/par.rs @@ -0,0 +1,431 @@ +use std::fmt::{self, Debug, Formatter}; + +use typst_utils::singleton; + +use crate::diag::{bail, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + elem, scope, Args, Cast, Construct, Content, NativeElement, Packed, Set, Smart, + StyleVec, Unlabellable, +}; +use crate::introspection::{Count, CounterUpdate, Locatable}; +use crate::layout::{Em, HAlignment, Length, OuterHAlignment}; +use crate::model::Numbering; + +/// Arranges text, spacing and inline-level elements into a paragraph. +/// +/// Although this function is primarily used in set rules to affect paragraph +/// properties, it can also be used to explicitly render its argument onto a +/// paragraph of its own. +/// +/// # Example +/// ```example +/// #set par( +/// first-line-indent: 1em, +/// spacing: 0.65em, +/// justify: true, +/// ) +/// +/// We proceed by contradiction. +/// Suppose that there exists a set +/// of positive integers $a$, $b$, and +/// $c$ that satisfies the equation +/// $a^n + b^n = c^n$ for some +/// integer value of $n > 2$. +/// +/// Without loss of generality, +/// let $a$ be the smallest of the +/// three integers. Then, we ... +/// ``` +#[elem(scope, title = "Paragraph", Debug, Construct)] +pub struct ParElem { + /// The spacing between lines. + /// + /// Leading defines the spacing between the [bottom edge]($text.bottom-edge) + /// of one line and the [top edge]($text.top-edge) of the following line. By + /// default, these two properties are up to the font, but they can also be + /// configured manually with a text set rule. + /// + /// By setting top edge, bottom edge, and leading, you can also configure a + /// consistent baseline-to-baseline distance. You could, for instance, set + /// the leading to `{1em}`, the top-edge to `{0.8em}`, and the bottom-edge + /// to `{-0.2em}` to get a baseline gap of exactly `{2em}`. The exact + /// distribution of the top- and bottom-edge values affects the bounds of + /// the first and last line. + #[resolve] + #[ghost] + #[default(Em::new(0.65).into())] + pub leading: Length, + + /// The spacing between paragraphs. + /// + /// Just like leading, this defines the spacing between the bottom edge of a + /// paragraph's last line and the top edge of the next paragraph's first + /// line. + /// + /// When a paragraph is adjacent to a [`block`] that is not a paragraph, + /// that block's [`above`]($block.above) or [`below`]($block.below) property + /// takes precedence over the paragraph spacing. Headings, for instance, + /// reduce the spacing below them by default for a better look. + #[resolve] + #[ghost] + #[default(Em::new(1.2).into())] + pub spacing: Length, + + /// Whether to justify text in its line. + /// + /// Hyphenation will be enabled for justified paragraphs if the + /// [text function's `hyphenate` property]($text.hyphenate) is set to + /// `{auto}` and the current language is known. + /// + /// Note that the current [alignment]($align.alignment) still has an effect + /// on the placement of the last line except if it ends with a + /// [justified line break]($linebreak.justify). + #[ghost] + #[default(false)] + pub justify: bool, + + /// How to determine line breaks. + /// + /// When this property is set to `{auto}`, its default value, optimized line + /// breaks will be used for justified paragraphs. Enabling optimized line + /// breaks for ragged paragraphs may also be worthwhile to improve the + /// appearance of the text. + /// + /// ```example + /// #set page(width: 207pt) + /// #set par(linebreaks: "simple") + /// Some texts feature many longer + /// words. Those are often exceedingly + /// challenging to break in a visually + /// pleasing way. + /// + /// #set par(linebreaks: "optimized") + /// Some texts feature many longer + /// words. Those are often exceedingly + /// challenging to break in a visually + /// pleasing way. + /// ``` + #[ghost] + pub linebreaks: Smart<Linebreaks>, + + /// The indent the first line of a paragraph should have. + /// + /// Only the first line of a consecutive paragraph will be indented (not + /// the first one in a block or on the page). + /// + /// By typographic convention, paragraph breaks are indicated either by some + /// space between paragraphs or by indented first lines. Consider reducing + /// the [paragraph spacing]($block.spacing) to the [`leading`]($par.leading) + /// when using this property (e.g. using `[#set par(spacing: 0.65em)]`). + #[ghost] + pub first_line_indent: Length, + + /// The indent all but the first line of a paragraph should have. + #[ghost] + #[resolve] + pub hanging_indent: Length, + + /// Indicates whether an overflowing line should be shrunk. + /// + /// This property is set to `false` on raw blocks, because shrinking a line + /// could visually break the indentation. + #[ghost] + #[internal] + #[default(true)] + pub shrink: bool, + + /// The contents of the paragraph. + #[external] + #[required] + pub body: Content, + + /// The paragraph's children. + #[internal] + #[variadic] + pub children: StyleVec, +} + +#[scope] +impl ParElem { + #[elem] + type ParLine; +} + +impl Construct for ParElem { + fn construct(engine: &mut Engine, args: &mut Args) -> SourceResult<Content> { + // The paragraph constructor is special: It doesn't create a paragraph + // element. Instead, it just ensures that the passed content lives in a + // separate paragraph and styles it. + let styles = Self::set(engine, args)?; + let body = args.expect::<Content>("body")?; + Ok(Content::sequence([ + ParbreakElem::shared().clone(), + body.styled_with_map(styles), + ParbreakElem::shared().clone(), + ])) + } +} + +impl Debug for ParElem { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Par ")?; + self.children.fmt(f) + } +} + +/// How to determine line breaks in a paragraph. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum Linebreaks { + /// Determine the line breaks in a simple first-fit style. + Simple, + /// Optimize the line breaks for the whole paragraph. + /// + /// Typst will try to produce more evenly filled lines of text by + /// considering the whole paragraph when calculating line breaks. + Optimized, +} + +/// A paragraph break. +/// +/// This starts a new paragraph. Especially useful when used within code like +/// [for loops]($scripting/#loops). Multiple consecutive +/// paragraph breaks collapse into a single one. +/// +/// # Example +/// ```example +/// #for i in range(3) { +/// [Blind text #i: ] +/// lorem(5) +/// parbreak() +/// } +/// ``` +/// +/// # Syntax +/// Instead of calling this function, you can insert a blank line into your +/// markup to create a paragraph break. +#[elem(title = "Paragraph Break", Unlabellable)] +pub struct ParbreakElem {} + +impl ParbreakElem { + /// Get the globally shared paragraph element. + pub fn shared() -> &'static Content { + singleton!(Content, ParbreakElem::new().pack()) + } +} + +impl Unlabellable for Packed<ParbreakElem> {} + +/// A paragraph line. +/// +/// This element is exclusively used for line number configuration through set +/// rules and cannot be placed. +/// +/// The [`numbering`]($par.line.numbering) option is used to enable line +/// numbers by specifying a numbering format. +/// +/// ```example +/// >>> #set page(margin: (left: 3em)) +/// #set par.line(numbering: "1") +/// +/// Roses are red. \ +/// Violets are blue. \ +/// Typst is there for you. +/// ``` +/// +/// The `numbering` option takes either a predefined +/// [numbering pattern]($numbering) or a function returning styled content. You +/// can disable line numbers for text inside certain elements by setting the +/// numbering to `{none}` using show-set rules. +/// +/// ```example +/// >>> #set page(margin: (left: 3em)) +/// // Styled red line numbers. +/// #set par.line( +/// numbering: n => text(red)[#n] +/// ) +/// +/// // Disable numbers inside figures. +/// #show figure: set par.line( +/// numbering: none +/// ) +/// +/// Roses are red. \ +/// Violets are blue. +/// +/// #figure( +/// caption: [Without line numbers.] +/// )[ +/// Lorem ipsum \ +/// dolor sit amet +/// ] +/// +/// The text above is a sample \ +/// originating from distant times. +/// ``` +/// +/// This element exposes further options which may be used to control other +/// aspects of line numbering, such as its [alignment]($par.line.number-align) +/// or [margin]($par.line.number-margin). In addition, you can control whether +/// the numbering is reset on each page through the +/// [`numbering-scope`]($par.line.numbering-scope) option. +#[elem(name = "line", title = "Paragraph Line", keywords = ["line numbering"], Construct, Locatable)] +pub struct ParLine { + /// How to number each line. Accepts a + /// [numbering pattern or function]($numbering). + /// + /// ```example + /// >>> #set page(margin: (left: 3em)) + /// #set par.line(numbering: "I") + /// + /// Roses are red. \ + /// Violets are blue. \ + /// Typst is there for you. + /// ``` + #[ghost] + pub numbering: Option<Numbering>, + + /// The alignment of line numbers associated with each line. + /// + /// The default of `{auto}` indicates a smart default where numbers grow + /// horizontally away from the text, considering the margin they're in and + /// the current text direction. + /// + /// ```example + /// >>> #set page(margin: (left: 3em)) + /// #set par.line( + /// numbering: "I", + /// number-align: left, + /// ) + /// + /// Hello world! \ + /// Today is a beautiful day \ + /// For exploring the world. + /// ``` + #[ghost] + pub number_align: Smart<HAlignment>, + + /// The margin at which line numbers appear. + /// + /// _Note:_ In a multi-column document, the line numbers for paragraphs + /// inside the last column will always appear on the `{end}` margin (right + /// margin for left-to-right text and left margin for right-to-left), + /// regardless of this configuration. That behavior cannot be changed at + /// this moment. + /// + /// ```example + /// >>> #set page(margin: (right: 3em)) + /// #set par.line( + /// numbering: "1", + /// number-margin: right, + /// ) + /// + /// = Report + /// - Brightness: Dark, yet darker + /// - Readings: Negative + /// ``` + #[ghost] + #[default(OuterHAlignment::Start)] + pub number_margin: OuterHAlignment, + + /// The distance between line numbers and text. + /// + /// The default value of `{auto}` results in a clearance that is adaptive to + /// the page width and yields reasonable results in most cases. + /// + /// ```example + /// >>> #set page(margin: (left: 3em)) + /// #set par.line( + /// numbering: "1", + /// number-clearance: 4pt, + /// ) + /// + /// Typesetting \ + /// Styling \ + /// Layout + /// ``` + #[ghost] + #[default] + pub number_clearance: Smart<Length>, + + /// Controls when to reset line numbering. + /// + /// _Note:_ The line numbering scope must be uniform across each page run (a + /// page run is a sequence of pages without an explicit pagebreak in + /// between). For this reason, set rules for it should be defined before any + /// page content, typically at the very start of the document. + /// + /// ```example + /// >>> #set page(margin: (left: 3em)) + /// #set par.line( + /// numbering: "1", + /// numbering-scope: "page", + /// ) + /// + /// First line \ + /// Second line + /// #pagebreak() + /// First line again \ + /// Second line again + /// ``` + #[ghost] + #[default(LineNumberingScope::Document)] + pub numbering_scope: LineNumberingScope, +} + +impl Construct for ParLine { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> { + bail!(args.span, "cannot be constructed manually"); + } +} + +/// Possible line numbering scope options, indicating how often the line number +/// counter should be reset. +/// +/// Note that, currently, manually resetting the line number counter is not +/// supported. +#[derive(Debug, Cast, Clone, Copy, PartialEq, Eq, Hash)] +pub enum LineNumberingScope { + /// Indicates that the line number counter spans the whole document, i.e., + /// it's never automatically reset. + Document, + /// Indicates that the line number counter should be reset at the start of + /// every new page. + Page, +} + +/// A marker used to indicate the presence of a line. +/// +/// This element is added to each line in a paragraph and later searched to +/// find out where to add line numbers. +#[elem(Construct, Locatable, Count)] +pub struct ParLineMarker { + #[internal] + #[required] + pub numbering: Numbering, + + #[internal] + #[required] + pub number_align: Smart<HAlignment>, + + #[internal] + #[required] + pub number_margin: OuterHAlignment, + + #[internal] + #[required] + pub number_clearance: Smart<Length>, +} + +impl Construct for ParLineMarker { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> { + bail!(args.span, "cannot be constructed manually"); + } +} + +impl Count for Packed<ParLineMarker> { + fn update(&self) -> Option<CounterUpdate> { + // The line counter must be updated manually by the root flow. + None + } +} diff --git a/crates/typst-library/src/model/quote.rs b/crates/typst-library/src/model/quote.rs new file mode 100644 index 00000000..110825f1 --- /dev/null +++ b/crates/typst-library/src/model/quote.rs @@ -0,0 +1,237 @@ +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, Content, Depth, Label, NativeElement, Packed, Show, ShowSet, Smart, + StyleChain, Styles, +}; +use crate::introspection::Locatable; +use crate::layout::{ + Alignment, BlockBody, BlockElem, Em, HElem, PadElem, Spacing, VElem, +}; +use crate::model::{CitationForm, CiteElem}; +use crate::text::{SmartQuoteElem, SmartQuotes, SpaceElem, TextElem}; + +/// Displays a quote alongside an optional attribution. +/// +/// # Example +/// ```example +/// Plato is often misquoted as the author of #quote[I know that I know +/// nothing], however, this is a derivation form his original quote: +/// +/// #set quote(block: true) +/// +/// #quote(attribution: [Plato])[ +/// ... ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι +/// ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι. +/// ] +/// #quote(attribution: [from the Henry Cary literal translation of 1897])[ +/// ... I seem, then, in just this little thing to be wiser than this man at +/// any rate, that what I do not know I do not think I know either. +/// ] +/// ``` +/// +/// By default block quotes are padded left and right by `{1em}`, alignment and +/// padding can be controlled with show rules: +/// ```example +/// #set quote(block: true) +/// #show quote: set align(center) +/// #show quote: set pad(x: 5em) +/// +/// #quote[ +/// You cannot pass... I am a servant of the Secret Fire, wielder of the +/// flame of Anor. You cannot pass. The dark fire will not avail you, +/// flame of Udûn. Go back to the Shadow! You cannot pass. +/// ] +/// ``` +#[elem(Locatable, ShowSet, Show)] +pub struct QuoteElem { + /// Whether this is a block quote. + /// + /// ```example + /// An inline citation would look like + /// this: #quote( + /// attribution: [René Descartes] + /// )[ + /// cogito, ergo sum + /// ], and a block equation like this: + /// #quote( + /// block: true, + /// attribution: [JFK] + /// )[ + /// Ich bin ein Berliner. + /// ] + /// ``` + block: bool, + + /// Whether double quotes should be added around this quote. + /// + /// The double quotes used are inferred from the `quotes` property on + /// [smartquote], which is affected by the `lang` property on [text]. + /// + /// - `{true}`: Wrap this quote in double quotes. + /// - `{false}`: Do not wrap this quote in double quotes. + /// - `{auto}`: Infer whether to wrap this quote in double quotes based on + /// the `block` property. If `block` is `{false}`, double quotes are + /// automatically added. + /// + /// ```example + /// #set text(lang: "de") + /// + /// Ein deutsch-sprechender Author + /// zitiert unter umständen JFK: + /// #quote[Ich bin ein Berliner.] + /// + /// #set text(lang: "en") + /// + /// And an english speaking one may + /// translate the quote: + /// #quote[I am a Berliner.] + /// ``` + quotes: Smart<bool>, + + /// The attribution of this quote, usually the author or source. Can be a + /// label pointing to a bibliography entry or any content. By default only + /// displayed for block quotes, but can be changed using a `{show}` rule. + /// + /// ```example + /// #quote(attribution: [René Descartes])[ + /// cogito, ergo sum + /// ] + /// + /// #show quote.where(block: false): it => { + /// ["] + h(0pt, weak: true) + it.body + h(0pt, weak: true) + ["] + /// if it.attribution != none [ (#it.attribution)] + /// } + /// + /// #quote( + /// attribution: link("https://typst.app/home")[typst.com] + /// )[ + /// Compose papers faster + /// ] + /// + /// #set quote(block: true) + /// + /// #quote(attribution: <tolkien54>)[ + /// You cannot pass... I am a servant + /// of the Secret Fire, wielder of the + /// flame of Anor. You cannot pass. The + /// dark fire will not avail you, flame + /// of Udûn. Go back to the Shadow! You + /// cannot pass. + /// ] + /// + /// #bibliography("works.bib", style: "apa") + /// ``` + #[borrowed] + attribution: Option<Attribution>, + + /// The quote. + #[required] + body: Content, + + /// The nesting depth. + #[internal] + #[fold] + #[ghost] + depth: Depth, +} + +/// Attribution for a [quote](QuoteElem). +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum Attribution { + Content(Content), + Label(Label), +} + +cast! { + Attribution, + self => match self { + Self::Content(content) => content.into_value(), + Self::Label(label) => label.into_value(), + }, + content: Content => Self::Content(content), + label: Label => Self::Label(label), +} + +impl Show for Packed<QuoteElem> { + #[typst_macros::time(name = "quote", span = self.span())] + fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let mut realized = self.body().clone(); + let block = self.block(styles); + + if self.quotes(styles) == Smart::Custom(true) || !block { + let quotes = SmartQuotes::get( + SmartQuoteElem::quotes_in(styles), + TextElem::lang_in(styles), + TextElem::region_in(styles), + SmartQuoteElem::alternative_in(styles), + ); + + // Alternate between single and double quotes. + let Depth(depth) = QuoteElem::depth_in(styles); + let double = depth % 2 == 0; + + // Add zero-width weak spacing to make the quotes "sticky". + let hole = HElem::hole().pack(); + realized = Content::sequence([ + TextElem::packed(quotes.open(double)), + hole.clone(), + realized, + hole, + TextElem::packed(quotes.close(double)), + ]) + .styled(QuoteElem::set_depth(Depth(1))); + } + + if block { + realized = BlockElem::new() + .with_body(Some(BlockBody::Content(realized))) + .pack() + .spanned(self.span()); + + if let Some(attribution) = self.attribution(styles).as_ref() { + let mut seq = vec![TextElem::packed('—'), SpaceElem::shared().clone()]; + + match attribution { + Attribution::Content(content) => { + seq.push(content.clone()); + } + Attribution::Label(label) => { + seq.push( + CiteElem::new(*label) + .with_form(Some(CitationForm::Prose)) + .pack() + .spanned(self.span()), + ); + } + } + + // Use v(0.9em, weak: true) bring the attribution closer to the + // quote. + let gap = Spacing::Rel(Em::new(0.9).into()); + let v = VElem::new(gap).with_weak(true).pack(); + realized += v + Content::sequence(seq).aligned(Alignment::END); + } + + realized = PadElem::new(realized).pack(); + } else if let Some(Attribution::Label(label)) = self.attribution(styles) { + realized += SpaceElem::shared().clone() + + CiteElem::new(*label).pack().spanned(self.span()); + } + + Ok(realized) + } +} + +impl ShowSet for Packed<QuoteElem> { + fn show_set(&self, styles: StyleChain) -> Styles { + let mut out = Styles::new(); + if self.block(styles) { + out.set(PadElem::set_left(Em::new(1.0).into())); + out.set(PadElem::set_right(Em::new(1.0).into())); + out.set(BlockElem::set_above(Smart::Custom(Em::new(2.4).into()))); + out.set(BlockElem::set_below(Smart::Custom(Em::new(1.8).into()))); + } + out + } +} diff --git a/crates/typst-library/src/model/reference.rs b/crates/typst-library/src/model/reference.rs new file mode 100644 index 00000000..bc1919d6 --- /dev/null +++ b/crates/typst-library/src/model/reference.rs @@ -0,0 +1,307 @@ +use comemo::Track; +use ecow::eco_format; + +use crate::diag::{bail, At, Hint, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, Content, Context, Func, IntoValue, Label, NativeElement, Packed, Show, + Smart, StyleChain, Synthesize, +}; +use crate::introspection::{Counter, Locatable}; +use crate::math::EquationElem; +use crate::model::{ + BibliographyElem, CiteElem, Destination, Figurable, FootnoteElem, Numbering, +}; +use crate::text::TextElem; + +/// A reference to a label or bibliography. +/// +/// Produces a textual reference to a label. For example, a reference to a +/// heading will yield an appropriate string such as "Section 1" for a reference +/// to the first heading. The references are also links to the respective +/// element. Reference syntax can also be used to [cite] from a bibliography. +/// +/// Referenceable elements include [headings]($heading), [figures]($figure), +/// [equations]($math.equation), and [footnotes]($footnote). To create a custom +/// referenceable element like a theorem, you can create a figure of a custom +/// [`kind`]($figure.kind) and write a show rule for it. In the future, there +/// might be a more direct way to define a custom referenceable element. +/// +/// If you just want to link to a labelled element and not get an automatic +/// textual reference, consider using the [`link`] function instead. +/// +/// # Example +/// ```example +/// #set heading(numbering: "1.") +/// #set math.equation(numbering: "(1)") +/// +/// = Introduction <intro> +/// Recent developments in +/// typesetting software have +/// rekindled hope in previously +/// frustrated researchers. @distress +/// As shown in @results, we ... +/// +/// = Results <results> +/// We discuss our approach in +/// comparison with others. +/// +/// == Performance <perf> +/// @slow demonstrates what slow +/// software looks like. +/// $ T(n) = O(2^n) $ <slow> +/// +/// #bibliography("works.bib") +/// ``` +/// +/// # Syntax +/// This function also has dedicated syntax: A reference to a label can be +/// created by typing an `@` followed by the name of the label (e.g. +/// `[= Introduction <intro>]` can be referenced by typing `[@intro]`). +/// +/// To customize the supplement, add content in square brackets after the +/// reference: `[@intro[Chapter]]`. +/// +/// # Customization +/// If you write a show rule for references, you can access the referenced +/// element through the `element` field of the reference. The `element` may +/// be `{none}` even if it exists if Typst hasn't discovered it yet, so you +/// always need to handle that case in your code. +/// +/// ```example +/// #set heading(numbering: "1.") +/// #set math.equation(numbering: "(1)") +/// +/// #show ref: it => { +/// let eq = math.equation +/// let el = it.element +/// if el != none and el.func() == eq { +/// // Override equation references. +/// link(el.location(),numbering( +/// el.numbering, +/// ..counter(eq).at(el.location()) +/// )) +/// } else { +/// // Other references as usual. +/// it +/// } +/// } +/// +/// = Beginnings <beginning> +/// In @beginning we prove @pythagoras. +/// $ a^2 + b^2 = c^2 $ <pythagoras> +/// ``` +#[elem(title = "Reference", Synthesize, Locatable, Show)] +pub struct RefElem { + /// The target label that should be referenced. + /// + /// Can be a label that is defined in the document or an entry from the + /// [`bibliography`]. + #[required] + pub target: Label, + + /// A supplement for the reference. + /// + /// For references to headings or figures, this is added before the + /// referenced number. For citations, this can be used to add a page number. + /// + /// If a function is specified, it is passed the referenced element and + /// should return content. + /// + /// ```example + /// #set heading(numbering: "1.") + /// #set ref(supplement: it => { + /// if it.func() == heading { + /// "Chapter" + /// } else { + /// "Thing" + /// } + /// }) + /// + /// = Introduction <intro> + /// In @intro, we see how to turn + /// Sections into Chapters. And + /// in @intro[Part], it is done + /// manually. + /// ``` + #[borrowed] + pub supplement: Smart<Option<Supplement>>, + + /// A synthesized citation. + #[synthesized] + pub citation: Option<Packed<CiteElem>>, + + /// The referenced element. + #[synthesized] + pub element: Option<Content>, +} + +impl Synthesize for Packed<RefElem> { + fn synthesize( + &mut self, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult<()> { + let citation = to_citation(self, engine, styles)?; + + let elem = self.as_mut(); + elem.push_citation(Some(citation)); + elem.push_element(None); + + let target = *elem.target(); + if !BibliographyElem::has(engine, target) { + if let Ok(found) = engine.introspector.query_label(target).cloned() { + elem.push_element(Some(found)); + return Ok(()); + } + } + + Ok(()) + } +} + +impl Show for Packed<RefElem> { + #[typst_macros::time(name = "ref", span = self.span())] + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let target = *self.target(); + let elem = engine.introspector.query_label(target); + let span = self.span(); + + if BibliographyElem::has(engine, target) { + if elem.is_ok() { + bail!(span, "label occurs in the document and its bibliography"); + } + + return Ok(to_citation(self, engine, styles)?.pack().spanned(span)); + } + + let elem = elem.at(span)?; + + if let Some(footnote) = elem.to_packed::<FootnoteElem>() { + return Ok(footnote.into_ref(target).pack().spanned(span)); + } + + let elem = elem.clone(); + let refable = elem + .with::<dyn Refable>() + .ok_or_else(|| { + if elem.can::<dyn Figurable>() { + eco_format!( + "cannot reference {} directly, try putting it into a figure", + elem.func().name() + ) + } else { + eco_format!("cannot reference {}", elem.func().name()) + } + }) + .at(span)?; + + let numbering = refable + .numbering() + .ok_or_else(|| { + eco_format!("cannot reference {} without numbering", elem.func().name()) + }) + .hint(eco_format!( + "you can enable {} numbering with `#set {}(numbering: \"1.\")`", + elem.func().name(), + if elem.func() == EquationElem::elem() { + "math.equation" + } else { + elem.func().name() + } + )) + .at(span)?; + + let loc = elem.location().unwrap(); + let numbers = refable.counter().display_at_loc( + engine, + loc, + styles, + &numbering.clone().trimmed(), + )?; + + let supplement = match self.supplement(styles).as_ref() { + Smart::Auto => refable.supplement(), + Smart::Custom(None) => Content::empty(), + Smart::Custom(Some(supplement)) => { + supplement.resolve(engine, styles, [elem])? + } + }; + + let mut content = numbers; + if !supplement.is_empty() { + content = supplement + TextElem::packed("\u{a0}") + content; + } + + Ok(content.linked(Destination::Location(loc))) + } +} + +/// Turn a reference into a citation. +fn to_citation( + reference: &Packed<RefElem>, + engine: &mut Engine, + styles: StyleChain, +) -> SourceResult<Packed<CiteElem>> { + let mut elem = Packed::new(CiteElem::new(*reference.target()).with_supplement( + match reference.supplement(styles).clone() { + Smart::Custom(Some(Supplement::Content(content))) => Some(content), + _ => None, + }, + )); + + if let Some(loc) = reference.location() { + elem.set_location(loc); + } + + elem.synthesize(engine, styles)?; + + Ok(elem) +} + +/// Additional content for a reference. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum Supplement { + Content(Content), + Func(Func), +} + +impl Supplement { + /// Tries to resolve the supplement into its content. + pub fn resolve<T: IntoValue>( + &self, + engine: &mut Engine, + styles: StyleChain, + args: impl IntoIterator<Item = T>, + ) -> SourceResult<Content> { + Ok(match self { + Supplement::Content(content) => content.clone(), + Supplement::Func(func) => func + .call(engine, Context::new(None, Some(styles)).track(), args)? + .display(), + }) + } +} + +cast! { + Supplement, + self => match self { + Self::Content(v) => v.into_value(), + Self::Func(v) => v.into_value(), + }, + v: Content => Self::Content(v), + v: Func => Self::Func(v), +} + +/// Marks an element as being able to be referenced. This is used to implement +/// the `@ref` element. +pub trait Refable { + /// The supplement, if not overridden by the reference. + fn supplement(&self) -> Content; + + /// Returns the counter of this element. + fn counter(&self) -> Counter; + + /// Returns the numbering of this element. + fn numbering(&self) -> Option<&Numbering>; +} diff --git a/crates/typst-library/src/model/strong.rs b/crates/typst-library/src/model/strong.rs new file mode 100644 index 00000000..0e23179e --- /dev/null +++ b/crates/typst-library/src/model/strong.rs @@ -0,0 +1,48 @@ +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{elem, Content, Packed, Show, StyleChain}; +use crate::text::{TextElem, WeightDelta}; + +/// Strongly emphasizes content by increasing the font weight. +/// +/// Increases the current font weight by a given `delta`. +/// +/// # Example +/// ```example +/// This is *strong.* \ +/// This is #strong[too.] \ +/// +/// #show strong: set text(red) +/// And this is *evermore.* +/// ``` +/// +/// # Syntax +/// This function also has dedicated syntax: To strongly emphasize content, +/// simply enclose it in stars/asterisks (`*`). Note that this only works at +/// word boundaries. To strongly emphasize part of a word, you have to use the +/// function. +#[elem(title = "Strong Emphasis", keywords = ["bold", "weight"], Show)] +pub struct StrongElem { + /// The delta to apply on the font weight. + /// + /// ```example + /// #set strong(delta: 0) + /// No *effect!* + /// ``` + #[default(300)] + pub delta: i64, + + /// The content to strongly emphasize. + #[required] + pub body: Content, +} + +impl Show for Packed<StrongElem> { + #[typst_macros::time(name = "strong", span = self.span())] + fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + Ok(self + .body() + .clone() + .styled(TextElem::set_delta(WeightDelta(self.delta(styles))))) + } +} diff --git a/crates/typst-library/src/model/table.rs b/crates/typst-library/src/model/table.rs new file mode 100644 index 00000000..7dfaf45d --- /dev/null +++ b/crates/typst-library/src/model/table.rs @@ -0,0 +1,724 @@ +use std::num::NonZeroUsize; +use std::sync::Arc; + +use typst_utils::NonZeroExt; + +use crate::diag::{bail, HintedStrResult, HintedString, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, scope, Content, NativeElement, Packed, Show, Smart, StyleChain, +}; +use crate::layout::{ + show_grid_cell, Abs, Alignment, BlockElem, Celled, GridCell, GridFooter, GridHLine, + GridHeader, GridVLine, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, + TrackSizings, +}; +use crate::model::Figurable; +use crate::text::LocalName; +use crate::visualize::{Paint, Stroke}; + +/// A table of items. +/// +/// Tables are used to arrange content in cells. Cells can contain arbitrary +/// content, including multiple paragraphs and are specified in row-major order. +/// For a hands-on explanation of all the ways you can use and customize tables +/// in Typst, check out the [table guide]($guides/table-guide). +/// +/// Because tables are just grids with different defaults for some cell +/// properties (notably `stroke` and `inset`), refer to the [grid +/// documentation]($grid) for more information on how to size the table tracks +/// and specify the cell appearance properties. +/// +/// If you are unsure whether you should be using a table or a grid, consider +/// whether the content you are arranging semantically belongs together as a set +/// of related data points or similar or whether you are just want to enhance +/// your presentation by arranging unrelated content in a grid. In the former +/// case, a table is the right choice, while in the latter case, a grid is more +/// appropriate. Furthermore, Typst will annotate its output in the future such +/// that screenreaders will announce content in `table` as tabular while a +/// grid's content will be announced no different than multiple content blocks +/// in the document flow. +/// +/// Note that, to override a particular cell's properties or apply show rules on +/// table cells, you can use the [`table.cell`]($table.cell) element. See its +/// documentation for more information. +/// +/// Although the `table` and the `grid` share most properties, set and show +/// rules on one of them do not affect the other. +/// +/// To give a table a caption and make it [referenceable]($ref), put it into a +/// [figure]. +/// +/// # Example +/// +/// The example below demonstrates some of the most common table options. +/// ```example +/// #table( +/// columns: (1fr, auto, auto), +/// inset: 10pt, +/// align: horizon, +/// table.header( +/// [], [*Volume*], [*Parameters*], +/// ), +/// image("cylinder.svg"), +/// $ pi h (D^2 - d^2) / 4 $, +/// [ +/// $h$: height \ +/// $D$: outer radius \ +/// $d$: inner radius +/// ], +/// image("tetrahedron.svg"), +/// $ sqrt(2) / 12 a^3 $, +/// [$a$: edge length] +/// ) +/// ``` +/// +/// Much like with grids, you can use [`table.cell`]($table.cell) to customize +/// the appearance and the position of each cell. +/// +/// ```example +/// >>> #set page(width: auto) +/// >>> #set text(font: "IBM Plex Sans") +/// >>> #let gray = rgb("#565565") +/// >>> +/// #set table( +/// stroke: none, +/// gutter: 0.2em, +/// fill: (x, y) => +/// if x == 0 or y == 0 { gray }, +/// inset: (right: 1.5em), +/// ) +/// +/// #show table.cell: it => { +/// if it.x == 0 or it.y == 0 { +/// set text(white) +/// strong(it) +/// } else if it.body == [] { +/// // Replace empty cells with 'N/A' +/// pad(..it.inset)[_N/A_] +/// } else { +/// it +/// } +/// } +/// +/// #let a = table.cell( +/// fill: green.lighten(60%), +/// )[A] +/// #let b = table.cell( +/// fill: aqua.lighten(60%), +/// )[B] +/// +/// #table( +/// columns: 4, +/// [], [Exam 1], [Exam 2], [Exam 3], +/// +/// [John], [], a, [], +/// [Mary], [], a, a, +/// [Robert], b, a, b, +/// ) +/// ``` +#[elem(scope, Show, LocalName, Figurable)] +pub struct TableElem { + /// The column sizes. See the [grid documentation]($grid) for more + /// information on track sizing. + #[borrowed] + pub columns: TrackSizings, + + /// The row sizes. See the [grid documentation]($grid) for more information + /// on track sizing. + #[borrowed] + pub rows: TrackSizings, + + /// The gaps between rows and columns. This is a shorthand for setting + /// `column-gutter` and `row-gutter` to the same value. See the [grid + /// documentation]($grid) for more information on gutters. + #[external] + pub gutter: TrackSizings, + + /// The gaps between columns. Takes precedence over `gutter`. See the + /// [grid documentation]($grid) for more information on gutters. + #[borrowed] + #[parse( + let gutter = args.named("gutter")?; + args.named("column-gutter")?.or_else(|| gutter.clone()) + )] + pub column_gutter: TrackSizings, + + /// The gaps between rows. Takes precedence over `gutter`. See the + /// [grid documentation]($grid) for more information on gutters. + #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))] + #[borrowed] + pub row_gutter: TrackSizings, + + /// How to fill the cells. + /// + /// This can be a color or a function that returns a color. The function + /// receives the cells' column and row indices, starting from zero. This can + /// be used to implement striped tables. + /// + /// ```example + /// #table( + /// fill: (x, _) => + /// if calc.odd(x) { luma(240) } + /// else { white }, + /// align: (x, y) => + /// if y == 0 { center } + /// else if x == 0 { left } + /// else { right }, + /// columns: 4, + /// [], [*Q1*], [*Q2*], [*Q3*], + /// [Revenue:], [1000 €], [2000 €], [3000 €], + /// [Expenses:], [500 €], [1000 €], [1500 €], + /// [Profit:], [500 €], [1000 €], [1500 €], + /// ) + /// ``` + #[borrowed] + pub fill: Celled<Option<Paint>>, + + /// How to align the cells' content. + /// + /// This can either be a single alignment, an array of alignments + /// (corresponding to each column) or a function that returns an alignment. + /// The function receives the cells' column and row indices, starting from + /// zero. If set to `{auto}`, the outer alignment is used. + /// + /// ```example + /// #table( + /// columns: 3, + /// align: (left, center, right), + /// [Hello], [Hello], [Hello], + /// [A], [B], [C], + /// ) + /// ``` + #[borrowed] + pub align: Celled<Smart<Alignment>>, + + /// How to [stroke] the cells. + /// + /// Strokes can be disabled by setting this to `{none}`. + /// + /// If it is necessary to place lines which can cross spacing between cells + /// produced by the `gutter` option, or to override the stroke between + /// multiple specific cells, consider specifying one or more of + /// [`table.hline`]($table.hline) and [`table.vline`]($table.vline) + /// alongside your table cells. + /// + /// See the [grid documentation]($grid.stroke) for more information on + /// strokes. + #[resolve] + #[fold] + #[default(Celled::Value(Sides::splat(Some(Some(Arc::new(Stroke::default()))))))] + pub stroke: Celled<Sides<Option<Option<Arc<Stroke>>>>>, + + /// How much to pad the cells' content. + /// + /// ```example + /// #table( + /// inset: 10pt, + /// [Hello], + /// [World], + /// ) + /// + /// #table( + /// columns: 2, + /// inset: ( + /// x: 20pt, + /// y: 10pt, + /// ), + /// [Hello], + /// [World], + /// ) + /// ``` + #[fold] + #[default(Celled::Value(Sides::splat(Some(Abs::pt(5.0).into()))))] + pub inset: Celled<Sides<Option<Rel<Length>>>>, + + /// The contents of the table cells, plus any extra table lines specified + /// with the [`table.hline`]($table.hline) and + /// [`table.vline`]($table.vline) elements. + #[variadic] + pub children: Vec<TableChild>, +} + +#[scope] +impl TableElem { + #[elem] + type TableCell; + + #[elem] + type TableHLine; + + #[elem] + type TableVLine; + + #[elem] + type TableHeader; + + #[elem] + type TableFooter; +} + +impl Show for Packed<TableElem> { + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_table) + .pack() + .spanned(self.span())) + } +} + +impl LocalName for Packed<TableElem> { + const KEY: &'static str = "table"; +} + +impl Figurable for Packed<TableElem> {} + +/// Any child of a table element. +#[derive(Debug, PartialEq, Clone, Hash)] +pub enum TableChild { + Header(Packed<TableHeader>), + Footer(Packed<TableFooter>), + Item(TableItem), +} + +cast! { + TableChild, + self => match self { + Self::Header(header) => header.into_value(), + Self::Footer(footer) => footer.into_value(), + Self::Item(item) => item.into_value(), + }, + v: Content => { + v.try_into()? + }, +} + +impl TryFrom<Content> for TableChild { + type Error = HintedString; + + fn try_from(value: Content) -> HintedStrResult<Self> { + if value.is::<GridHeader>() { + bail!( + "cannot use `grid.header` as a table header"; + hint: "use `table.header` instead" + ) + } + if value.is::<GridFooter>() { + bail!( + "cannot use `grid.footer` as a table footer"; + hint: "use `table.footer` instead" + ) + } + + value + .into_packed::<TableHeader>() + .map(Self::Header) + .or_else(|value| value.into_packed::<TableFooter>().map(Self::Footer)) + .or_else(|value| TableItem::try_from(value).map(Self::Item)) + } +} + +/// A table item, which is the basic unit of table specification. +#[derive(Debug, PartialEq, Clone, Hash)] +pub enum TableItem { + HLine(Packed<TableHLine>), + VLine(Packed<TableVLine>), + Cell(Packed<TableCell>), +} + +cast! { + TableItem, + self => match self { + Self::HLine(hline) => hline.into_value(), + Self::VLine(vline) => vline.into_value(), + Self::Cell(cell) => cell.into_value(), + }, + v: Content => { + v.try_into()? + }, +} + +impl TryFrom<Content> for TableItem { + type Error = HintedString; + + fn try_from(value: Content) -> HintedStrResult<Self> { + if value.is::<GridHeader>() { + bail!("cannot place a grid header within another header or footer"); + } + if value.is::<TableHeader>() { + bail!("cannot place a table header within another header or footer"); + } + if value.is::<GridFooter>() { + bail!("cannot place a grid footer within another footer or header"); + } + if value.is::<TableFooter>() { + bail!("cannot place a table footer within another footer or header"); + } + if value.is::<GridCell>() { + bail!( + "cannot use `grid.cell` as a table cell"; + hint: "use `table.cell` instead" + ); + } + if value.is::<GridHLine>() { + bail!( + "cannot use `grid.hline` as a table line"; + hint: "use `table.hline` instead" + ); + } + if value.is::<GridVLine>() { + bail!( + "cannot use `grid.vline` as a table line"; + hint: "use `table.vline` instead" + ); + } + + Ok(value + .into_packed::<TableHLine>() + .map(Self::HLine) + .or_else(|value| value.into_packed::<TableVLine>().map(Self::VLine)) + .or_else(|value| value.into_packed::<TableCell>().map(Self::Cell)) + .unwrap_or_else(|value| { + let span = value.span(); + Self::Cell(Packed::new(TableCell::new(value)).spanned(span)) + })) + } +} + +/// A repeatable table header. +/// +/// You should wrap your tables' heading rows in this function even if you do not +/// plan to wrap your table across pages because Typst will use this function to +/// attach accessibility metadata to tables in the future and ensure universal +/// access to your document. +/// +/// You can use the `repeat` parameter to control whether your table's header +/// will be repeated across pages. +/// +/// ```example +/// #set page(height: 11.5em) +/// #set table( +/// fill: (x, y) => +/// if x == 0 or y == 0 { +/// gray.lighten(40%) +/// }, +/// align: right, +/// ) +/// +/// #show table.cell.where(x: 0): strong +/// #show table.cell.where(y: 0): strong +/// +/// #table( +/// columns: 4, +/// table.header( +/// [], [Blue chip], +/// [Fresh IPO], [Penny st'k], +/// ), +/// table.cell( +/// rowspan: 6, +/// align: horizon, +/// rotate(-90deg, reflow: true)[ +/// *USD / day* +/// ], +/// ), +/// [0.20], [104], [5], +/// [3.17], [108], [4], +/// [1.59], [84], [1], +/// [0.26], [98], [15], +/// [0.01], [195], [4], +/// [7.34], [57], [2], +/// ) +/// ``` +#[elem(name = "header", title = "Table Header")] +pub struct TableHeader { + /// Whether this header should be repeated across pages. + #[default(true)] + pub repeat: bool, + + /// The cells and lines within the header. + #[variadic] + pub children: Vec<TableItem>, +} + +/// A repeatable table footer. +/// +/// Just like the [`table.header`]($table.header) element, the footer can repeat +/// itself on every page of the table. This is useful for improving legibility +/// by adding the column labels in both the header and footer of a large table, +/// totals, or other information that should be visible on every page. +/// +/// No other table cells may be placed after the footer. +#[elem(name = "footer", title = "Table Footer")] +pub struct TableFooter { + /// Whether this footer should be repeated across pages. + #[default(true)] + pub repeat: bool, + + /// The cells and lines within the footer. + #[variadic] + pub children: Vec<TableItem>, +} + +/// A horizontal line in the table. +/// +/// Overrides any per-cell stroke, including stroke specified through the +/// table's `stroke` field. Can cross spacing between cells created through the +/// table's [`column-gutter`]($table.column-gutter) option. +/// +/// Use this function instead of the table's `stroke` field if you want to +/// manually place a horizontal line at a specific position in a single table. +/// Consider using [table's `stroke`]($table.stroke) field or [`table.cell`'s +/// `stroke`]($table.cell.stroke) field instead if the line you want to place is +/// part of all your tables' designs. +/// +/// ```example +/// #set table.hline(stroke: .6pt) +/// +/// #table( +/// stroke: none, +/// columns: (auto, 1fr), +/// [09:00], [Badge pick up], +/// [09:45], [Opening Keynote], +/// [10:30], [Talk: Typst's Future], +/// [11:15], [Session: Good PRs], +/// table.hline(start: 1), +/// [Noon], [_Lunch break_], +/// table.hline(start: 1), +/// [14:00], [Talk: Tracked Layout], +/// [15:00], [Talk: Automations], +/// [16:00], [Workshop: Tables], +/// table.hline(), +/// [19:00], [Day 1 Attendee Mixer], +/// ) +/// ``` +#[elem(name = "hline", title = "Table Horizontal Line")] +pub struct TableHLine { + /// The row above which the horizontal line is placed (zero-indexed). + /// Functions identically to the `y` field in [`grid.hline`]($grid.hline.y). + pub y: Smart<usize>, + + /// The column at which the horizontal line starts (zero-indexed, inclusive). + pub start: usize, + + /// The column before which the horizontal line ends (zero-indexed, + /// exclusive). + pub end: Option<NonZeroUsize>, + + /// The line's stroke. + /// + /// Specifying `{none}` removes any lines previously placed across this + /// line's range, including hlines or per-cell stroke below it. + #[resolve] + #[fold] + #[default(Some(Arc::new(Stroke::default())))] + pub stroke: Option<Arc<Stroke>>, + + /// The position at which the line is placed, given its row (`y`) - either + /// `{top}` to draw above it or `{bottom}` to draw below it. + /// + /// This setting is only relevant when row gutter is enabled (and + /// shouldn't be used otherwise - prefer just increasing the `y` field by + /// one instead), since then the position below a row becomes different + /// from the position above the next row due to the spacing between both. + #[default(OuterVAlignment::Top)] + pub position: OuterVAlignment, +} + +/// A vertical line in the table. See the docs for [`grid.vline`]($grid.vline) +/// for more information regarding how to use this element's fields. +/// +/// Overrides any per-cell stroke, including stroke specified through the +/// table's `stroke` field. Can cross spacing between cells created through the +/// table's [`row-gutter`]($table.row-gutter) option. +/// +/// Similar to [`table.hline`]($table.hline), use this function if you want to +/// manually place a vertical line at a specific position in a single table and +/// use the [table's `stroke`]($table.stroke) field or [`table.cell`'s +/// `stroke`]($table.cell.stroke) field instead if the line you want to place is +/// part of all your tables' designs. +#[elem(name = "vline", title = "Table Vertical Line")] +pub struct TableVLine { + /// The column before which the horizontal line is placed (zero-indexed). + /// Functions identically to the `x` field in [`grid.vline`]($grid.vline). + pub x: Smart<usize>, + + /// The row at which the vertical line starts (zero-indexed, inclusive). + pub start: usize, + + /// The row on top of which the vertical line ends (zero-indexed, + /// exclusive). + pub end: Option<NonZeroUsize>, + + /// The line's stroke. + /// + /// Specifying `{none}` removes any lines previously placed across this + /// line's range, including vlines or per-cell stroke below it. + #[resolve] + #[fold] + #[default(Some(Arc::new(Stroke::default())))] + pub stroke: Option<Arc<Stroke>>, + + /// The position at which the line is placed, given its column (`x`) - + /// either `{start}` to draw before it or `{end}` to draw after it. + /// + /// The values `{left}` and `{right}` are also accepted, but discouraged as + /// they cause your table to be inconsistent between left-to-right and + /// right-to-left documents. + /// + /// This setting is only relevant when column gutter is enabled (and + /// shouldn't be used otherwise - prefer just increasing the `x` field by + /// one instead), since then the position after a column becomes different + /// from the position before the next column due to the spacing between + /// both. + #[default(OuterHAlignment::Start)] + pub position: OuterHAlignment, +} + +/// A cell in the table. Use this to position a cell manually or to apply +/// styling. To do the latter, you can either use the function to override the +/// properties for a particular cell, or use it in show rules to apply certain +/// styles to multiple cells at once. +/// +/// Perhaps the most important use case of `{table.cell}` is to make a cell span +/// multiple columns and/or rows with the `colspan` and `rowspan` fields. +/// +/// ```example +/// >>> #set page(width: auto) +/// #show table.cell.where(y: 0): strong +/// #set table( +/// stroke: (x, y) => if y == 0 { +/// (bottom: 0.7pt + black) +/// }, +/// align: (x, y) => ( +/// if x > 0 { center } +/// else { left } +/// ) +/// ) +/// +/// #table( +/// columns: 3, +/// table.header( +/// [Substance], +/// [Subcritical °C], +/// [Supercritical °C], +/// ), +/// [Hydrochloric Acid], +/// [12.0], [92.1], +/// [Sodium Myreth Sulfate], +/// [16.6], [104], +/// [Potassium Hydroxide], +/// table.cell(colspan: 2)[24.7], +/// ) +/// ``` +/// +/// For example, you can override the fill, alignment or inset for a single +/// cell: +/// +/// ```example +/// >>> #set page(width: auto) +/// // You can also import those. +/// #import table: cell, header +/// +/// #table( +/// columns: 2, +/// align: center, +/// header( +/// [*Trip progress*], +/// [*Itinerary*], +/// ), +/// cell( +/// align: right, +/// fill: fuchsia.lighten(80%), +/// [🚗], +/// ), +/// [Get in, folks!], +/// [🚗], [Eat curbside hotdog], +/// cell(align: left)[🌴🚗], +/// cell( +/// inset: 0.06em, +/// text(1.62em)[🛖🌅🌊], +/// ), +/// ) +/// ``` +/// +/// You may also apply a show rule on `table.cell` to style all cells at once. +/// Combined with selectors, this allows you to apply styles based on a cell's +/// position: +/// +/// ```example +/// #show table.cell.where(x: 0): strong +/// +/// #table( +/// columns: 3, +/// gutter: 3pt, +/// [Name], [Age], [Strength], +/// [Hannes], [36], [Grace], +/// [Irma], [50], [Resourcefulness], +/// [Vikram], [49], [Perseverance], +/// ) +/// ``` +#[elem(name = "cell", title = "Table Cell", Show)] +pub struct TableCell { + /// The cell's body. + #[required] + pub body: Content, + + /// The cell's column (zero-indexed). + /// Functions identically to the `x` field in [`grid.cell`]($grid.cell). + pub x: Smart<usize>, + + /// The cell's row (zero-indexed). + /// Functions identically to the `y` field in [`grid.cell`]($grid.cell). + pub y: Smart<usize>, + + /// The amount of columns spanned by this cell. + #[default(NonZeroUsize::ONE)] + pub colspan: NonZeroUsize, + + /// The amount of rows spanned by this cell. + #[default(NonZeroUsize::ONE)] + pub rowspan: NonZeroUsize, + + /// The cell's [fill]($table.fill) override. + pub fill: Smart<Option<Paint>>, + + /// The cell's [alignment]($table.align) override. + pub align: Smart<Alignment>, + + /// The cell's [inset]($table.inset) override. + pub inset: Smart<Sides<Option<Rel<Length>>>>, + + /// The cell's [stroke]($table.stroke) override. + #[resolve] + #[fold] + pub stroke: Sides<Option<Option<Arc<Stroke>>>>, + + /// Whether rows spanned by this cell can be placed in different pages. + /// When equal to `{auto}`, a cell spanning only fixed-size rows is + /// unbreakable, while a cell spanning at least one `{auto}`-sized row is + /// breakable. + pub breakable: Smart<bool>, +} + +cast! { + TableCell, + v: Content => v.into(), +} + +impl Show for Packed<TableCell> { + fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles)) + } +} + +impl Default for Packed<TableCell> { + fn default() -> Self { + Packed::new(TableCell::new(Content::default())) + } +} + +impl From<Content> for TableCell { + fn from(value: Content) -> Self { + #[allow(clippy::unwrap_or_default)] + value.unpack::<Self>().unwrap_or_else(Self::new) + } +} diff --git a/crates/typst-library/src/model/terms.rs b/crates/typst-library/src/model/terms.rs new file mode 100644 index 00000000..036a03e2 --- /dev/null +++ b/crates/typst-library/src/model/terms.rs @@ -0,0 +1,204 @@ +use typst_utils::Numeric; + +use crate::diag::{bail, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain, + Styles, +}; +use crate::layout::{Dir, Em, HElem, Length, Sides, StackChild, StackElem, VElem}; +use crate::model::{ListItemLike, ListLike, ParElem}; +use crate::text::TextElem; + +/// A list of terms and their descriptions. +/// +/// Displays a sequence of terms and their descriptions vertically. When the +/// descriptions span over multiple lines, they use hanging indent to +/// communicate the visual hierarchy. +/// +/// # Example +/// ```example +/// / Ligature: A merged glyph. +/// / Kerning: A spacing adjustment +/// between two adjacent letters. +/// ``` +/// +/// # Syntax +/// This function also has dedicated syntax: Starting a line with a slash, +/// followed by a term, a colon and a description creates a term list item. +#[elem(scope, title = "Term List", Show)] +pub struct TermsElem { + /// Defines the default [spacing]($terms.spacing) of the term list. If it is + /// `{false}`, the items are spaced apart with + /// [paragraph spacing]($par.spacing). If it is `{true}`, they use + /// [paragraph leading]($par.leading) instead. This makes the list more + /// compact, which can look better if the items are short. + /// + /// In markup mode, the value of this parameter is determined based on + /// whether items are separated with a blank line. If items directly follow + /// each other, this is set to `{true}`; if items are separated by a blank + /// line, this is set to `{false}`. The markup-defined tightness cannot be + /// overridden with set rules. + /// + /// ```example + /// / Fact: If a term list has a lot + /// of text, and maybe other inline + /// content, it should not be tight + /// anymore. + /// + /// / Tip: To make it wide, simply + /// insert a blank line between the + /// items. + /// ``` + #[default(true)] + pub tight: bool, + + /// The separator between the item and the description. + /// + /// If you want to just separate them with a certain amount of space, use + /// `{h(2cm, weak: true)}` as the separator and replace `{2cm}` with your + /// desired amount of space. + /// + /// ```example + /// #set terms(separator: [: ]) + /// + /// / Colon: A nice separator symbol. + /// ``` + #[default(HElem::new(Em::new(0.6).into()).with_weak(true).pack())] + #[borrowed] + pub separator: Content, + + /// The indentation of each item. + pub indent: Length, + + /// The hanging indent of the description. + /// + /// This is in addition to the whole item's `indent`. + /// + /// ```example + /// #set terms(hanging-indent: 0pt) + /// / Term: This term list does not + /// make use of hanging indents. + /// ``` + #[default(Em::new(2.0).into())] + pub hanging_indent: Length, + + /// The spacing between the items of the term list. + /// + /// If set to `{auto}`, uses paragraph [`leading`]($par.leading) for tight + /// term lists and paragraph [`spacing`]($par.spacing) for wide + /// (non-tight) term lists. + pub spacing: Smart<Length>, + + /// The term list's children. + /// + /// When using the term list syntax, adjacent items are automatically + /// collected into term lists, even through constructs like for loops. + /// + /// ```example + /// #for (year, product) in ( + /// "1978": "TeX", + /// "1984": "LaTeX", + /// "2019": "Typst", + /// ) [/ #product: Born in #year.] + /// ``` + #[variadic] + pub children: Vec<Packed<TermItem>>, +} + +#[scope] +impl TermsElem { + #[elem] + type TermItem; +} + +impl Show for Packed<TermsElem> { + fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let separator = self.separator(styles); + let indent = self.indent(styles); + let hanging_indent = self.hanging_indent(styles); + let gutter = self.spacing(styles).unwrap_or_else(|| { + if self.tight(styles) { + ParElem::leading_in(styles).into() + } else { + ParElem::spacing_in(styles).into() + } + }); + + let pad = hanging_indent + indent; + let unpad = (!hanging_indent.is_zero()) + .then(|| HElem::new((-hanging_indent).into()).pack()); + + let mut children = vec![]; + for child in self.children().iter() { + let mut seq = vec![]; + seq.extend(unpad.clone()); + seq.push(child.term().clone().strong()); + seq.push((*separator).clone()); + seq.push(child.description().clone()); + children.push(StackChild::Block(Content::sequence(seq))); + } + + let mut padding = Sides::default(); + if TextElem::dir_in(styles) == Dir::LTR { + padding.left = pad.into(); + } else { + padding.right = pad.into(); + } + + let mut realized = StackElem::new(children) + .with_spacing(Some(gutter.into())) + .pack() + .padded(padding); + + if self.tight(styles) { + let leading = ParElem::leading_in(styles); + let spacing = + VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); + realized = spacing + realized; + } + + Ok(realized) + } +} + +/// A term list item. +#[elem(name = "item", title = "Term List Item")] +pub struct TermItem { + /// The term described by the list item. + #[required] + pub term: Content, + + /// The description of the term. + #[required] + pub description: Content, +} + +cast! { + TermItem, + array: Array => { + let mut iter = array.into_iter(); + let (term, description) = match (iter.next(), iter.next(), iter.next()) { + (Some(a), Some(b), None) => (a.cast()?, b.cast()?), + _ => bail!("array must contain exactly two entries"), + }; + Self::new(term, description) + }, + v: Content => v.unpack::<Self>().map_err(|_| "expected term item or array")?, +} + +impl ListLike for TermsElem { + type Item = TermItem; + + fn create(children: Vec<Packed<Self::Item>>, tight: bool) -> Self { + Self::new(children).with_tight(tight) + } +} + +impl ListItemLike for TermItem { + fn styled(mut item: Packed<Self>, styles: Styles) -> Packed<Self> { + item.term.style_in_place(styles.clone()); + item.description.style_in_place(styles); + item + } +} diff --git a/crates/typst-library/src/routines.rs b/crates/typst-library/src/routines.rs new file mode 100644 index 00000000..6b78b7fb --- /dev/null +++ b/crates/typst-library/src/routines.rs @@ -0,0 +1,368 @@ +#![allow(unused)] + +use std::hash::{Hash, Hasher}; +use std::num::NonZeroUsize; + +use comemo::{Tracked, TrackedMut}; +use typst_syntax::Span; +use typst_utils::LazyHash; + +use crate::diag::SourceResult; +use crate::engine::{Engine, Route, Sink, Traced}; +use crate::foundations::{ + Args, Cast, Closure, Content, Context, Func, Packed, Scope, StyleChain, StyleVec, + Styles, Value, +}; +use crate::introspection::{Introspector, Locator, SplitLocator}; +use crate::layout::{ + Abs, BoxElem, ColumnsElem, Fragment, Frame, GridElem, InlineItem, MoveElem, PadElem, + Region, Regions, Rel, RepeatElem, RotateElem, ScaleElem, Size, SkewElem, StackElem, +}; +use crate::math::EquationElem; +use crate::model::{Document, DocumentInfo, EnumElem, ListElem, TableElem}; +use crate::visualize::{ + CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem, + SquareElem, +}; +use crate::World; + +/// Defines the `Routines` struct. +macro_rules! routines { + ($( + $(#[$attr:meta])* + fn $name:ident $(<$($time:lifetime),*>)? ($($args:tt)*) -> $ret:ty + )*) => { + /// Defines implementation of various Typst compiler routines as a table + /// of function pointers. + /// + /// This is essentially dynamic linking and done to allow for crate + /// splitting. + pub struct Routines { + $( + $(#[$attr])* + pub $name: $(for<$($time),*>)? fn ($($args)*) -> $ret + ),* + } + + impl Hash for Routines { + fn hash<H: Hasher>(&self, _: &mut H) {} + } + }; +} + +routines! { + /// Evaluates a string as code and return the resulting value. + fn eval_string( + routines: &Routines, + world: Tracked<dyn World + '_>, + string: &str, + span: Span, + mode: EvalMode, + scope: Scope, + ) -> SourceResult<Value> + + /// Call the closure in the context with the arguments. + fn eval_closure( + func: &Func, + closure: &LazyHash<Closure>, + routines: &Routines, + world: Tracked<dyn World + '_>, + introspector: Tracked<Introspector>, + traced: Tracked<Traced>, + sink: TrackedMut<Sink>, + route: Tracked<Route>, + context: Tracked<Context>, + args: Args, + ) -> SourceResult<Value> + + /// Realizes content into a flat list of well-known, styled items. + fn realize<'a>( + kind: RealizationKind, + engine: &mut Engine, + locator: &mut SplitLocator, + arenas: &'a Arenas, + content: &'a Content, + styles: StyleChain<'a>, + ) -> SourceResult<Vec<Pair<'a>>> + + /// Layout content into a document. + fn layout_document( + engine: &mut Engine, + content: &Content, + styles: StyleChain, + ) -> SourceResult<Document> + + /// Lays out content into multiple regions. + fn layout_fragment( + engine: &mut Engine, + content: &Content, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> + + /// Lays out content into a single region, producing a single frame. + fn layout_frame( + engine: &mut Engine, + content: &Content, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out inline content. + fn layout_inline( + engine: &mut Engine, + children: &StyleVec, + locator: Locator, + styles: StyleChain, + consecutive: bool, + region: Size, + expand: bool, + ) -> SourceResult<Fragment> + + /// Lays out a [`BoxElem`]. + fn layout_box( + elem: &Packed<BoxElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Size, + ) -> SourceResult<Frame> + + /// Lays out a [`ListElem`]. + fn layout_list( + elem: &Packed<ListElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> + + /// Lays out an [`EnumElem`]. + fn layout_enum( + elem: &Packed<EnumElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> + + /// Lays out a [`GridElem`]. + fn layout_grid( + elem: &Packed<GridElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> + + /// Lays out a [`TableElem`]. + fn layout_table( + elem: &Packed<TableElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> + + /// Lays out a [`StackElem`]. + fn layout_stack( + elem: &Packed<StackElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> + + /// Lays out a [`ColumnsElem`]. + fn layout_columns( + elem: &Packed<ColumnsElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> + + /// Lays out a [`MoveElem`]. + fn layout_move( + elem: &Packed<MoveElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out a [`RotateElem`]. + fn layout_rotate( + elem: &Packed<RotateElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out a [`ScaleElem`]. + fn layout_scale( + elem: &Packed<ScaleElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out a [`SkewElem`]. + fn layout_skew( + elem: &Packed<SkewElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out a [`RepeatElem`]. + fn layout_repeat( + elem: &Packed<RepeatElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out a [`PadElem`]. + fn layout_pad( + elem: &Packed<PadElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> + + /// Lays out a [`LineElem`]. + fn layout_line( + elem: &Packed<LineElem>, + _: &mut Engine, + _: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out a [`PathElem`]. + fn layout_path( + elem: &Packed<PathElem>, + _: &mut Engine, + _: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out a [`PolygonElem`]. + fn layout_polygon( + elem: &Packed<PolygonElem>, + _: &mut Engine, + _: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out a [`RectElem`]. + fn layout_rect( + elem: &Packed<RectElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out a [`SquareElem`]. + fn layout_square( + elem: &Packed<SquareElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out a [`EllipseElem`]. + fn layout_ellipse( + elem: &Packed<EllipseElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out a [`CircleElem`]. + fn layout_circle( + elem: &Packed<CircleElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out an [`ImageElem`]. + fn layout_image( + elem: &Packed<ImageElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out an [`EquationElem`] in a paragraph. + fn layout_equation_inline( + elem: &Packed<EquationElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Size, + ) -> SourceResult<Vec<InlineItem>> + + /// Lays out an [`EquationElem`] in a flow. + fn layout_equation_block( + elem: &Packed<EquationElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> +} + +/// In which mode to evaluate a string. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum EvalMode { + /// Evaluate as code, as after a hash. + Code, + /// Evaluate as markup, like in a Typst file. + Markup, + /// Evaluate as math, as in an equation. + Math, +} + +/// Defines what kind of realization we are performing. +pub enum RealizationKind<'a> { + /// This the root realization for the document. Requires a mutable reference + /// to document metadata that will be filled from `set document` rules. + Root(&'a mut DocumentInfo), + /// A nested realization in a container (e.g. a `block`). + Container, + /// A realization within math. + Math, +} + +/// Temporary storage arenas for lifetime extension during realization. +/// +/// Must be kept live while the content returned from realization is processed. +#[derive(Default)] +pub struct Arenas { + /// A typed arena for owned content. + pub content: typed_arena::Arena<Content>, + /// A typed arena for owned styles. + pub styles: typed_arena::Arena<Styles>, + /// An untyped arena for everything that is `Copy`. + pub bump: bumpalo::Bump, +} + +/// A pair of content and a style chain that applies to it. +pub type Pair<'a> = (&'a Content, StyleChain<'a>); diff --git a/crates/typst-library/src/symbols/emoji.rs b/crates/typst-library/src/symbols/emoji.rs new file mode 100644 index 00000000..1bd57ec0 --- /dev/null +++ b/crates/typst-library/src/symbols/emoji.rs @@ -0,0 +1,1363 @@ +use crate::foundations::{Module, Scope, Symbol}; + +/// A module with all emoji. +pub fn emoji() -> Module { + let mut scope = Scope::new(); + for (name, symbol) in EMOJI { + scope.define(*name, symbol.clone()); + } + Module::new("emoji", scope) +} + +/// A list of named emoji. +const EMOJI: &[(&str, Symbol)] = typst_macros::symbols! { + abacus: '🧮', + abc: '🔤', + abcd: '🔡', + ABCD: '🔠', + accordion: '🪗', + aesculapius: '⚕', + airplane: [ + '✈', + landing: '🛬', + small: '🛩', + takeoff: '🛫', + ], + alembic: '⚗', + alien: ['👽', monster: '👾'], + ambulance: '🚑', + amphora: '🏺', + anchor: '⚓', + anger: '💢', + ant: '🐜', + apple: [green: '🍏', red: '🍎'], + arm: [mech: '🦾', muscle: '💪', selfie: '🤳'], + arrow: [ + r.filled: '➡', + r.hook: '↪', + r.soon: '🔜', + l.filled: '⬅', + l.hook: '↩', + l.back: '🔙', + l.end: '🔚', + t.filled: '⬆', + t.curve: '⤴', + t.top: '🔝', + b.filled: '⬇', + b.curve: '⤵', + l.r: '↔', + l.r.on: '🔛', + t.b: '↕', + bl: '↙', + br: '↘', + tl: '↖', + tr: '↗', + ], + arrows: [cycle: '🔄'], + ast: ['*', box: '✳'], + atm: '🏧', + atom: '⚛', + aubergine: '🍆', + avocado: '🥑', + axe: '🪓', + baby: ['👶', angel: '👼', box: '🚼'], + babybottle: '🍼', + backpack: '🎒', + bacon: '🥓', + badger: '🦡', + badminton: '🏸', + bagel: '🥯', + baggageclaim: '🛄', + baguette: '🥖', + balloon: '🎈', + ballot: [check: '☑'], + ballotbox: '🗳', + banana: '🍌', + banjo: '🪕', + bank: '🏦', + barberpole: '💈', + baseball: '⚾', + basecap: '🧢', + basket: '🧺', + basketball: ['⛹', ball: '🏀'], + bat: '🦇', + bathtub: ['🛀', foam: '🛁'], + battery: ['🔋', low: '🪫'], + beach: [palm: '🏝', umbrella: '🏖'], + beads: '📿', + beans: '🫘', + bear: '🐻', + beaver: '🦫', + bed: ['🛏', person: '🛌'], + bee: '🐝', + beer: ['🍺', clink: '🍻'], + beet: '', + beetle: ['🪲', lady: '🐞'], + bell: ['🔔', ding: '🛎', not: '🔕'], + bento: '🍱', + bicyclist: ['🚴', mountain: '🚵'], + bike: ['🚲', not: '🚳'], + bikini: '👙', + billiards: '🎱', + bin: '🗑', + biohazard: '☣', + bird: '🐦', + bison: '🦬', + blood: '🩸', + blouse: '👚', + blowfish: '🐡', + blueberries: '🫐', + boar: '🐗', + boat: [ + sail: '⛵', + row: '🚣', + motor: '🛥', + speed: '🚤', + canoe: '🛶', + ], + bolt: '🔩', + bomb: '💣', + bone: '🦴', + book: [ + red: '📕', + blue: '📘', + green: '📗', + orange: '📙', + spiral: '📒', + open: '📖', + ], + bookmark: '🔖', + books: '📚', + boomerang: '🪃', + bordercontrol: '🛂', + bouquet: '💐', + bow: '🏹', + bowl: [spoon: '🥣', steam: '🍜'], + bowling: '🎳', + boxing: '🥊', + boy: '👦', + brain: '🧠', + bread: '🍞', + brick: '🧱', + bride: '👰', + bridge: [fog: '🌁', night: '🌉'], + briefcase: '💼', + briefs: '🩲', + brightness: [high: '🔆', low: '🔅'], + broccoli: '🥦', + broom: '🧹', + brush: '🖌', + bubble: [ + speech.r: '💬', + speech.l: '🗨', + thought: '💭', + anger.r: '🗯', + ], + bubbles: '🫧', + bubbletea: '🧋', + bucket: '🪣', + buffalo: [water: '🐃'], + bug: '🐛', + builder: '👷', + burger: '🍔', + burrito: '🌯', + bus: [ + '🚌', + front: '🚍', + small: '🚐', + stop: '🚏', + trolley: '🚎', + ], + butter: '🧈', + butterfly: '🦋', + button: ['🔲', alt: '🔳', radio: '🔘'], + cabinet: [file: '🗄'], + cablecar: ['🚠', small: '🚡'], + cactus: '🌵', + cake: [ + '🎂', + fish: '🍥', + moon: '🥮', + slice: '🍰', + ], + calendar: ['📅', spiral: '🗓', tearoff: '📆'], + camel: ['🐫', dromedar: '🐪'], + camera: [ + '📷', + flash: '📸', + movie: '🎥', + movie.box: '🎦', + video: '📹', + ], + camping: '🏕', + can: '🥫', + candle: '🕯', + candy: '🍬', + cane: '🦯', + car: [ + '🚗', + front: '🚘', + pickup: '🛻', + police: '🚓', + police.front: '🚔', + racing: '🏎', + rickshaw: '🛺', + suv: '🚙', + ], + card: [credit: '💳', id: '🪪'], + cardindex: '📇', + carrot: '🥕', + cart: '🛒', + cassette: '📼', + castle: [eu: '🏰', jp: '🏯'], + cat: [ + '🐈', + face: '🐱', + face.angry: '😾', + face.cry: '😿', + face.heart: '😻', + face.joy: '😹', + face.kiss: '😽', + face.laugh: '😸', + face.shock: '🙀', + face.smile: '😺', + face.smirk: '😼', + ], + chain: '🔗', + chains: '⛓', + chair: '🪑', + champagne: '🍾', + chart: [ + bar: '📊', + up: '📈', + down: '📉', + yen.up: '💹', + ], + checkmark: [heavy: '✔', box: '✅'], + cheese: '🧀', + cherries: '🍒', + chess: '♟', + chestnut: '🌰', + chicken: [ + '🐔', + baby: '🐥', + baby.egg: '🐣', + baby.head: '🐤', + leg: '🍗', + male: '🐓', + ], + child: '🧒', + chipmunk: '🐿', + chocolate: '🍫', + chopsticks: '🥢', + church: ['⛪', love: '💒'], + cigarette: ['🚬', not: '🚭'], + circle: [ + black: '⚫', + blue: '🔵', + brown: '🟤', + green: '🟢', + orange: '🟠', + purple: '🟣', + white: '⚪', + red: '🔴', + yellow: '🟡', + stroked: '⭕', + ], + circus: '🎪', + city: [ + '🏙', + dusk: '🌆', + night: '🌃', + sunset: '🌇', + ], + clamp: '🗜', + clapperboard: '🎬', + climbing: '🧗', + clip: '📎', + clipboard: '📋', + clips: '🖇', + clock: [ + one: '🕐', + one.thirty: '🕜', + two: '🕑', + two.thirty: '🕝', + three: '🕒', + three.thirty: '🕞', + four: '🕓', + four.thirty: '🕟', + five: '🕔', + five.thirty: '🕠', + six: '🕕', + six.thirty: '🕡', + seven: '🕖', + seven.thirty: '🕢', + eight: '🕗', + eight.thirty: '🕣', + nine: '🕘', + nine.thirty: '🕤', + ten: '🕙', + ten.thirty: '🕥', + eleven: '🕚', + eleven.thirty: '🕦', + twelve: '🕛', + twelve.thirty: '🕧', + alarm: '⏰', + old: '🕰', + timer: '⏲', + ], + cloud: [ + '☁', + dust: '💨', + rain: '🌧', + snow: '🌨', + storm: '⛈', + sun: '⛅', + sun.hidden: '🌥', + sun.rain: '🌦', + thunder: '🌩', + ], + coat: ['🧥', lab: '🥼'], + cockroach: '🪳', + cocktail: [martini: '🍸', tropical: '🍹'], + coconut: '🥥', + coffee: '☕', + coffin: '⚰', + coin: '🪙', + comet: '☄', + compass: '🧭', + computer: '🖥', + computermouse: '🖱', + confetti: '🎊', + construction: '🚧', + controller: '🎮', + cookie: ['🍪', fortune: '🥠'], + cooking: '🍳', + cool: '🆒', + copyright: '©', + coral: '🪸', + corn: '🌽', + couch: '🛋', + couple: '💑', + cow: ['🐄', face: '🐮'], + crab: '🦀', + crane: '🏗', + crayon: '🖍', + cricket: '🦗', + cricketbat: '🏏', + crocodile: '🐊', + croissant: '🥐', + crossmark: ['❌', box: '❎'], + crown: '👑', + crutch: '🩼', + crystal: '🔮', + cucumber: '🥒', + cup: [straw: '🥤'], + cupcake: '🧁', + curling: '🥌', + curry: '🍛', + custard: '🍮', + customs: '🛃', + cutlery: '🍴', + cyclone: '🌀', + dancing: [man: '🕺', woman: '💃', women.bunny: '👯'], + darts: '🎯', + dash: [wave.double: '〰'], + deer: '🦌', + desert: '🏜', + detective: '🕵', + diamond: [ + blue: '🔷', + blue.small: '🔹', + orange: '🔶', + orange.small: '🔸', + dot: '💠', + ], + die: '🎲', + dino: [pod: '🦕', rex: '🦖'], + disc: [cd: '💿', dvd: '📀', mini: '💽'], + discoball: '🪩', + diving: '🤿', + dodo: '🦤', + dog: [ + '🐕', + face: '🐶', + guide: '🦮', + poodle: '🐩', + ], + dollar: '💲', + dolphin: '🐬', + donut: '🍩', + door: '🚪', + dove: [peace: '🕊'], + dragon: ['🐉', face: '🐲'], + dress: ['👗', kimono: '👘', sari: '🥻'], + drop: '💧', + drops: '💦', + drum: ['🥁', big: '🪘'], + duck: '🦆', + dumpling: '🥟', + eagle: '🦅', + ear: ['👂', aid: '🦻'], + egg: '🥚', + eighteen: [not: '🔞'], + elephant: '🐘', + elevator: '🛗', + elf: '🧝', + email: '📧', + excl: [ + '❗', + white: '❕', + double: '‼', + quest: '⁉', + ], + explosion: '💥', + extinguisher: '🧯', + eye: '👁', + eyes: '👀', + face: [ + grin: '😀', + angry: '😠', + angry.red: '😡', + anguish: '😧', + astonish: '😲', + bandage: '🤕', + beam: '😁', + blank: '😶', + clown: '🤡', + cold: '🥶', + concern: '😦', + cool: '😎', + cover: '🤭', + cowboy: '🤠', + cry: '😭', + devil.smile: '😈', + devil.frown: '👿', + diagonal: '🫤', + disguise: '🥸', + distress: '😫', + dizzy: '😵', + dotted: '🫥', + down: '😞', + down.sweat: '😓', + drool: '🤤', + explode: '🤯', + eyeroll: '🙄', + friendly: '☺', + fear: '😨', + fear.sweat: '😰', + fever: '🤒', + flush: '😳', + frown: '☹', + frown.slight: '🙁', + frust: '😣', + goofy: '🤪', + halo: '😇', + happy: '😊', + heart: '😍', + hearts: '🥰', + heat: '🥵', + hug: '🤗', + inv: '🙃', + joy: '😂', + kiss: '😗', + kiss.smile: '😙', + kiss.heart: '😘', + kiss.blush: '😚', + lick: '😋', + lie: '🤥', + mask: '😷', + meh: '😒', + melt: '🫠', + money: '🤑', + monocle: '🧐', + nausea: '🤢', + nerd: '🤓', + neutral: '😐', + open: '😃', + party: '🥳', + peek: '🫣', + plead: '🥺', + relief: '😌', + rofl: '🤣', + sad: '😔', + salute: '🫡', + shock: '😱', + shush: '🤫', + skeptic: '🤨', + sleep: '😴', + sleepy: '😪', + smile: '😄', + smile.slight: '🙂', + smile.sweat: '😅', + smile.tear: '🥲', + smirk: '😏', + sneeze: '🤧', + speak.not: '🫢', + squint: '😆', + stars: '🤩', + straight: '😑', + suffer: '😖', + surprise: '😯', + symbols: '🤬', + tear: '😢', + tear.relief: '😥', + tear.withheld: '🥹', + teeth: '😬', + think: '🤔', + tired: '', + tongue: '😛', + tongue.squint: '😝', + tongue.wink: '😜', + triumph: '😤', + unhappy: '😕', + vomit: '🤮', + weary: '😩', + wink: '😉', + woozy: '🥴', + worry: '😟', + wow: '😮', + yawn: '🥱', + zip: '🤐', + ], + factory: '🏭', + fairy: '🧚', + faith: [ + christ: '✝', + dharma: '☸', + islam: '☪', + judaism: '✡', + menorah: '🕎', + om: '🕉', + orthodox: '☦', + peace: '☮', + star.dot: '🔯', + worship: '🛐', + yinyang: '☯', + ], + falafel: '🧆', + family: '👪', + fax: '📠', + feather: '🪶', + feeding: [breast: '🤱'], + fencing: '🤺', + ferriswheel: '🎡', + filebox: '🗃', + filedividers: '🗂', + film: '🎞', + finger: [ + r: '👉', + l: '👈', + t: '👆', + t.alt: '☝', + b: '👇', + front: '🫵', + m: '🖕', + ], + fingerprint: '', + fingers: [cross: '🤞', pinch: '🤌', snap: '🫰'], + fire: '🔥', + firecracker: '🧨', + fireengine: '🚒', + fireworks: '🎆', + fish: ['🐟', tropical: '🐠'], + fishing: '🎣', + fist: [ + front: '👊', + r: '🤜', + l: '🤛', + raised: '✊', + ], + flag: [ + black: '🏴', + white: '🏳', + goal: '🏁', + golf: '⛳', + red: '🚩', + ], + flags: [jp.crossed: '🎌'], + flamingo: '🦩', + flashlight: '🔦', + flatbread: '🫓', + fleur: '⚜', + floppy: '💾', + flower: [ + hibiscus: '🌺', + lotus: '🪷', + pink: '🌸', + rose: '🌹', + sun: '🌻', + tulip: '🌷', + white: '💮', + wilted: '🥀', + yellow: '🌼', + ], + fly: '🪰', + fog: '🌫', + folder: ['📁', open: '📂'], + fondue: '🫕', + foot: '🦶', + football: ['⚽', am: '🏈'], + forex: '💱', + fountain: '⛲', + fox: '🦊', + free: '🆓', + fries: '🍟', + frisbee: '🥏', + frog: [face: '🐸'], + fuelpump: '⛽', + garlic: '🧄', + gear: '⚙', + gem: '💎', + genie: '🧞', + ghost: '👻', + giraffe: '🦒', + girl: '👧', + glass: [ + clink: '🥂', + milk: '🥛', + pour: '🫗', + tumbler: '🥃', + ], + glasses: ['👓', sun: '🕶'], + globe: [ + am: '🌎', + as.au: '🌏', + eu.af: '🌍', + meridian: '🌐', + ], + gloves: '🧤', + goal: '🥅', + goat: '🐐', + goggles: '🥽', + golfing: '🏌', + gorilla: '🦍', + grapes: '🍇', + guard: [man: '💂'], + guitar: '🎸', + gymnastics: '🤸', + haircut: '💇', + hammer: ['🔨', pick: '⚒', wrench: '🛠'], + hamsa: '🪬', + hamster: [face: '🐹'], + hand: [ + raised: '✋', + raised.alt: '🤚', + r: '🫱', + l: '🫲', + t: '🫴', + b: '🫳', + ok: '👌', + call: '🤙', + love: '🤟', + part: '🖖', + peace: '✌', + pinch: '🤏', + rock: '🤘', + splay: '🖐', + wave: '👋', + write: '✍', + ], + handbag: '👜', + handball: '🤾', + handholding: [man.man: '👬', woman.man: '👫', woman.woman: '👭'], + hands: [ + folded: '🙏', + palms: '🤲', + clap: '👏', + heart: '🫶', + open: '👐', + raised: '🙌', + shake: '🤝', + ], + harp: '', + hash: '#', + hat: [ribbon: '👒', top: '🎩'], + headphone: '🎧', + heart: [ + '❤', + arrow: '💘', + beat: '💓', + black: '🖤', + blue: '💙', + box: '💟', + broken: '💔', + brown: '🤎', + double: '💕', + excl: '❣', + green: '💚', + grow: '💗', + orange: '🧡', + purple: '💜', + real: '🫀', + revolve: '💞', + ribbon: '💝', + spark: '💖', + white: '🤍', + yellow: '💛', + ], + hedgehog: '🦔', + helicopter: '🚁', + helix: '🧬', + helmet: [cross: '⛑', military: '🪖'], + hippo: '🦛', + hockey: '🏑', + hole: '🕳', + honey: '🍯', + hongbao: '🧧', + hook: '🪝', + horn: [postal: '📯'], + horse: [ + '🐎', + carousel: '🎠', + face: '🐴', + race: '🏇', + ], + hospital: '🏥', + hotdog: '🌭', + hotel: ['🏨', love: '🏩'], + hotspring: '♨', + hourglass: ['⌛', flow: '⏳'], + house: [ + '🏠', + derelict: '🏚', + garden: '🏡', + multiple: '🏘', + ], + hundred: '💯', + hut: '🛖', + ice: '🧊', + icecream: ['🍨', shaved: '🍧', soft: '🍦'], + icehockey: '🏒', + id: '🆔', + info: 'ℹ', + izakaya: '🏮', + jar: '🫙', + jeans: '👖', + jigsaw: '🧩', + joystick: '🕹', + juggling: '🤹', + juice: '🧃', + kaaba: '🕋', + kadomatsu: '🎍', + kangaroo: '🦘', + gachi: '🈷', + go: '🈴', + hi: '㊙', + ka: '🉑', + kachi: '🈹', + kara: '🈳', + kon: '🈲', + man: '🈵', + muryo: '🈚', + shin: '🈸', + shuku: '㊗', + toku: '🉐', + yo: '🈺', + yubi: '🈯', + yuryo: '🈶', + koko: '🈁', + sa: '🈂', + kebab: '🥙', + key: ['🔑', old: '🗝'], + keyboard: '⌨', + kiss: '💏', + kissmark: '💋', + kite: '🪁', + kiwi: '🥝', + knife: ['🔪', dagger: '🗡'], + knot: '🪢', + koala: '🐨', + koinobori: '🎏', + label: '🏷', + lacrosse: '🥍', + ladder: '🪜', + lamp: [diya: '🪔'], + laptop: '💻', + a: '🅰', + ab: '🆎', + b: '🅱', + cl: '🆑', + o: '🅾', + leaf: [ + clover.three: '☘', + clover.four: '🍀', + fall: '🍂', + herb: '🌿', + maple: '🍁', + wind: '🍃', + ], + leftluggage: '🛅', + leg: ['🦵', mech: '🦿'], + lemon: '🍋', + leopard: '🐆', + letter: [love: '💌'], + liberty: '🗽', + lightbulb: '💡', + lightning: '⚡', + lion: '🦁', + lipstick: '💄', + litter: ['🚮', not: '🚯'], + lizard: '🦎', + llama: '🦙', + lobster: '🦞', + lock: [ + '🔒', + key: '🔐', + open: '🔓', + pen: '🔏', + ], + lollipop: '🍭', + lotion: '🧴', + luggage: '🧳', + lungs: '🫁', + mage: '🧙', + magnet: '🧲', + magnify: [r: '🔎', l: '🔍'], + mahjong: [dragon.red: '🀄'], + mail: ['✉', arrow: '📩'], + mailbox: [ + closed.empty: '📪', + closed.full: '📫', + open.empty: '📭', + open.full: '📬', + ], + mammoth: '🦣', + man: [ + '👨', + box: '🚹', + crown: '🤴', + guapimao: '👲', + levitate: '🕴', + old: '👴', + pregnant: '🫃', + turban: '👳', + tuxedo: '🤵', + ], + mango: '🥭', + map: [world: '🗺', jp: '🗾'], + martialarts: '🥋', + masks: '🎭', + mate: '🧉', + matryoshka: '🪆', + meat: ['🥩', bone: '🍖'], + medal: [ + first: '🥇', + second: '🥈', + third: '🥉', + sports: '🏅', + military: '🎖', + ], + megaphone: ['📢', simple: '📣'], + melon: '🍈', + merperson: '🧜', + metro: 'Ⓜ', + microbe: '🦠', + microphone: ['🎤', studio: '🎙'], + microscope: '🔬', + milkyway: '🌌', + mirror: '🪞', + mixer: '🎛', + money: [ + bag: '💰', + dollar: '💵', + euro: '💶', + pound: '💷', + yen: '💴', + wings: '💸', + ], + monkey: [ + '🐒', + face: '🐵', + hear.not: '🙉', + see.not: '🙈', + speak.not: '🙊', + ], + moon: [ + crescent: '🌙', + full: '🌕', + full.face: '🌝', + new: '🌑', + new.face: '🌚', + wane.one: '🌖', + wane.two: '🌗', + wane.three.face: '🌜', + wane.three: '🌘', + wax.one: '🌒', + wax.two: '🌓', + wax.two.face: '🌛', + wax.three: '🌔', + ], + mortarboard: '🎓', + mosque: '🕌', + mosquito: '🦟', + motorcycle: '🏍', + motorway: '🛣', + mountain: [ + '⛰', + fuji: '🗻', + snow: '🏔', + sunrise: '🌄', + ], + mouse: ['🐁', face: '🐭'], + mousetrap: '🪤', + mouth: ['👄', bite: '🫦'], + moyai: '🗿', + museum: '🏛', + mushroom: '🍄', + musicalscore: '🎼', + nails: [polish: '💅'], + namebadge: '📛', + nazar: '🧿', + necktie: '👔', + needle: '🪡', + nest: [empty: '🪹', eggs: '🪺'], + new: '🆕', + newspaper: ['📰', rolled: '🗞'], + ng: '🆖', + ningyo: '🎎', + ninja: '🥷', + noentry: '⛔', + nose: '👃', + notebook: ['📓', deco: '📔'], + notepad: '🗒', + notes: ['🎵', triple: '🎶'], + numbers: '🔢', + octopus: '🐙', + office: '🏢', + oil: '🛢', + ok: '🆗', + olive: '🫒', + oni: '👹', + onion: '🧅', + orangutan: '🦧', + otter: '🦦', + owl: '🦉', + ox: '🐂', + oyster: '🦪', + package: '📦', + paella: '🥘', + page: ['📄', curl: '📃', pencil: '📝'], + pager: '📟', + pages: [tabs: '📑'], + painting: '🖼', + palette: '🎨', + pancakes: '🥞', + panda: '🐼', + parachute: '🪂', + park: '🏞', + parking: '🅿', + parrot: '🦜', + partalteration: '〽', + party: '🎉', + peach: '🍑', + peacock: '🦚', + peanuts: '🥜', + pear: '🍐', + pedestrian: ['🚶', not: '🚷'], + pen: [ball: '🖊', fountain: '🖋'], + pencil: '✏', + penguin: '🐧', + pepper: ['🫑', hot: '🌶'], + person: [ + '🧑', + angry: '🙎', + beard: '🧔', + blonde: '👱', + bow: '🙇', + crown: '🫅', + deaf: '🧏', + facepalm: '🤦', + frown: '🙍', + hijab: '🧕', + kneel: '🧎', + lotus: '🧘', + massage: '💆', + no: '🙅', + ok: '🙆', + old: '🧓', + pregnant: '🫄', + raise: '🙋', + sassy: '💁', + shrug: '🤷', + stand: '🧍', + steam: '🧖', + ], + petri: '🧫', + phone: [ + '📱', + arrow: '📲', + classic: '☎', + not: '📵', + off: '📴', + receiver: '📞', + signal: '📶', + vibrate: '📳', + ], + piano: '🎹', + pick: '⛏', + pie: '🥧', + pig: ['🐖', face: '🐷', nose: '🐽'], + pill: '💊', + pin: ['📌', round: '📍'], + pinata: '🪅', + pineapple: '🍍', + pingpong: '🏓', + pistol: '🔫', + pizza: '🍕', + placard: '🪧', + planet: '🪐', + plant: '🪴', + plaster: '🩹', + plate: [cutlery: '🍽'], + playback: [ + down: '⏬', + eject: '⏏', + forward: '⏩', + pause: '⏸', + record: '⏺', + repeat: '🔁', + repeat.once: '🔂', + repeat.v: '🔃', + restart: '⏮', + rewind: '⏪', + shuffle: '🔀', + skip: '⏭', + stop: '⏹', + toggle: '⏯', + up: '⏫', + ], + playingcard: [flower: '🎴', joker: '🃏'], + plunger: '🪠', + policeofficer: '👮', + poo: '💩', + popcorn: '🍿', + post: [eu: '🏤', jp: '🏣'], + postbox: '📮', + potato: ['🥔', sweet: '🍠'], + pouch: '👝', + powerplug: '🔌', + present: '🎁', + pretzel: '🥨', + printer: '🖨', + prints: [foot: '👣', paw: '🐾'], + prohibited: '🚫', + projector: '📽', + pumpkin: [lantern: '🎃'], + purse: '👛', + quest: ['❓', white: '❔'], + rabbit: ['🐇', face: '🐰'], + raccoon: '🦝', + radio: '📻', + radioactive: '☢', + railway: '🛤', + rainbow: '🌈', + ram: '🐏', + rat: '🐀', + razor: '🪒', + receipt: '🧾', + recycling: '♻', + reg: '®', + restroom: '🚻', + rhino: '🦏', + ribbon: ['🎀', remind: '🎗'], + rice: [ + '🍚', + cracker: '🍘', + ear: '🌾', + onigiri: '🍙', + ], + ring: '💍', + ringbuoy: '🛟', + robot: '🤖', + rock: '🪨', + rocket: '🚀', + rollercoaster: '🎢', + rosette: '🏵', + rugby: '🏉', + ruler: ['📏', triangle: '📐'], + running: '🏃', + safetypin: '🧷', + safetyvest: '🦺', + sake: '🍶', + salad: '🥗', + salt: '🧂', + sandwich: '🥪', + santa: [man: '🎅', woman: '🤶'], + satdish: '📡', + satellite: '🛰', + saw: '🪚', + saxophone: '🎷', + scales: '⚖', + scarf: '🧣', + school: '🏫', + scissors: '✂', + scooter: ['🛴', motor: '🛵'], + scorpion: '🦂', + screwdriver: '🪛', + scroll: '📜', + seal: '🦭', + seat: '💺', + seedling: '🌱', + shark: '🦈', + sheep: '🐑', + shell: [spiral: '🐚'], + shield: '🛡', + ship: ['🚢', cruise: '🛳', ferry: '⛴'], + shirt: [sports: '🎽', t: '👕'], + shoe: [ + '👞', + ballet: '🩰', + flat: '🥿', + heel: '👠', + hike: '🥾', + ice: '⛸', + roller: '🛼', + sandal.heel: '👡', + ski: '🎿', + sneaker: '👟', + tall: '👢', + thong: '🩴', + ], + shopping: '🛍', + shorts: '🩳', + shoshinsha: '🔰', + shovel: '', + shower: '🚿', + shrimp: ['🦐', fried: '🍤'], + shrine: '⛩', + sign: [crossing: '🚸', stop: '🛑'], + silhouette: [ + '👤', + double: '👥', + hug: '🫂', + speak: '🗣', + ], + siren: '🚨', + skateboard: '🛹', + skewer: [dango: '🍡', oden: '🍢'], + skiing: '⛷', + skull: ['💀', bones: '☠'], + skunk: '🦨', + sled: '🛷', + slide: '🛝', + slider: '🎚', + sloth: '🦥', + slots: '🎰', + snail: '🐌', + snake: '🐍', + snowboarding: '🏂', + snowflake: '❄', + snowman: ['⛄', snow: '☃'], + soap: '🧼', + socks: '🧦', + softball: '🥎', + sos: '🆘', + soup: '🍲', + spaghetti: '🍝', + sparkle: [box: '❇'], + sparkler: '🎇', + sparkles: '✨', + speaker: [ + '🔈', + not: '🔇', + wave: '🔉', + waves: '🔊', + ], + spider: '🕷', + spiderweb: '🕸', + spinach: '🥬', + splatter: '', + sponge: '🧽', + spoon: '🥄', + square: [ + black: '⬛', + black.tiny: '▪', + black.small: '◾', + black.medium: '◼', + white: '⬜', + white.tiny: '▫', + white.small: '◽', + white.medium: '◻', + blue: '🟦', + brown: '🟫', + green: '🟩', + orange: '🟧', + purple: '🟪', + red: '🟥', + yellow: '🟨', + ], + squid: '🦑', + stadium: '🏟', + star: [ + '⭐', + arc: '💫', + box: '✴', + glow: '🌟', + shoot: '🌠', + ], + stethoscope: '🩺', + store: [big: '🏬', small: '🏪'], + strawberry: '🍓', + suit: [ + club: '♣', + diamond: '♦', + heart: '♥', + spade: '♠', + ], + sun: ['☀', cloud: '🌤', face: '🌞'], + sunrise: '🌅', + superhero: '🦸', + supervillain: '🦹', + surfing: '🏄', + sushi: '🍣', + swan: '🦢', + swimming: '🏊', + swimsuit: '🩱', + swords: '⚔', + symbols: '🔣', + synagogue: '🕍', + syringe: '💉', + taco: '🌮', + takeout: '🥡', + tamale: '🫔', + tanabata: '🎋', + tangerine: '🍊', + tap: ['🚰', not: '🚱'], + taxi: ['🚕', front: '🚖'], + teacup: '🍵', + teapot: '🫖', + teddy: '🧸', + telescope: '🔭', + temple: '🛕', + ten: '🔟', + tengu: '👺', + tennis: '🎾', + tent: '⛺', + testtube: '🧪', + thermometer: '🌡', + thread: '🧵', + thumb: [up: '👍', down: '👎'], + ticket: [event: '🎟', travel: '🎫'], + tiger: ['🐅', face: '🐯'], + tm: '™', + toilet: '🚽', + toiletpaper: '🧻', + tomato: '🍅', + tombstone: '🪦', + tongue: '👅', + toolbox: '🧰', + tooth: '🦷', + toothbrush: '🪥', + tornado: '🌪', + tower: [tokyo: '🗼'], + trackball: '🖲', + tractor: '🚜', + trafficlight: [v: '🚦', h: '🚥'], + train: [ + '🚆', + car: '🚃', + light: '🚈', + metro: '🚇', + mono: '🚝', + mountain: '🚞', + speed: '🚄', + speed.bullet: '🚅', + steam: '🚂', + stop: '🚉', + suspend: '🚟', + tram: '🚊', + tram.car: '🚋', + ], + transgender: '⚧', + tray: [inbox: '📥', mail: '📨', outbox: '📤'], + tree: [ + deciduous: '🌳', + evergreen: '🌲', + leafless: '', + palm: '🌴', + xmas: '🎄', + ], + triangle: [ + r: '▶', + l: '◀', + t: '🔼', + b: '🔽', + t.red: '🔺', + b.red: '🔻', + ], + trident: '🔱', + troll: '🧌', + trophy: '🏆', + truck: ['🚚', trailer: '🚛'], + trumpet: '🎺', + tsukimi: '🎑', + turkey: '🦃', + turtle: '🐢', + tv: '📺', + ufo: '🛸', + umbrella: [ + open: '☂', + closed: '🌂', + rain: '☔', + sun: '⛱', + ], + unicorn: '🦄', + unknown: '🦳', + up: '🆙', + urn: '⚱', + vampire: '🧛', + violin: '🎻', + volcano: '🌋', + volleyball: '🏐', + vs: '🆚', + waffle: '🧇', + wand: '🪄', + warning: '⚠', + watch: ['⌚', stop: '⏱'], + watermelon: '🍉', + waterpolo: '🤽', + wave: '🌊', + wc: '🚾', + weightlifting: '🏋', + whale: ['🐋', spout: '🐳'], + wheel: '🛞', + wheelchair: ['🦽', box: '♿', motor: '🦼'], + wind: '🌬', + windchime: '🎐', + window: '🪟', + wine: '🍷', + wolf: '🐺', + woman: [ + '👩', + box: '🚺', + crown: '👸', + old: '👵', + pregnant: '🤰', + ], + wood: '🪵', + worm: '🪱', + wrench: '🔧', + wrestling: '🤼', + xray: '🩻', + yarn: '🧶', + yoyo: '🪀', + zebra: '🦓', + zodiac: [ + aquarius: '♒', + aries: '♈', + cancer: '♋', + capri: '♑', + gemini: '♊', + leo: '♌', + libra: '♎', + ophi: '⛎', + pisces: '♓', + sagit: '♐', + scorpio: '♏', + taurus: '♉', + virgo: '♍', + ], + zombie: '🧟', + zzz: '💤', +}; diff --git a/crates/typst-library/src/symbols/mod.rs b/crates/typst-library/src/symbols/mod.rs new file mode 100644 index 00000000..7e0fb55b --- /dev/null +++ b/crates/typst-library/src/symbols/mod.rs @@ -0,0 +1,24 @@ +//! Modifiable symbols. + +mod emoji; +mod sym; + +pub use self::emoji::*; +pub use self::sym::*; + +use crate::foundations::{category, Category, Scope}; + +/// These two modules give names to symbols and emoji to make them easy to +/// insert with a normal keyboard. Alternatively, you can also always directly +/// enter Unicode symbols into your text and formulas. In addition to the +/// symbols listed below, math mode defines `dif` and `Dif`. These are not +/// normal symbol values because they also affect spacing and font style. +#[category] +pub static SYMBOLS: Category; + +/// Hook up all `symbol` definitions. +pub(super) fn define(global: &mut Scope) { + global.category(SYMBOLS); + global.define_module(sym()); + global.define_module(emoji()); +} diff --git a/crates/typst-library/src/symbols/sym.rs b/crates/typst-library/src/symbols/sym.rs new file mode 100644 index 00000000..606e44ea --- /dev/null +++ b/crates/typst-library/src/symbols/sym.rs @@ -0,0 +1,995 @@ +use crate::foundations::{Module, Scope, Symbol}; + +/// A module with all general symbols. +pub fn sym() -> Module { + let mut scope = Scope::new(); + for (name, symbol) in SYM { + scope.define(*name, symbol.clone()); + } + Module::new("sym", scope) +} + +/// The list of general symbols. +pub(crate) const SYM: &[(&str, Symbol)] = typst_macros::symbols! { + // Control. + wj: '\u{2060}', + zwj: '\u{200D}', + zwnj: '\u{200C}', + zws: '\u{200B}', + lrm: '\u{200E}', + rlm: '\u{200F}', + + // Spaces. + space: [ + ' ', + nobreak: '\u{A0}', + nobreak.narrow: '\u{202F}', + en: '\u{2002}', + quad: '\u{2003}', + third: '\u{2004}', + quarter: '\u{2005}', + sixth: '\u{2006}', + med: '\u{205F}', + fig: '\u{2007}', + punct: '\u{2008}', + thin: '\u{2009}', + hair: '\u{200A}', + ], + + // Delimiters. + paren: [l: '(', l.double: '⦅', r: ')', r.double: '⦆', t: '⏜', b: '⏝'], + brace: [l: '{', l.double: '⦃', r: '}', r.double: '⦄', t: '⏞', b: '⏟'], + bracket: [l: '[', l.double: '⟦', r: ']', r.double: '⟧', t: '⎴', b: '⎵'], + shell: [l: '❲', l.double: '⟬', r: '❳', r.double: '⟭', t: '⏠', b: '⏡'], + bar: [v: '|', v.double: '‖', v.triple: '⦀', v.broken: '¦', v.circle: '⦶', h: '―'], + fence: [l: '⧘', l.double: '⧚', r: '⧙', r.double: '⧛', dotted: '⦙'], + angle: [ + '∠', + l: '⟨', + l.curly: '⧼', + l.dot: '⦑', + l.double: '《', + r: '⟩', + r.curly: '⧽', + r.dot: '⦒', + r.double: '》', + acute: '⦟', + arc: '∡', + arc.rev: '⦛', + oblique: '⦦', + rev: '⦣', + right: '∟', + right.rev: '⯾', + right.arc: '⊾', + right.dot: '⦝', + right.sq: '⦜', + s: '⦞', + spatial: '⟀', + spheric: '∢', + spheric.rev: '⦠', + spheric.top: '⦡', + ], + ceil: [ + #[call(crate::math::ceil)] l: '⌈', + r: '⌉', + ], + floor: [ + #[call(crate::math::floor)] l: '⌊', + r: '⌋', + ], + + // Punctuation. + amp: ['&', inv: '⅋'], + ast: [ + op: '∗', + basic: '*', + low: '⁎', + double: '⁑', + triple: '⁂', + small: '﹡', + circle: '⊛', + square: '⧆', + ], + at: '@', + backslash: ['\\', circle: '⦸', not: '⧷'], + co: '℅', + colon: [':', double: '∷', eq: '≔', double.eq: '⩴'], + comma: ',', + dagger: ['†', double: '‡'], + dash: [ + #[call(crate::math::accent::dash)] en: '–', + em: '—', + em.two: '⸺', + em.three: '⸻', + fig: '‒', + wave: '〜', + colon: '∹', + circle: '⊝', + wave.double: '〰', + ], + dot: [ + #[call(crate::math::accent::dot)] op: '⋅', + basic: '.', + c: '·', + circle: '⊙', + circle.big: '⨀', + square: '⊡', + #[call(crate::math::accent::dot_double)] double: '¨', + #[call(crate::math::accent::dot_triple)] triple: '\u{20db}', + #[call(crate::math::accent::dot_quad)] quad: '\u{20dc}', + ], + excl: ['!', double: '‼', inv: '¡', quest: '⁉'], + quest: ['?', double: '⁇', excl: '⁈', inv: '¿'], + interrobang: '‽', + hash: '#', + hyph: ['‐', minus: '\u{2D}', nobreak: '\u{2011}', point: '‧', soft: '\u{ad}'], + percent: '%', + permille: '‰', + pilcrow: ['¶', rev: '⁋'], + section: '§', + semi: [';', rev: '⁏'], + slash: ['/', double: '⫽', triple: '⫻', big: '⧸'], + dots: [h.c: '⋯', h: '…', v: '⋮', down: '⋱', up: '⋰'], + tilde: [ + #[call(crate::math::accent::tilde)] op: '∼', + basic: '~', + dot: '⩪', + eq: '≃', + eq.not: '≄', + eq.rev: '⋍', + equiv: '≅', + equiv.not: '≇', + nequiv: '≆', + not: '≁', + rev: '∽', + rev.equiv: '≌', + triple: '≋', + ], + + // Accents, quotes, and primes. + acute: [ + #[call(crate::math::accent::acute)] '´', + #[call(crate::math::accent::acute_double)] double: '˝', + ], + breve: #[call(crate::math::accent::breve)] '˘', + caret: '‸', + caron: #[call(crate::math::accent::caron)] 'ˇ', + hat: #[call(crate::math::accent::hat)] '^', + diaer: #[call(crate::math::accent::dot_double)] '¨', + grave: #[call(crate::math::accent::grave)] '`', + macron: #[call(crate::math::accent::macron)] '¯', + quote: [ + double: '"', + single: '\'', + l.double: '“', + l.single: '‘', + r.double: '”', + r.single: '’', + angle.l.double: '«', + angle.l.single: '‹', + angle.r.double: '»', + angle.r.single: '›', + high.double: '‟', + high.single: '‛', + low.double: '„', + low.single: '‚', + ], + prime: [ + '′', + rev: '‵', + double: '″', + double.rev: '‶', + triple: '‴', + triple.rev: '‷', + quad: '⁗', + ], + + // https://en.wikipedia.org/wiki/List_of_mathematical_symbols_by_subject + // Arithmetic. + plus: [ + '+', + circle: '⊕', + circle.arrow: '⟴', + circle.big: '⨁', + dot: '∔', + double: '⧺', + minus: '±', + small: '﹢', + square: '⊞', + triangle: '⨹', + triple: '⧻', + ], + minus: [ + '−', + circle: '⊖', + dot: '∸', + plus: '∓', + square: '⊟', + tilde: '≂', + triangle: '⨺', + ], + div: ['÷', circle: '⨸'], + times: [ + '×', + big: '⨉', + circle: '⊗', + circle.big: '⨂', + div: '⋇', + three.l: '⋋', + three.r: '⋌', + l: '⋉', + r: '⋊', + square: '⊠', + triangle: '⨻', + ], + ratio: '∶', + + // Relations. + eq: [ + '=', + star: '≛', + circle: '⊜', + colon: '≕', + def: '≝', + delta: '≜', + equi: '≚', + est: '≙', + gt: '⋝', + lt: '⋜', + m: '≞', + not: '≠', + prec: '⋞', + quest: '≟', + small: '﹦', + succ: '⋟', + triple: '≡', + quad: '≣', + ], + gt: [ + '>', + circle: '⧁', + dot: '⋗', + approx: '⪆', + double: '≫', + eq: '≥', + eq.slant: '⩾', + eq.lt: '⋛', + eq.not: '≱', + equiv: '≧', + lt: '≷', + lt.not: '≹', + neq: '⪈', + napprox: '⪊', + nequiv: '≩', + not: '≯', + ntilde: '⋧', + small: '﹥', + tilde: '≳', + tilde.not: '≵', + tri: '⊳', + tri.eq: '⊵', + tri.eq.not: '⋭', + tri.not: '⋫', + triple: '⋙', + triple.nested: '⫸', + ], + lt: [ + '<', + circle: '⧀', + dot: '⋖', + approx: '⪅', + double: '≪', + eq: '≤', + eq.slant: '⩽' , + eq.gt: '⋚', + eq.not: '≰', + equiv: '≦', + gt: '≶', + gt.not: '≸', + neq: '⪇', + napprox: '⪉', + nequiv: '≨', + not: '≮', + ntilde: '⋦', + small: '﹤', + tilde: '≲', + tilde.not: '≴', + tri: '⊲', + tri.eq: '⊴', + tri.eq.not: '⋬', + tri.not: '⋪', + triple: '⋘', + triple.nested: '⫷', + ], + approx: ['≈', eq: '≊', not: '≉'], + prec: [ + '≺', + approx: '⪷', + curly.eq: '≼', + curly.eq.not: '⋠', + double: '⪻', + eq: '⪯', + equiv: '⪳', + napprox: '⪹', + neq: '⪱', + nequiv: '⪵', + not: '⊀', + ntilde: '⋨', + tilde: '≾', + ], + succ: [ + '≻', + approx: '⪸', + curly.eq: '≽', + curly.eq.not: '⋡', + double: '⪼', + eq: '⪰', + equiv: '⪴', + napprox: '⪺', + neq: '⪲', + nequiv: '⪶', + not: '⊁', + ntilde: '⋩', + tilde: '≿', + ], + equiv: ['≡', not: '≢'], + prop: '∝', + original: '⊶', + image: '⊷', + + // Set theory. + emptyset: [ + '∅', + arrow.r: '⦳', + arrow.l: '⦴', + bar: '⦱', + circle: '⦲', + rev: '⦰', + ], + nothing: [ + '∅', + arrow.r: '⦳', + arrow.l: '⦴', + bar: '⦱', + circle: '⦲', + rev: '⦰', + ], + without: '∖', + complement: '∁', + in: [ + '∈', + not: '∉', + rev: '∋', + rev.not: '∌', + rev.small: '∍', + small: '∊', + ], + subset: [ + '⊂', + dot: '⪽', + double: '⋐', + eq: '⊆', + eq.not: '⊈', + eq.sq: '⊑', + eq.sq.not: '⋢', + neq: '⊊', + not: '⊄', + sq: '⊏', + sq.neq: '⋤', + ], + supset: [ + '⊃', + dot: '⪾', + double: '⋑', + eq: '⊇', + eq.not: '⊉', + eq.sq: '⊒', + eq.sq.not: '⋣', + neq: '⊋', + not: '⊅', + sq: '⊐', + sq.neq: '⋥', + ], + union: [ + '∪', + arrow: '⊌', + big: '⋃', + dot: '⊍', + dot.big: '⨃', + double: '⋓', + minus: '⩁', + or: '⩅', + plus: '⊎', + plus.big: '⨄', + sq: '⊔', + sq.big: '⨆', + sq.double: '⩏', + ], + sect: [ + '∩', + and: '⩄', + big: '⋂', + dot: '⩀', + double: '⋒', + sq: '⊓', + sq.big: '⨅', + sq.double: '⩎', + ], + + // Calculus. + infinity: [ + '∞', + bar: '⧞', + incomplete: '⧜', + tie: '⧝', + ], + oo: '∞', + diff: '∂', // Deprecation planned + partial: '∂', + gradient: '∇', + nabla: '∇', + sum: ['∑', integral: '⨋'], + product: ['∏', co: '∐'], + integral: [ + '∫', + arrow.hook: '⨗', + ccw: '⨑', + cont: '∮', + cont.ccw: '∳', + cont.cw: '∲', + cw: '∱', + dash: '⨍', + dash.double: '⨎', + double: '∬', + quad: '⨌', + sect: '⨙', + slash: '⨏', + square: '⨖', + surf: '∯', + times: '⨘', + triple: '∭', + union: '⨚', + vol: '∰', + ], + laplace: '∆', + + // Logic. + forall: '∀', + exists: ['∃', not: '∄'], + top: '⊤', + bot: '⊥', + not: '¬', + and: ['∧', big: '⋀', curly: '⋏', dot: '⟑', double: '⩓'], + or: ['∨', big: '⋁', curly: '⋎', dot: '⟇', double: '⩔'], + xor: ['⊕', big: '⨁'], + models: '⊧', + forces: ['⊩', not: '⊮'], + therefore: '∴', + because: '∵', + qed: '∎', + + // Function and category theory. + compose: '∘', + convolve: '∗', + multimap: ['⊸', double: '⧟'], + + // Game theory. + tiny: '⧾', + miny: '⧿', + + // Number theory. + divides: ['∣', not: '∤'], + + // Algebra. + wreath: '≀', + + // Geometry. + parallel: [ + '∥', + struck: '⫲', + circle: '⦷', + eq: '⋕', + equiv: '⩨', + not: '∦', + slanted.eq: '⧣', + slanted.eq.tilde: '⧤', + slanted.equiv: '⧥', + tilde: '⫳', + ], + perp: ['⟂', circle: '⦹'], + + // Miscellaneous Technical. + diameter: '⌀', + join: ['⨝', r: '⟖', l: '⟕', l.r: '⟗'], + degree: ['°', c: '℃', f: '℉'], + smash: '⨳', + + // Currency. + bitcoin: '₿', + dollar: '$', + euro: '€', + franc: '₣', + lira: '₺', + peso: '₱', + pound: '£', + ruble: '₽', + rupee: '₹', + won: '₩', + yen: '¥', + + // Miscellaneous. + ballot: ['☐', cross: '☒', check: '☑', check.heavy: '🗹'], + checkmark: ['✓', light: '🗸', heavy: '✔'], + crossmark: ['✗', heavy: '✘'], + floral: ['❦', l: '☙', r: '❧'], + refmark: '※', + copyright: ['©', sound: '℗'], + copyleft: '🄯', + trademark: ['™', registered: '®', service: '℠'], + maltese: '✠', + suit: [ + club.filled: '♣', + club.stroked: '♧', + diamond.filled: '♦', + diamond.stroked: '♢', + heart.filled: '♥', + heart.stroked: '♡', + spade.filled: '♠', + spade.stroked: '♤', + ], + + // Music. + note: [ + up: '🎜', + down: '🎝', + whole: '𝅝', + half: '𝅗𝅥', + quarter: '𝅘𝅥', + quarter.alt: '♩', + eighth: '𝅘𝅥𝅮', + eighth.alt: '♪', + eighth.beamed: '♫', + sixteenth: '𝅘𝅥𝅯', + sixteenth.beamed: '♬', + grace: '𝆕', + grace.slash: '𝆔', + ], + rest: [ + whole: '𝄻', + multiple: '𝄺', + multiple.measure: '𝄩', + half: '𝄼', + quarter: '𝄽', + eighth: '𝄾', + sixteenth: '𝄿', + ], + natural: [ + '♮', + t: '𝄮', + b: '𝄯', + ], + flat: [ + '♭', + t: '𝄬', + b: '𝄭', + double: '𝄫', + quarter: '𝄳', + ], + sharp: [ + '♯', + t: '𝄰', + b: '𝄱', + double: '𝄪', + quarter: '𝄲', + ], + + // Shapes. + bullet: '•', + circle: [ + #[call(crate::math::accent::circle)] stroked: '○', + stroked.tiny: '∘', + stroked.small: '⚬', + stroked.big: '◯', + filled: '●', + filled.tiny: '⦁', + filled.small: '∙', + filled.big: '⬤', + dotted: '◌', + nested: '⊚', + ], + ellipse: [ + stroked.h: '⬭', + stroked.v: '⬯', + filled.h: '⬬', + filled.v: '⬮', + ], + triangle: [ + stroked.t: '△', + stroked.b: '▽', + stroked.r: '▷', + stroked.l: '◁', + stroked.bl: '◺', + stroked.br: '◿', + stroked.tl: '◸', + stroked.tr: '◹', + stroked.small.t: '▵', + stroked.small.b: '▿', + stroked.small.r: '▹', + stroked.small.l: '◃', + stroked.rounded: '🛆', + stroked.nested: '⟁', + stroked.dot: '◬', + filled.t: '▲', + filled.b: '▼', + filled.r: '▶', + filled.l: '◀', + filled.bl: '◣', + filled.br: '◢', + filled.tl: '◤', + filled.tr: '◥', + filled.small.t: '▴', + filled.small.b: '▾', + filled.small.r: '▸', + filled.small.l: '◂', + ], + square: [ + stroked: '□', + stroked.tiny: '▫', + stroked.small: '◽', + stroked.medium: '◻', + stroked.big: '⬜', + stroked.dotted: '⬚', + stroked.rounded: '▢', + filled: '■', + filled.tiny: '▪', + filled.small: '◾', + filled.medium: '◼', + filled.big: '⬛', + ], + rect: [ + stroked.h: '▭', + stroked.v: '▯', + filled.h: '▬', + filled.v: '▮', + ], + penta: [stroked: '⬠', filled: '⬟'], + hexa: [stroked: '⬡', filled: '⬢'], + diamond: [ + stroked: '◇', + stroked.small: '⋄', + stroked.medium: '⬦', + stroked.dot: '⟐', + filled: '◆', + filled.medium: '⬥', + filled.small: '⬩', + ], + lozenge: [ + stroked: '◊', + stroked.small: '⬫', + stroked.medium: '⬨', + filled: '⧫', + filled.small: '⬪', + filled.medium: '⬧', + ], + parallelogram: [ + stroked: '▱', + filled: '▰', + ], + star: [op: '⋆', stroked: '☆', filled: '★'], + + // Arrows, harpoons, and tacks. + arrow: [ + #[call(crate::math::accent::arrow)] r: '→', + r.long.bar: '⟼', + r.bar: '↦', + r.curve: '⤷', + r.turn: '⮎', + r.dashed: '⇢', + r.dotted: '⤑', + r.double: '⇒', + r.double.bar: '⤇', + r.double.long: '⟹', + r.double.long.bar: '⟾', + r.double.not: '⇏', + r.filled: '➡', + r.hook: '↪', + r.long: '⟶', + r.long.squiggly: '⟿', + r.loop: '↬', + r.not: '↛', + r.quad: '⭆', + r.squiggly: '⇝', + r.stop: '⇥', + r.stroked: '⇨', + r.tail: '↣', + r.tilde: '⥲', + r.triple: '⇛', + r.twohead.bar: '⤅', + r.twohead: '↠', + r.wave: '↝', + #[call(crate::math::accent::arrow_l)] l: '←', + l.bar: '↤', + l.curve: '⤶', + l.turn: '⮌', + l.dashed: '⇠', + l.dotted: '⬸', + l.double: '⇐', + l.double.bar: '⤆', + l.double.long: '⟸', + l.double.long.bar: '⟽', + l.double.not: '⇍', + l.filled: '⬅', + l.hook: '↩', + l.long: '⟵', + l.long.bar: '⟻', + l.long.squiggly: '⬳', + l.loop: '↫', + l.not: '↚', + l.quad: '⭅', + l.squiggly: '⇜', + l.stop: '⇤', + l.stroked: '⇦', + l.tail: '↢', + l.tilde: '⭉', + l.triple: '⇚', + l.twohead.bar: '⬶', + l.twohead: '↞', + l.wave: '↜', + t: '↑', + t.bar: '↥', + t.curve: '⤴', + t.turn: '⮍', + t.dashed: '⇡', + t.double: '⇑', + t.filled: '⬆', + t.quad: '⟰', + t.stop: '⤒', + t.stroked: '⇧', + t.triple: '⤊', + t.twohead: '↟', + b: '↓', + b.bar: '↧', + b.curve: '⤵', + b.turn: '⮏', + b.dashed: '⇣', + b.double: '⇓', + b.filled: '⬇', + b.quad: '⟱', + b.stop: '⤓', + b.stroked: '⇩', + b.triple: '⤋', + b.twohead: '↡', + #[call(crate::math::accent::arrow_l_r)] l.r: '↔', + l.r.double: '⇔', + l.r.double.long: '⟺', + l.r.double.not: '⇎', + l.r.filled: '⬌', + l.r.long: '⟷', + l.r.not: '↮', + l.r.stroked: '⬄', + l.r.wave: '↭', + t.b: '↕', + t.b.double: '⇕', + t.b.filled: '⬍', + t.b.stroked: '⇳', + tr: '↗', + tr.double: '⇗', + tr.filled: '⬈', + tr.hook: '⤤', + tr.stroked: '⬀', + br: '↘', + br.double: '⇘', + br.filled: '⬊', + br.hook: '⤥', + br.stroked: '⬂', + tl: '↖', + tl.double: '⇖', + tl.filled: '⬉', + tl.hook: '⤣', + tl.stroked: '⬁', + bl: '↙', + bl.double: '⇙', + bl.filled: '⬋', + bl.hook: '⤦', + bl.stroked: '⬃', + tl.br: '⤡', + tr.bl: '⤢', + ccw: '↺', + ccw.half: '↶', + cw: '↻', + cw.half: '↷', + zigzag: '↯', + ], + arrows: [ + rr: '⇉', + ll: '⇇', + tt: '⇈', + bb: '⇊', + lr: '⇆', + lr.stop: '↹', + rl: '⇄', + tb: '⇅', + bt: '⇵', + rrr: '⇶', + lll: '⬱', + ], + arrowhead: [ + t: '⌃', + b: '⌄', + ], + harpoon: [ + #[call(crate::math::accent::harpoon)] rt: '⇀', + rt.bar: '⥛', + rt.stop: '⥓', + rb: '⇁', + rb.bar: '⥟', + rb.stop: '⥗', + #[call(crate::math::accent::harpoon_lt)] lt: '↼', + lt.bar: '⥚', + lt.stop: '⥒', + lb: '↽', + lb.bar: '⥞', + lb.stop: '⥖', + tl: '↿', + tl.bar: '⥠', + tl.stop: '⥘', + tr: '↾', + tr.bar: '⥜', + tr.stop: '⥔', + bl: '⇃', + bl.bar: '⥡', + bl.stop: '⥙', + br: '⇂', + br.bar: '⥝', + br.stop: '⥕', + lt.rt: '⥎', + lb.rb: '⥐', + lb.rt: '⥋', + lt.rb: '⥊', + tl.bl: '⥑', + tr.br: '⥏', + tl.br: '⥍', + tr.bl: '⥌', + ], + harpoons: [ + rtrb: '⥤', + blbr: '⥥', + bltr: '⥯', + lbrb: '⥧', + ltlb: '⥢', + ltrb: '⇋', + ltrt: '⥦', + rblb: '⥩', + rtlb: '⇌', + rtlt: '⥨', + tlbr: '⥮', + tltr: '⥣', + ], + tack: [ + r: '⊢', + r.not: '⊬', + r.long: '⟝', + r.short: '⊦', + r.double: '⊨', + r.double.not: '⊭', + l: '⊣', + l.long: '⟞', + l.short: '⫞', + l.double: '⫤', + t: '⊥', + t.big: '⟘', + t.double: '⫫', + t.short: '⫠', + b: '⊤', + b.big: '⟙', + b.double: '⫪', + b.short: '⫟', + l.r: '⟛', + ], + + // Lowercase Greek. + alpha: 'α', + beta: ['β', alt: 'ϐ'], + chi: 'χ', + delta: 'δ', + epsilon: ['ε', alt: 'ϵ'], + eta: 'η', + gamma: 'γ', + iota: 'ι', + kai: 'ϗ', + kappa: ['κ', alt: 'ϰ'], + lambda: 'λ', + mu: 'μ', + nu: 'ν', + ohm: ['Ω', inv: '℧'], + omega: 'ω', + omicron: 'ο', + phi: ['φ', alt: 'ϕ'], + pi: ['π', alt: 'ϖ'], + psi: 'ψ', + rho: ['ρ', alt: 'ϱ'], + sigma: ['σ', alt: 'ς'], + tau: 'τ', + theta: ['θ', alt: 'ϑ'], + upsilon: 'υ', + xi: 'ξ', + zeta: 'ζ', + + // Uppercase Greek. + Alpha: 'Α', + Beta: 'Β', + Chi: 'Χ', + Delta: 'Δ', + Epsilon: 'Ε', + Eta: 'Η', + Gamma: 'Γ', + Iota: 'Ι', + Kai: 'Ϗ', + Kappa: 'Κ', + Lambda: 'Λ', + Mu: 'Μ', + Nu: 'Ν', + Omega: 'Ω', + Omicron: 'Ο', + Phi: 'Φ', + Pi: 'Π', + Psi: 'Ψ', + Rho: 'Ρ', + Sigma: 'Σ', + Tau: 'Τ', + Theta: 'Θ', + Upsilon: 'Υ', + Xi: 'Ξ', + Zeta: 'Ζ', + + // Hebrew. + // In math, the following symbols are replaced with corresponding characters + // from Letterlike Symbols. + // See https://github.com/typst/typst/pull/3375. + aleph: 'א', + alef: 'א', + beth: 'ב', + bet: 'ב', + gimmel: 'ג', + gimel: 'ג', + daleth: 'ד', + dalet: 'ד', + shin: 'ש', + + // Double-struck. + AA: '𝔸', + BB: '𝔹', + CC: 'ℂ', + DD: '𝔻', + EE: '𝔼', + FF: '𝔽', + GG: '𝔾', + HH: 'ℍ', + II: '𝕀', + JJ: '𝕁', + KK: '𝕂', + LL: '𝕃', + MM: '𝕄', + NN: 'ℕ', + OO: '𝕆', + PP: 'ℙ', + QQ: 'ℚ', + RR: 'ℝ', + SS: '𝕊', + TT: '𝕋', + UU: '𝕌', + VV: '𝕍', + WW: '𝕎', + XX: '𝕏', + YY: '𝕐', + ZZ: 'ℤ', + + // Miscellaneous letter-likes. + ell: 'ℓ', + planck: ['ℎ', reduce: 'ℏ'], + angstrom: 'Å', + kelvin: 'K', + Re: 'ℜ', + Im: 'ℑ', + dotless: [i: '𝚤', j: '𝚥'], +}; diff --git a/crates/typst-library/src/text/case.rs b/crates/typst-library/src/text/case.rs new file mode 100644 index 00000000..69dbf5e1 --- /dev/null +++ b/crates/typst-library/src/text/case.rs @@ -0,0 +1,79 @@ +use crate::foundations::{cast, func, Cast, Content, Str}; +use crate::text::TextElem; + +/// Converts a string or content to lowercase. +/// +/// # Example +/// ```example +/// #lower("ABC") \ +/// #lower[*My Text*] \ +/// #lower[already low] +/// ``` +#[func(title = "Lowercase")] +pub fn lower( + /// The text to convert to lowercase. + text: Caseable, +) -> Caseable { + case(text, Case::Lower) +} + +/// Converts a string or content to uppercase. +/// +/// # Example +/// ```example +/// #upper("abc") \ +/// #upper[*my text*] \ +/// #upper[ALREADY HIGH] +/// ``` +#[func(title = "Uppercase")] +pub fn upper( + /// The text to convert to uppercase. + text: Caseable, +) -> Caseable { + case(text, Case::Upper) +} + +/// Change the case of text. +fn case(text: Caseable, case: Case) -> Caseable { + match text { + Caseable::Str(v) => Caseable::Str(case.apply(&v).into()), + Caseable::Content(v) => { + Caseable::Content(v.styled(TextElem::set_case(Some(case)))) + } + } +} + +/// A value whose case can be changed. +pub enum Caseable { + Str(Str), + Content(Content), +} + +cast! { + Caseable, + self => match self { + Self::Str(v) => v.into_value(), + Self::Content(v) => v.into_value(), + }, + v: Str => Self::Str(v), + v: Content => Self::Content(v), +} + +/// A case transformation on text. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum Case { + /// Everything is lowercased. + Lower, + /// Everything is uppercased. + Upper, +} + +impl Case { + /// Apply the case to a string. + pub fn apply(self, text: &str) -> String { + match self { + Self::Lower => text.to_lowercase(), + Self::Upper => text.to_uppercase(), + } + } +} diff --git a/crates/typst-library/src/text/deco.rs b/crates/typst-library/src/text/deco.rs new file mode 100644 index 00000000..5da7ecec --- /dev/null +++ b/crates/typst-library/src/text/deco.rs @@ -0,0 +1,401 @@ +use smallvec::smallvec; + +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{elem, Content, Packed, Show, Smart, StyleChain}; +use crate::layout::{Abs, Corners, Length, Rel, Sides}; +use crate::text::{BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric}; +use crate::visualize::{Color, FixedStroke, Paint, Stroke}; + +/// Underlines text. +/// +/// # Example +/// ```example +/// This is #underline[important]. +/// ``` +#[elem(Show)] +pub struct UnderlineElem { + /// How to [stroke] the line. + /// + /// If set to `{auto}`, takes on the text's color and a thickness defined in + /// the current font. + /// + /// ```example + /// Take #underline( + /// stroke: 1.5pt + red, + /// offset: 2pt, + /// [care], + /// ) + /// ``` + #[resolve] + #[fold] + pub stroke: Smart<Stroke>, + + /// The position of the line relative to the baseline, read from the font + /// tables if `{auto}`. + /// + /// ```example + /// #underline(offset: 5pt)[ + /// The Tale Of A Faraway Line I + /// ] + /// ``` + #[resolve] + pub offset: Smart<Length>, + + /// The amount by which to extend the line beyond (or within if negative) + /// the content. + /// + /// ```example + /// #align(center, + /// underline(extent: 2pt)[Chapter 1] + /// ) + /// ``` + #[resolve] + pub extent: Length, + + /// Whether the line skips sections in which it would collide with the + /// glyphs. + /// + /// ```example + /// This #underline(evade: true)[is great]. + /// This #underline(evade: false)[is less great]. + /// ``` + #[default(true)] + pub evade: bool, + + /// Whether the line is placed behind the content it underlines. + /// + /// ```example + /// #set underline(stroke: (thickness: 1em, paint: maroon, cap: "round")) + /// #underline(background: true)[This is stylized.] \ + /// #underline(background: false)[This is partially hidden.] + /// ``` + #[default(false)] + pub background: bool, + + /// The content to underline. + #[required] + pub body: Content, +} + +impl Show for Packed<UnderlineElem> { + #[typst_macros::time(name = "underline", span = self.span())] + fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + Ok(self.body().clone().styled(TextElem::set_deco(smallvec![Decoration { + line: DecoLine::Underline { + stroke: self.stroke(styles).unwrap_or_default(), + offset: self.offset(styles), + evade: self.evade(styles), + background: self.background(styles), + }, + extent: self.extent(styles), + }]))) + } +} + +/// Adds a line over text. +/// +/// # Example +/// ```example +/// #overline[A line over text.] +/// ``` +#[elem(Show)] +pub struct OverlineElem { + /// How to [stroke] the line. + /// + /// If set to `{auto}`, takes on the text's color and a thickness defined in + /// the current font. + /// + /// ```example + /// #set text(fill: olive) + /// #overline( + /// stroke: green.darken(20%), + /// offset: -12pt, + /// [The Forest Theme], + /// ) + /// ``` + #[resolve] + #[fold] + pub stroke: Smart<Stroke>, + + /// The position of the line relative to the baseline. Read from the font + /// tables if `{auto}`. + /// + /// ```example + /// #overline(offset: -1.2em)[ + /// The Tale Of A Faraway Line II + /// ] + /// ``` + #[resolve] + pub offset: Smart<Length>, + + /// The amount by which to extend the line beyond (or within if negative) + /// the content. + /// + /// ```example + /// #set overline(extent: 4pt) + /// #set underline(extent: 4pt) + /// #overline(underline[Typography Today]) + /// ``` + #[resolve] + pub extent: Length, + + /// Whether the line skips sections in which it would collide with the + /// glyphs. + /// + /// ```example + /// #overline( + /// evade: false, + /// offset: -7.5pt, + /// stroke: 1pt, + /// extent: 3pt, + /// [Temple], + /// ) + /// ``` + #[default(true)] + pub evade: bool, + + /// Whether the line is placed behind the content it overlines. + /// + /// ```example + /// #set overline(stroke: (thickness: 1em, paint: maroon, cap: "round")) + /// #overline(background: true)[This is stylized.] \ + /// #overline(background: false)[This is partially hidden.] + /// ``` + #[default(false)] + pub background: bool, + + /// The content to add a line over. + #[required] + pub body: Content, +} + +impl Show for Packed<OverlineElem> { + #[typst_macros::time(name = "overline", span = self.span())] + fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + Ok(self.body().clone().styled(TextElem::set_deco(smallvec![Decoration { + line: DecoLine::Overline { + stroke: self.stroke(styles).unwrap_or_default(), + offset: self.offset(styles), + evade: self.evade(styles), + background: self.background(styles), + }, + extent: self.extent(styles), + }]))) + } +} + +/// Strikes through text. +/// +/// # Example +/// ```example +/// This is #strike[not] relevant. +/// ``` +#[elem(title = "Strikethrough", Show)] +pub struct StrikeElem { + /// How to [stroke] the line. + /// + /// If set to `{auto}`, takes on the text's color and a thickness defined in + /// the current font. + /// + /// _Note:_ Please don't use this for real redaction as you can still copy + /// paste the text. + /// + /// ```example + /// This is #strike(stroke: 1.5pt + red)[very stricken through]. \ + /// This is #strike(stroke: 10pt)[redacted]. + /// ``` + #[resolve] + #[fold] + pub stroke: Smart<Stroke>, + + /// The position of the line relative to the baseline. Read from the font + /// tables if `{auto}`. + /// + /// This is useful if you are unhappy with the offset your font provides. + /// + /// ```example + /// #set text(font: "Inria Serif") + /// This is #strike(offset: auto)[low-ish]. \ + /// This is #strike(offset: -3.5pt)[on-top]. + /// ``` + #[resolve] + pub offset: Smart<Length>, + + /// The amount by which to extend the line beyond (or within if negative) + /// the content. + /// + /// ```example + /// This #strike(extent: -2pt)[skips] parts of the word. + /// This #strike(extent: 2pt)[extends] beyond the word. + /// ``` + #[resolve] + pub extent: Length, + + /// Whether the line is placed behind the content. + /// + /// ```example + /// #set strike(stroke: red) + /// #strike(background: true)[This is behind.] \ + /// #strike(background: false)[This is in front.] + /// ``` + #[default(false)] + pub background: bool, + + /// The content to strike through. + #[required] + pub body: Content, +} + +impl Show for Packed<StrikeElem> { + #[typst_macros::time(name = "strike", span = self.span())] + fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + Ok(self.body().clone().styled(TextElem::set_deco(smallvec![Decoration { + // Note that we do not support evade option for strikethrough. + line: DecoLine::Strikethrough { + stroke: self.stroke(styles).unwrap_or_default(), + offset: self.offset(styles), + background: self.background(styles), + }, + extent: self.extent(styles), + }]))) + } +} + +/// Highlights text with a background color. +/// +/// # Example +/// ```example +/// This is #highlight[important]. +/// ``` +#[elem(Show)] +pub struct HighlightElem { + /// The color to highlight the text with. + /// + /// ```example + /// This is #highlight( + /// fill: blue + /// )[highlighted with blue]. + /// ``` + #[default(Some(Color::from_u8(0xFF, 0xFD, 0x11, 0xA1).into()))] + pub fill: Option<Paint>, + + /// The highlight's border color. See the + /// [rectangle's documentation]($rect.stroke) for more details. + /// + /// ```example + /// This is a #highlight( + /// stroke: fuchsia + /// )[stroked highlighting]. + /// ``` + #[resolve] + #[fold] + pub stroke: Sides<Option<Option<Stroke>>>, + + /// The top end of the background rectangle. + /// + /// ```example + /// #set highlight(top-edge: "ascender") + /// #highlight[a] #highlight[aib] + /// + /// #set highlight(top-edge: "x-height") + /// #highlight[a] #highlight[aib] + /// ``` + #[default(TopEdge::Metric(TopEdgeMetric::Ascender))] + pub top_edge: TopEdge, + + /// The bottom end of the background rectangle. + /// + /// ```example + /// #set highlight(bottom-edge: "descender") + /// #highlight[a] #highlight[ap] + /// + /// #set highlight(bottom-edge: "baseline") + /// #highlight[a] #highlight[ap] + /// ``` + #[default(BottomEdge::Metric(BottomEdgeMetric::Descender))] + pub bottom_edge: BottomEdge, + + /// The amount by which to extend the background to the sides beyond + /// (or within if negative) the content. + /// + /// ```example + /// A long #highlight(extent: 4pt)[background]. + /// ``` + #[resolve] + pub extent: Length, + + /// How much to round the highlight's corners. See the + /// [rectangle's documentation]($rect.radius) for more details. + /// + /// ```example + /// Listen #highlight( + /// radius: 5pt, extent: 2pt + /// )[carefully], it will be on the test. + /// ``` + #[resolve] + #[fold] + pub radius: Corners<Option<Rel<Length>>>, + + /// The content that should be highlighted. + #[required] + pub body: Content, +} + +impl Show for Packed<HighlightElem> { + #[typst_macros::time(name = "highlight", span = self.span())] + fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + Ok(self.body().clone().styled(TextElem::set_deco(smallvec![Decoration { + line: DecoLine::Highlight { + fill: self.fill(styles), + stroke: self + .stroke(styles) + .unwrap_or_default() + .map(|stroke| stroke.map(Stroke::unwrap_or_default)), + top_edge: self.top_edge(styles), + bottom_edge: self.bottom_edge(styles), + radius: self.radius(styles).unwrap_or_default(), + }, + extent: self.extent(styles), + }]))) + } +} + +/// A text decoration. +/// +/// Can be positioned over, under, or on top of text, or highlight the text with +/// a background. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Decoration { + pub line: DecoLine, + pub extent: Abs, +} + +/// A kind of decorative line. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum DecoLine { + Underline { + stroke: Stroke<Abs>, + offset: Smart<Abs>, + evade: bool, + background: bool, + }, + Strikethrough { + stroke: Stroke<Abs>, + offset: Smart<Abs>, + background: bool, + }, + Overline { + stroke: Stroke<Abs>, + offset: Smart<Abs>, + evade: bool, + background: bool, + }, + Highlight { + fill: Option<Paint>, + stroke: Sides<Option<FixedStroke>>, + top_edge: TopEdge, + bottom_edge: BottomEdge, + radius: Corners<Rel<Abs>>, + }, +} diff --git a/crates/typst-library/src/text/font/book.rs b/crates/typst-library/src/text/font/book.rs new file mode 100644 index 00000000..23e27f64 --- /dev/null +++ b/crates/typst-library/src/text/font/book.rs @@ -0,0 +1,546 @@ +use std::cmp::Reverse; +use std::collections::BTreeMap; +use std::fmt::{self, Debug, Formatter}; + +use serde::{Deserialize, Serialize}; +use ttf_parser::{name_id, PlatformId, Tag}; +use unicode_segmentation::UnicodeSegmentation; + +use super::exceptions::find_exception; +use crate::text::{Font, FontStretch, FontStyle, FontVariant, FontWeight}; + +/// Metadata about a collection of fonts. +#[derive(Debug, 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 from a collection of font infos. + pub fn from_infos(infos: impl IntoIterator<Item = FontInfo>) -> Self { + let mut book = Self::new(); + for info in infos { + book.push(info); + } + book + } + + /// Create a font book for a collection of fonts. + pub fn from_fonts<'a>(fonts: impl IntoIterator<Item = &'a Font>) -> Self { + Self::from_infos(fonts.into_iter().map(|font| font.info().clone())) + } + + /// 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) + } + + /// Returns true if the book contains a font family with the given name. + pub fn contains_family(&self, family: &str) -> bool { + self.families.contains_key(family) + } + + /// 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 non-space char ... + let c = text.chars().find(|c| !c.is_whitespace())?; + 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(¤t.family, &like.family)), + current.family.len(), + ) + }), + current.variant.style.distance(variant.style), + current.variant.stretch.distance(variant.stretch), + current.variant.weight.distance(variant.weight), + ); + + if best_key.map_or(true, |b| key < b) { + best = Some(id); + best_key = Some(key); + } + } + + best + } +} + +/// Properties of a single font. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct FontInfo { + /// The typographic font family this font is part of. + pub family: String, + /// Properties that distinguish this font from other fonts in the same + /// family. + pub variant: FontVariant, + /// Properties of the font. + pub flags: FontFlags, + /// The unicode coverage of the font. + pub coverage: Coverage, +} + +bitflags::bitflags! { + /// Bitflags describing characteristics of a font. + #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] + #[derive(Serialize, Deserialize)] + #[serde(transparent)] + pub struct FontFlags: u32 { + /// All glyphs have the same width. + const MONOSPACE = 1 << 0; + /// Glyphs have short strokes at their stems. + const SERIF = 1 << 1; + } +} + +impl FontInfo { + /// Compute metadata for font at the `index` of the given data. + pub fn new(data: &[u8], index: u32) -> Option<Self> { + let ttf = ttf_parser::Face::parse(data, index).ok()?; + Self::from_ttf(&ttf) + } + + /// 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| Self::new(data, index)) + } + + /// Compute metadata for a single ttf-parser face. + pub(super) fn from_ttf(ttf: &ttf_parser::Face) -> Option<Self> { + let ps_name = find_name(ttf, name_id::POST_SCRIPT_NAME); + let exception = ps_name.as_deref().and_then(find_exception); + // 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"). + let family = + exception.and_then(|c| c.family.map(str::to_string)).or_else(|| { + let family = find_name(ttf, name_id::FAMILY)?; + Some(typographic_family(&family).to_string()) + })?; + + let variant = { + let style = exception.and_then(|c| c.style).unwrap_or_else(|| { + 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"); + + match (italic, oblique) { + (false, false) => FontStyle::Normal, + (true, _) => FontStyle::Italic, + (_, true) => FontStyle::Oblique, + } + }); + + let weight = exception.and_then(|c| c.weight).unwrap_or_else(|| { + let number = ttf.weight().to_number(); + FontWeight::from_number(number) + }); + + let stretch = exception + .and_then(|c| c.stretch) + .unwrap_or_else(|| 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), + }) + } + + /// Whether this is the macOS LastResort font. It can yield tofus with + /// glyph ID != 0. + pub fn is_last_resort(&self) -> bool { + self.family == "LastResort" + } +} + +/// 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" + ]; + + // 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().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]; + + family +} + +/// 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(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 + }) + } +} + +impl Debug for Coverage { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("Coverage(..)") + } +} + +#[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/crates/typst-library/src/text/font/color.rs b/crates/typst-library/src/text/font/color.rs new file mode 100644 index 00000000..08f6fe0a --- /dev/null +++ b/crates/typst-library/src/text/font/color.rs @@ -0,0 +1,627 @@ +//! Utilities for color font handling + +use std::io::Read; + +use ttf_parser::{GlyphId, RgbaColor}; +use typst_syntax::Span; +use usvg::tiny_skia_path; +use xmlwriter::XmlWriter; + +use crate::layout::{Abs, Frame, FrameItem, Point, Size}; +use crate::text::{Font, Glyph}; +use crate::visualize::{FixedStroke, Geometry, Image, RasterFormat, VectorFormat}; + +/// Whether this glyph should be rendered via simple outlining instead of via +/// `glyph_frame`. +pub fn should_outline(font: &Font, glyph: &Glyph) -> bool { + let ttf = font.ttf(); + let glyph_id = GlyphId(glyph.id); + (ttf.tables().glyf.is_some() || ttf.tables().cff.is_some()) + && !ttf + .glyph_raster_image(glyph_id, u16::MAX) + .is_some_and(|img| img.format == ttf_parser::RasterImageFormat::PNG) + && !ttf.is_color_glyph(glyph_id) + && ttf.glyph_svg_image(glyph_id).is_none() +} + +/// Returns a frame representing a glyph and whether it is a fallback tofu +/// frame. +/// +/// Should only be called on glyphs for which [`should_outline`] returns false. +/// +/// The glyphs are sized in font units, [`text.item.size`] is not taken into +/// account. +#[comemo::memoize] +pub fn glyph_frame(font: &Font, glyph_id: u16) -> (Frame, bool) { + let upem = Abs::pt(font.units_per_em()); + let glyph_id = GlyphId(glyph_id); + + let mut frame = Frame::soft(Size::splat(upem)); + let mut tofu = false; + + if draw_glyph(&mut frame, font, upem, glyph_id).is_none() + && font.ttf().glyph_index(' ') != Some(glyph_id) + { + // Generate a fallback tofu if the glyph couldn't be drawn, unless it is + // the space glyph. Then, an empty frame does the job. (This happens for + // some rare CBDT fonts, which don't define a bitmap for the space, but + // also don't have a glyf or CFF table.) + draw_fallback_tofu(&mut frame, font, upem, glyph_id); + tofu = true; + } + + (frame, tofu) +} + +/// Tries to draw a glyph. +fn draw_glyph( + frame: &mut Frame, + font: &Font, + upem: Abs, + glyph_id: GlyphId, +) -> Option<()> { + let ttf = font.ttf(); + if let Some(raster_image) = ttf + .glyph_raster_image(glyph_id, u16::MAX) + .filter(|img| img.format == ttf_parser::RasterImageFormat::PNG) + { + draw_raster_glyph(frame, font, upem, raster_image) + } else if ttf.is_color_glyph(glyph_id) { + draw_colr_glyph(frame, font, upem, glyph_id) + } else if ttf.glyph_svg_image(glyph_id).is_some() { + draw_svg_glyph(frame, font, upem, glyph_id) + } else { + None + } +} + +/// Draws a fallback tofu box with the advance width of the glyph. +fn draw_fallback_tofu(frame: &mut Frame, font: &Font, upem: Abs, glyph_id: GlyphId) { + let advance = font + .ttf() + .glyph_hor_advance(glyph_id) + .map(|advance| Abs::pt(advance as f64)) + .unwrap_or(upem / 3.0); + let inset = 0.15 * advance; + let height = 0.7 * upem; + let pos = Point::new(inset, upem - height); + let size = Size::new(advance - inset * 2.0, height); + let thickness = upem / 20.0; + let stroke = FixedStroke { thickness, ..Default::default() }; + let shape = Geometry::Rect(size).stroked(stroke); + frame.push(pos, FrameItem::Shape(shape, Span::detached())); +} + +/// Draws a raster glyph in a frame. +/// +/// Supports only PNG images. +fn draw_raster_glyph( + frame: &mut Frame, + font: &Font, + upem: Abs, + raster_image: ttf_parser::RasterGlyphImage, +) -> Option<()> { + let image = + Image::new(raster_image.data.into(), RasterFormat::Png.into(), None).ok()?; + + // Apple Color emoji doesn't provide offset information (or at least + // not in a way ttf-parser understands), so we artificially shift their + // baseline to make it look good. + let y_offset = if font.info().family.to_lowercase() == "apple color emoji" { + 20.0 + } else { + -(raster_image.y as f64) + }; + + let position = Point::new( + upem * raster_image.x as f64 / raster_image.pixels_per_em as f64, + upem * y_offset / raster_image.pixels_per_em as f64, + ); + let aspect_ratio = image.width() / image.height(); + let size = Size::new(upem, upem * aspect_ratio); + frame.push(position, FrameItem::Image(image, size, Span::detached())); + + Some(()) +} + +/// Draws a glyph from the COLR table into the frame. +fn draw_colr_glyph( + frame: &mut Frame, + font: &Font, + upem: Abs, + glyph_id: GlyphId, +) -> Option<()> { + let mut svg = XmlWriter::new(xmlwriter::Options::default()); + + let ttf = font.ttf(); + let width = ttf.global_bounding_box().width() as f64; + let height = ttf.global_bounding_box().height() as f64; + let x_min = ttf.global_bounding_box().x_min as f64; + let y_max = ttf.global_bounding_box().y_max as f64; + let tx = -x_min; + let ty = -y_max; + + svg.start_element("svg"); + svg.write_attribute("xmlns", "http://www.w3.org/2000/svg"); + svg.write_attribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); + svg.write_attribute("width", &width); + svg.write_attribute("height", &height); + svg.write_attribute_fmt("viewBox", format_args!("0 0 {width} {height}")); + + let mut path_buf = String::with_capacity(256); + let gradient_index = 1; + let clip_path_index = 1; + + svg.start_element("g"); + svg.write_attribute_fmt( + "transform", + format_args!("matrix(1 0 0 -1 0 0) matrix(1 0 0 1 {tx} {ty})"), + ); + + let mut glyph_painter = GlyphPainter { + face: ttf, + svg: &mut svg, + path_buf: &mut path_buf, + gradient_index, + clip_path_index, + palette_index: 0, + transform: ttf_parser::Transform::default(), + outline_transform: ttf_parser::Transform::default(), + transforms_stack: vec![ttf_parser::Transform::default()], + }; + + ttf.paint_color_glyph(glyph_id, 0, RgbaColor::new(0, 0, 0, 255), &mut glyph_painter)?; + svg.end_element(); + + let data = svg.end_document().into_bytes(); + + let image = Image::new(data.into(), VectorFormat::Svg.into(), None).ok()?; + + let y_shift = Abs::pt(upem.to_pt() - y_max); + let position = Point::new(Abs::pt(x_min), y_shift); + let size = Size::new(Abs::pt(width), Abs::pt(height)); + frame.push(position, FrameItem::Image(image, size, Span::detached())); + + Some(()) +} + +/// Draws an SVG glyph in a frame. +fn draw_svg_glyph( + frame: &mut Frame, + font: &Font, + upem: Abs, + glyph_id: GlyphId, +) -> Option<()> { + // TODO: Our current conversion of the SVG table works for Twitter Color Emoji, + // but might not work for others. See also: https://github.com/RazrFalcon/resvg/pull/776 + let mut data = font.ttf().glyph_svg_image(glyph_id)?.data; + + // 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()?; + + // Parse SVG. + let opts = usvg::Options::default(); + let tree = usvg::Tree::from_xmltree(&document, &opts).ok()?; + + let bbox = tree.root().bounding_box(); + let width = bbox.width() as f64; + let height = bbox.height() as f64; + let left = bbox.left() as f64; + let top = bbox.top() as f64; + + let mut data = tree.to_string(&usvg::WriteOptions::default()); + + // The SVG coordinates and the font coordinates are not the same: the Y axis + // is mirrored. But the origin of the axes are the same (which means that + // the horizontal axis in the SVG document corresponds to the baseline). See + // the reference for more details: + // https://learn.microsoft.com/en-us/typography/opentype/spec/svg#coordinate-systems-and-glyph-metrics + // + // If we used the SVG document as it is, svg2pdf would produce a cropped + // glyph (only what is under the baseline would be visible). So we need to + // embed the original SVG in another one that has the exact dimensions of + // the glyph, with a transform to make it fit. We also need to remove the + // viewBox, height and width attributes from the inner SVG, otherwise usvg + // takes into account these values to clip the embedded SVG. + make_svg_unsized(&mut data); + let wrapper_svg = format!( + r#" + <svg + width="{width}" + height="{height}" + viewBox="0 0 {width} {height}" + xmlns="http://www.w3.org/2000/svg"> + <g transform="matrix(1 0 0 1 {tx} {ty})"> + {inner} + </g> + </svg> + "#, + inner = data, + tx = -left, + ty = -top, + ); + + let image = + Image::new(wrapper_svg.into_bytes().into(), VectorFormat::Svg.into(), None) + .ok()?; + + let position = Point::new(Abs::pt(left), Abs::pt(top) + upem); + let size = Size::new(Abs::pt(width), Abs::pt(height)); + frame.push(position, FrameItem::Image(image, size, Span::detached())); + + Some(()) +} + +/// Remove all size specifications (viewBox, width and height attributes) from a +/// SVG document. +fn make_svg_unsized(svg: &mut String) { + let mut viewbox_range = None; + let mut width_range = None; + let mut height_range = None; + + let mut s = unscanny::Scanner::new(svg); + + s.eat_until("<svg"); + s.eat_if("<svg"); + while !s.eat_if('>') && !s.done() { + s.eat_whitespace(); + let start = s.cursor(); + let attr_name = s.eat_until('=').trim(); + // Eat the equal sign and the quote. + s.eat(); + s.eat(); + let mut escaped = false; + while (escaped || !s.eat_if('"')) && !s.done() { + escaped = s.eat() == Some('\\'); + } + match attr_name { + "viewBox" => viewbox_range = Some(start..s.cursor()), + "width" => width_range = Some(start..s.cursor()), + "height" => height_range = Some(start..s.cursor()), + _ => {} + } + } + + // Remove the `viewBox` attribute. + if let Some(range) = viewbox_range { + svg.replace_range(range.clone(), &" ".repeat(range.len())); + } + + // Remove the `width` attribute. + if let Some(range) = width_range { + svg.replace_range(range.clone(), &" ".repeat(range.len())); + } + + // Remove the `height` attribute. + if let Some(range) = height_range { + svg.replace_range(range, ""); + } +} + +struct ColrBuilder<'a>(&'a mut String); + +impl ColrBuilder<'_> { + fn finish(&mut self) { + if !self.0.is_empty() { + self.0.pop(); // remove trailing space + } + } +} + +impl ttf_parser::OutlineBuilder for ColrBuilder<'_> { + fn move_to(&mut self, x: f32, y: f32) { + use std::fmt::Write; + write!(self.0, "M {x} {y} ").unwrap() + } + + fn line_to(&mut self, x: f32, y: f32) { + use std::fmt::Write; + write!(self.0, "L {x} {y} ").unwrap() + } + + fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { + use std::fmt::Write; + write!(self.0, "Q {x1} {y1} {x} {y} ").unwrap() + } + + fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { + use std::fmt::Write; + write!(self.0, "C {x1} {y1} {x2} {y2} {x} {y} ").unwrap() + } + + fn close(&mut self) { + self.0.push_str("Z ") + } +} + +// NOTE: This is only a best-effort translation of COLR into SVG. It's not feature-complete +// and it's also not possible to make it feature-complete using just raw SVG features. +pub(crate) struct GlyphPainter<'a> { + pub(crate) face: &'a ttf_parser::Face<'a>, + pub(crate) svg: &'a mut xmlwriter::XmlWriter, + pub(crate) path_buf: &'a mut String, + pub(crate) gradient_index: usize, + pub(crate) clip_path_index: usize, + pub(crate) palette_index: u16, + pub(crate) transform: ttf_parser::Transform, + pub(crate) outline_transform: ttf_parser::Transform, + pub(crate) transforms_stack: Vec<ttf_parser::Transform>, +} + +impl<'a> GlyphPainter<'a> { + fn write_gradient_stops(&mut self, stops: ttf_parser::colr::GradientStopsIter) { + for stop in stops { + self.svg.start_element("stop"); + self.svg.write_attribute("offset", &stop.stop_offset); + self.write_color_attribute("stop-color", stop.color); + let opacity = f32::from(stop.color.alpha) / 255.0; + self.svg.write_attribute("stop-opacity", &opacity); + self.svg.end_element(); + } + } + + fn write_color_attribute(&mut self, name: &str, color: ttf_parser::RgbaColor) { + self.svg.write_attribute_fmt( + name, + format_args!("rgb({}, {}, {})", color.red, color.green, color.blue), + ); + } + + fn write_transform_attribute(&mut self, name: &str, ts: ttf_parser::Transform) { + if ts.is_default() { + return; + } + + self.svg.write_attribute_fmt( + name, + format_args!("matrix({} {} {} {} {} {})", ts.a, ts.b, ts.c, ts.d, ts.e, ts.f), + ); + } + + fn write_spread_method_attribute( + &mut self, + extend: ttf_parser::colr::GradientExtend, + ) { + self.svg.write_attribute( + "spreadMethod", + match extend { + ttf_parser::colr::GradientExtend::Pad => &"pad", + ttf_parser::colr::GradientExtend::Repeat => &"repeat", + ttf_parser::colr::GradientExtend::Reflect => &"reflect", + }, + ); + } + + fn paint_solid(&mut self, color: ttf_parser::RgbaColor) { + self.svg.start_element("path"); + self.write_color_attribute("fill", color); + let opacity = f32::from(color.alpha) / 255.0; + self.svg.write_attribute("fill-opacity", &opacity); + self.write_transform_attribute("transform", self.outline_transform); + self.svg.write_attribute("d", self.path_buf); + self.svg.end_element(); + } + + fn paint_linear_gradient(&mut self, gradient: ttf_parser::colr::LinearGradient<'a>) { + let gradient_id = format!("lg{}", self.gradient_index); + self.gradient_index += 1; + + let gradient_transform = paint_transform(self.outline_transform, self.transform); + + // TODO: We ignore x2, y2. Have to apply them somehow. + // TODO: The way spreadMode works in ttf and svg is a bit different. In SVG, the spreadMode + // will always be applied based on x1/y1 and x2/y2. However, in TTF the spreadMode will + // be applied from the first/last stop. So if we have a gradient with x1=0 x2=1, and + // a stop at x=0.4 and x=0.6, then in SVG we will always see a padding, while in ttf + // we will see the actual spreadMode. We need to account for that somehow. + self.svg.start_element("linearGradient"); + self.svg.write_attribute("id", &gradient_id); + self.svg.write_attribute("x1", &gradient.x0); + self.svg.write_attribute("y1", &gradient.y0); + self.svg.write_attribute("x2", &gradient.x1); + self.svg.write_attribute("y2", &gradient.y1); + self.svg.write_attribute("gradientUnits", &"userSpaceOnUse"); + self.write_spread_method_attribute(gradient.extend); + self.write_transform_attribute("gradientTransform", gradient_transform); + self.write_gradient_stops( + gradient.stops(self.palette_index, self.face.variation_coordinates()), + ); + self.svg.end_element(); + + self.svg.start_element("path"); + self.svg + .write_attribute_fmt("fill", format_args!("url(#{gradient_id})")); + self.write_transform_attribute("transform", self.outline_transform); + self.svg.write_attribute("d", self.path_buf); + self.svg.end_element(); + } + + fn paint_radial_gradient(&mut self, gradient: ttf_parser::colr::RadialGradient<'a>) { + let gradient_id = format!("rg{}", self.gradient_index); + self.gradient_index += 1; + + let gradient_transform = paint_transform(self.outline_transform, self.transform); + + self.svg.start_element("radialGradient"); + self.svg.write_attribute("id", &gradient_id); + self.svg.write_attribute("cx", &gradient.x1); + self.svg.write_attribute("cy", &gradient.y1); + self.svg.write_attribute("r", &gradient.r1); + self.svg.write_attribute("fr", &gradient.r0); + self.svg.write_attribute("fx", &gradient.x0); + self.svg.write_attribute("fy", &gradient.y0); + self.svg.write_attribute("gradientUnits", &"userSpaceOnUse"); + self.write_spread_method_attribute(gradient.extend); + self.write_transform_attribute("gradientTransform", gradient_transform); + self.write_gradient_stops( + gradient.stops(self.palette_index, self.face.variation_coordinates()), + ); + self.svg.end_element(); + + self.svg.start_element("path"); + self.svg + .write_attribute_fmt("fill", format_args!("url(#{gradient_id})")); + self.write_transform_attribute("transform", self.outline_transform); + self.svg.write_attribute("d", self.path_buf); + self.svg.end_element(); + } + + fn paint_sweep_gradient(&mut self, _: ttf_parser::colr::SweepGradient<'a>) {} +} + +fn paint_transform( + outline_transform: ttf_parser::Transform, + transform: ttf_parser::Transform, +) -> ttf_parser::Transform { + let outline_transform = tiny_skia_path::Transform::from_row( + outline_transform.a, + outline_transform.b, + outline_transform.c, + outline_transform.d, + outline_transform.e, + outline_transform.f, + ); + + let gradient_transform = tiny_skia_path::Transform::from_row( + transform.a, + transform.b, + transform.c, + transform.d, + transform.e, + transform.f, + ); + + let gradient_transform = outline_transform + .invert() + // In theory, we should error out. But the transform shouldn't ever be uninvertible, so let's ignore it. + .unwrap_or_default() + .pre_concat(gradient_transform); + + ttf_parser::Transform { + a: gradient_transform.sx, + b: gradient_transform.ky, + c: gradient_transform.kx, + d: gradient_transform.sy, + e: gradient_transform.tx, + f: gradient_transform.ty, + } +} + +impl GlyphPainter<'_> { + fn clip_with_path(&mut self, path: &str) { + let clip_id = format!("cp{}", self.clip_path_index); + self.clip_path_index += 1; + + self.svg.start_element("clipPath"); + self.svg.write_attribute("id", &clip_id); + self.svg.start_element("path"); + self.write_transform_attribute("transform", self.outline_transform); + self.svg.write_attribute("d", &path); + self.svg.end_element(); + self.svg.end_element(); + + self.svg.start_element("g"); + self.svg + .write_attribute_fmt("clip-path", format_args!("url(#{clip_id})")); + } +} + +impl<'a> ttf_parser::colr::Painter<'a> for GlyphPainter<'a> { + fn outline_glyph(&mut self, glyph_id: ttf_parser::GlyphId) { + self.path_buf.clear(); + let mut builder = ColrBuilder(self.path_buf); + match self.face.outline_glyph(glyph_id, &mut builder) { + Some(v) => v, + None => return, + }; + builder.finish(); + + // We have to write outline using the current transform. + self.outline_transform = self.transform; + } + + fn push_layer(&mut self, mode: ttf_parser::colr::CompositeMode) { + self.svg.start_element("g"); + + use ttf_parser::colr::CompositeMode; + // TODO: Need to figure out how to represent the other blend modes + // in SVG. + let mode = match mode { + CompositeMode::SourceOver => "normal", + CompositeMode::Screen => "screen", + CompositeMode::Overlay => "overlay", + CompositeMode::Darken => "darken", + CompositeMode::Lighten => "lighten", + CompositeMode::ColorDodge => "color-dodge", + CompositeMode::ColorBurn => "color-burn", + CompositeMode::HardLight => "hard-light", + CompositeMode::SoftLight => "soft-light", + CompositeMode::Difference => "difference", + CompositeMode::Exclusion => "exclusion", + CompositeMode::Multiply => "multiply", + CompositeMode::Hue => "hue", + CompositeMode::Saturation => "saturation", + CompositeMode::Color => "color", + CompositeMode::Luminosity => "luminosity", + _ => "normal", + }; + self.svg.write_attribute_fmt( + "style", + format_args!("mix-blend-mode: {mode}; isolation: isolate"), + ); + } + + fn pop_layer(&mut self) { + self.svg.end_element(); // g + } + + fn push_transform(&mut self, transform: ttf_parser::Transform) { + self.transforms_stack.push(self.transform); + self.transform = ttf_parser::Transform::combine(self.transform, transform); + } + + fn paint(&mut self, paint: ttf_parser::colr::Paint<'a>) { + match paint { + ttf_parser::colr::Paint::Solid(color) => self.paint_solid(color), + ttf_parser::colr::Paint::LinearGradient(lg) => self.paint_linear_gradient(lg), + ttf_parser::colr::Paint::RadialGradient(rg) => self.paint_radial_gradient(rg), + ttf_parser::colr::Paint::SweepGradient(sg) => self.paint_sweep_gradient(sg), + } + } + + fn pop_transform(&mut self) { + if let Some(ts) = self.transforms_stack.pop() { + self.transform = ts + } + } + + fn push_clip(&mut self) { + self.clip_with_path(&self.path_buf.clone()); + } + + fn pop_clip(&mut self) { + self.svg.end_element(); + } + + fn push_clip_box(&mut self, clipbox: ttf_parser::colr::ClipBox) { + let x_min = clipbox.x_min; + let x_max = clipbox.x_max; + let y_min = clipbox.y_min; + let y_max = clipbox.y_max; + + let clip_path = format!( + "M {x_min} {y_min} L {x_max} {y_min} L {x_max} {y_max} L {x_min} {y_max} Z" + ); + + self.clip_with_path(&clip_path); + } +} diff --git a/crates/typst-library/src/text/font/exceptions.rs b/crates/typst-library/src/text/font/exceptions.rs new file mode 100644 index 00000000..6393df4b --- /dev/null +++ b/crates/typst-library/src/text/font/exceptions.rs @@ -0,0 +1,337 @@ +use serde::Deserialize; + +use super::{FontStretch, FontStyle, FontWeight}; + +pub fn find_exception(postscript_name: &str) -> Option<&'static Exception> { + EXCEPTION_MAP.get(postscript_name) +} + +#[derive(Debug, Default, Deserialize)] +pub struct Exception { + pub family: Option<&'static str>, + pub style: Option<FontStyle>, + pub weight: Option<FontWeight>, + pub stretch: Option<FontStretch>, +} + +impl Exception { + const fn new() -> Self { + Self { + family: None, + style: None, + weight: None, + stretch: None, + } + } + + const fn family(self, family: &'static str) -> Self { + Self { family: Some(family), ..self } + } + + const fn style(self, style: FontStyle) -> Self { + Self { style: Some(style), ..self } + } + + const fn weight(self, weight: u16) -> Self { + Self { weight: Some(FontWeight(weight)), ..self } + } + + #[allow(unused)] // left for future use + const fn stretch(self, stretch: u16) -> Self { + Self { stretch: Some(FontStretch(stretch)), ..self } + } +} + +/// A map which keys are PostScript name and values are override entries. +static EXCEPTION_MAP: phf::Map<&'static str, Exception> = phf::phf_map! { + // The old version of Arial-Black, published by Microsoft in 1996 in their + // "core fonts for the web" project, has a wrong weight of 400. + // See https://corefonts.sourceforge.net/. + "Arial-Black" => Exception::new() + .weight(900), + // Archivo Narrow is different from Archivo and Archivo Black. Since Archivo Black + // seems identical to Archivo weight 900, only differentiate between Archivo and + // Archivo Narrow. + "ArchivoNarrow-Regular" => Exception::new() + .family("Archivo Narrow"), + "ArchivoNarrow-Italic" => Exception::new() + .family("Archivo Narrow"), + "ArchivoNarrow-Bold" => Exception::new() + .family("Archivo Narrow"), + "ArchivoNarrow-BoldItalic" => Exception::new() + .family("Archivo Narrow"), + // Fandol fonts designed for Chinese typesetting. + // See https://ctan.org/tex-archive/fonts/fandol/. + "FandolHei-Bold" => Exception::new() + .weight(700), + "FandolSong-Bold" => Exception::new() + .weight(700), + // Noto fonts + "NotoNaskhArabicUISemi-Bold" => Exception::new() + .family("Noto Naskh Arabic UI") + .weight(600), + "NotoSansSoraSompengSemi-Bold" => Exception::new() + .family("Noto Sans Sora Sompeng") + .weight(600), + "NotoSans-DisplayBlackItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayCondensedBlackItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayCondensedBold" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayCondensedBoldItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayCondensedExtraBoldItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayCondensedExtraLightItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayCondensedItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayCondensedLightItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayCondensedMediumItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayCondensedSemiBoldItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayCondensedThinItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayExtraBoldItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayExtraCondensedBlackItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayExtraCondensedBold" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayExtraCondensedBoldItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayExtraCondensedExtraBoldItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayExtraCondensedExtraLightItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayExtraCondensedItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayExtraCondensedLightItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayExtraCondensedMediumItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayExtraCondensedSemiBoldItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayExtraCondensedThinItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayExtraLightItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayLightItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayMediumItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplaySemiBoldItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplaySemiCondensedBlackItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplaySemiCondensedBold" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplaySemiCondensedBoldItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplaySemiCondensedExtraBoldItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplaySemiCondensedExtraLightItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplaySemiCondensedItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplaySemiCondensedLightItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplaySemiCondensedMediumItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplaySemiCondensedSemiBoldItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplaySemiCondensedThinItalic" => Exception::new() + .family("Noto Sans Display"), + "NotoSans-DisplayThinItalic" => Exception::new() + .family("Noto Sans Display"), + // The following three postscript names are only used in the version 2.007 + // of the Noto Sans font. Other versions, while have different postscript + // name, happen to have correct metadata. + "NotoSerif-DisplayCondensedBold" => Exception::new() + .family("Noto Serif Display"), + "NotoSerif-DisplayExtraCondensedBold" => Exception::new() + .family("Noto Serif Display"), + "NotoSerif-DisplaySemiCondensedBold" => Exception::new() + .family("Noto Serif Display"), + // New Computer Modern + "NewCM08-Book" => Exception::new() + .family("New Computer Modern 08") + .weight(450), + "NewCM08-BookItalic" => Exception::new() + .family("New Computer Modern 08") + .weight(450), + "NewCM08-Italic" => Exception::new() + .family("New Computer Modern 08"), + "NewCM08-Regular" => Exception::new() + .family("New Computer Modern 08"), + "NewCM10-Bold" => Exception::new() + .family("New Computer Modern"), + "NewCM10-BoldItalic" => Exception::new() + .family("New Computer Modern"), + "NewCM10-Book" => Exception::new() + .family("New Computer Modern") + .weight(450), + "NewCM10-BookItalic" => Exception::new() + .family("New Computer Modern") + .weight(450), + "NewCM10-Italic" => Exception::new() + .family("New Computer Modern"), + "NewCM10-Regular" => Exception::new() + .family("New Computer Modern"), + "NewCMMath-Bold" => Exception::new() + .family("New Computer Modern Math") + .weight(700), + "NewCMMath-Book" => Exception::new() + .family("New Computer Modern Math") + .weight(450), + "NewCMMath-Regular" => Exception::new() + .family("New Computer Modern Math"), + "NewCMMono10-Bold" => Exception::new() + .family("New Computer Modern Mono"), + "NewCMMono10-BoldOblique" => Exception::new() + .family("New Computer Modern Mono"), + "NewCMMono10-Book" => Exception::new() + .family("New Computer Modern Mono") + .weight(450), + "NewCMMono10-BookItalic" => Exception::new() + .family("New Computer Modern Mono") + .weight(450), + "NewCMMono10-Italic" => Exception::new() + .family("New Computer Modern Mono"), + "NewCMMono10-Regular" => Exception::new() + .family("New Computer Modern Mono"), + "NewCMSans08-Book" => Exception::new() + .family("New Computer Modern Sans 08") + .weight(450), + "NewCMSans08-BookOblique" => Exception::new() + .family("New Computer Modern Sans 08") + .weight(450), + "NewCMSans08-Oblique" => Exception::new() + .family("New Computer Modern Sans 08"), + "NewCMSans08-Regular" => Exception::new() + .family("New Computer Modern Sans 08"), + "NewCMSans10-Bold" => Exception::new() + .family("New Computer Modern Sans"), + "NewCMSans10-BoldOblique" => Exception::new() + .family("New Computer Modern Sans"), + "NewCMSans10-Book" => Exception::new() + .family("New Computer Modern Sans") + .weight(450), + "NewCMSans10-BookOblique" => Exception::new() + .family("New Computer Modern Sans") + .weight(450) + .style(FontStyle::Oblique), + "NewCMSans10-Oblique" => Exception::new() + .family("New Computer Modern Sans") + .style(FontStyle::Oblique), + "NewCMSans10-Regular" => Exception::new() + .family("New Computer Modern Sans"), + "NewCMUncial08-Bold" => Exception::new() + .family("New Computer Modern Uncial 08"), + "NewCMUncial08-Book" => Exception::new() + .family("New Computer Modern Uncial 08") + .weight(450), + "NewCMUncial08-Regular" => Exception::new() + .family("New Computer Modern Uncial 08"), + "NewCMUncial10-Bold" => Exception::new() + .family("New Computer Modern Uncial"), + "NewCMUncial10-Book" => Exception::new() + .family("New Computer Modern Uncial") + .weight(450), + "NewCMUncial10-Regular" => Exception::new() + .family("New Computer Modern Uncial"), + // Latin Modern + "LMMono8-Regular" => Exception::new() + .family("Latin Modern Mono 8"), + "LMMono9-Regular" => Exception::new() + .family("Latin Modern Mono 9"), + "LMMono12-Regular" => Exception::new() + .family("Latin Modern Mono 12"), + "LMMonoLt10-BoldOblique" => Exception::new() + .style(FontStyle::Oblique), + "LMMonoLt10-Regular" => Exception::new() + .weight(300), + "LMMonoLt10-Oblique" => Exception::new() + .weight(300) + .style(FontStyle::Oblique), + "LMMonoLtCond10-Regular" => Exception::new() + .weight(300) + .stretch(666), + "LMMonoLtCond10-Oblique" => Exception::new() + .weight(300) + .style(FontStyle::Oblique) + .stretch(666), + "LMMonoPropLt10-Regular" => Exception::new() + .weight(300), + "LMMonoPropLt10-Oblique" => Exception::new() + .weight(300), + "LMRoman5-Regular" => Exception::new() + .family("Latin Modern Roman 5"), + "LMRoman6-Regular" => Exception::new() + .family("Latin Modern Roman 6"), + "LMRoman7-Regular" => Exception::new() + .family("Latin Modern Roman 7"), + "LMRoman8-Regular" => Exception::new() + .family("Latin Modern Roman 8"), + "LMRoman9-Regular" => Exception::new() + .family("Latin Modern Roman 9"), + "LMRoman12-Regular" => Exception::new() + .family("Latin Modern Roman 12"), + "LMRoman17-Regular" => Exception::new() + .family("Latin Modern Roman 17"), + "LMRoman7-Italic" => Exception::new() + .family("Latin Modern Roman 7"), + "LMRoman8-Italic" => Exception::new() + .family("Latin Modern Roman 8"), + "LMRoman9-Italic" => Exception::new() + .family("Latin Modern Roman 9"), + "LMRoman12-Italic" => Exception::new() + .family("Latin Modern Roman 12"), + "LMRoman5-Bold" => Exception::new() + .family("Latin Modern Roman 5"), + "LMRoman6-Bold" => Exception::new() + .family("Latin Modern Roman 6"), + "LMRoman7-Bold" => Exception::new() + .family("Latin Modern Roman 7"), + "LMRoman8-Bold" => Exception::new() + .family("Latin Modern Roman 8"), + "LMRoman9-Bold" => Exception::new() + .family("Latin Modern Roman 9"), + "LMRoman12-Bold" => Exception::new() + .family("Latin Modern Roman 12"), + "LMRomanSlant8-Regular" => Exception::new() + .family("Latin Modern Roman 8"), + "LMRomanSlant9-Regular" => Exception::new() + .family("Latin Modern Roman 9"), + "LMRomanSlant12-Regular" => Exception::new() + .family("Latin Modern Roman 12"), + "LMRomanSlant17-Regular" => Exception::new() + .family("Latin Modern Roman 17"), + "LMSans8-Regular" => Exception::new() + .family("Latin Modern Sans 8"), + "LMSans9-Regular" => Exception::new() + .family("Latin Modern Sans 9"), + "LMSans12-Regular" => Exception::new() + .family("Latin Modern Sans 12"), + "LMSans17-Regular" => Exception::new() + .family("Latin Modern Sans 17"), + "LMSans8-Oblique" => Exception::new() + .family("Latin Modern Sans 8"), + "LMSans9-Oblique" => Exception::new() + .family("Latin Modern Sans 9"), + "LMSans12-Oblique" => Exception::new() + .family("Latin Modern Sans 12"), + "LMSans17-Oblique" => Exception::new() + .family("Latin Modern Sans 17"), + // STKaiti is a set of Kai fonts. Their weight values need to be corrected + // according to their PostScript names. + "STKaitiSC-Regular" => Exception::new().weight(400), + "STKaitiTC-Regular" => Exception::new().weight(400), + "STKaitiSC-Bold" => Exception::new().weight(700), + "STKaitiTC-Bold" => Exception::new().weight(700), + "STKaitiSC-Black" => Exception::new().weight(900), + "STKaitiTC-Black" => Exception::new().weight(900), +}; diff --git a/crates/typst-library/src/text/font/mod.rs b/crates/typst-library/src/text/font/mod.rs new file mode 100644 index 00000000..09837312 --- /dev/null +++ b/crates/typst-library/src/text/font/mod.rs @@ -0,0 +1,306 @@ +//! Font handling. + +pub mod color; + +mod book; +mod exceptions; +mod variant; + +pub use self::book::{Coverage, FontBook, FontFlags, FontInfo}; +pub use self::variant::{FontStretch, FontStyle, FontVariant, FontWeight}; + +use std::cell::OnceCell; +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::foundations::{Bytes, Cast}; +use crate::layout::{Abs, Em, Frame}; +use crate::text::{BottomEdge, TopEdge}; + +/// 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 + } + + /// Resolve the top and bottom edges of text. + pub fn edges( + &self, + top_edge: TopEdge, + bottom_edge: BottomEdge, + font_size: Abs, + bounds: TextEdgeBounds, + ) -> (Abs, Abs) { + let cell = OnceCell::new(); + let bbox = |gid, f: fn(ttf_parser::Rect) -> i16| { + cell.get_or_init(|| self.ttf().glyph_bounding_box(GlyphId(gid))) + .map(|bbox| self.to_em(f(bbox)).at(font_size)) + .unwrap_or_default() + }; + + let top = match top_edge { + TopEdge::Metric(metric) => match metric.try_into() { + Ok(metric) => self.metrics().vertical(metric).at(font_size), + Err(_) => match bounds { + TextEdgeBounds::Zero => Abs::zero(), + TextEdgeBounds::Frame(frame) => frame.ascent(), + TextEdgeBounds::Glyph(gid) => bbox(gid, |b| b.y_max), + }, + }, + TopEdge::Length(length) => length.at(font_size), + }; + + let bottom = match bottom_edge { + BottomEdge::Metric(metric) => match metric.try_into() { + Ok(metric) => -self.metrics().vertical(metric).at(font_size), + Err(_) => match bounds { + TextEdgeBounds::Zero => Abs::zero(), + TextEdgeBounds::Frame(frame) => frame.descent(), + TextEdgeBounds::Glyph(gid) => -bbox(gid, |b| b.y_min), + }, + }, + BottomEdge::Length(length) => -length.at(font_size), + }; + + (top, bottom) + } +} + +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, self.info().variant) + } +} + +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, +} + +/// Defines how to resolve a `Bounds` text edge. +#[derive(Debug, Copy, Clone)] +pub enum TextEdgeBounds<'a> { + /// Set the bounds to zero. + Zero, + /// Use the bounding box of the given glyph for the bounds. + Glyph(u16), + /// Use the dimension of the given frame for the bounds. + Frame(&'a Frame), +} diff --git a/crates/typst-library/src/text/font/variant.rs b/crates/typst-library/src/text/font/variant.rs new file mode 100644 index 00000000..c7a00fb3 --- /dev/null +++ b/crates/typst-library/src/text/font/variant.rs @@ -0,0 +1,320 @@ +use std::fmt::{self, Debug, Formatter}; + +use ecow::EcoString; +use serde::{Deserialize, Serialize}; + +use crate::foundations::{cast, Cast, IntoValue, Repr}; +use crate::layout::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 + } +} + +impl From<usvg::FontStyle> for FontStyle { + fn from(style: usvg::FontStyle) -> Self { + match style { + usvg::FontStyle::Normal => Self::Normal, + usvg::FontStyle::Italic => Self::Italic, + usvg::FontStyle::Oblique => Self::Oblique, + } + } +} + +/// The weight of a font. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[derive(Serialize, Deserialize)] +#[serde(transparent)] +pub struct FontWeight(pub(super) u16); + +/// Font weight names and numbers. +/// See `<https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-weight#common_weight_name_mapping>` +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.clamp(100, 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).clamp(100, 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) + } +} + +impl From<fontdb::Weight> for FontWeight { + fn from(weight: fontdb::Weight) -> Self { + Self::from_number(weight.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(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[derive(Serialize, Deserialize)] +#[serde(transparent)] +pub struct FontStretch(pub(super) 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().clamp(0.5, 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) + } + + /// Round to one of the pre-defined variants. + pub fn round(self) -> Self { + match self.0 { + ..=562 => Self::ULTRA_CONDENSED, + 563..=687 => Self::EXTRA_CONDENSED, + 688..=812 => Self::CONDENSED, + 813..=937 => Self::SEMI_CONDENSED, + 938..=1062 => Self::NORMAL, + 1063..=1187 => Self::SEMI_EXPANDED, + 1188..=1374 => Self::EXPANDED, + 1375..=1749 => Self::EXTRA_EXPANDED, + 1750.. => Self::ULTRA_EXPANDED, + } + } + + /// 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 Repr for FontStretch { + fn repr(&self) -> EcoString { + self.to_ratio().repr() + } +} + +impl From<usvg::FontStretch> for FontStretch { + fn from(stretch: usvg::FontStretch) -> Self { + match stretch { + usvg::FontStretch::UltraCondensed => Self::ULTRA_CONDENSED, + usvg::FontStretch::ExtraCondensed => Self::EXTRA_CONDENSED, + usvg::FontStretch::Condensed => Self::CONDENSED, + usvg::FontStretch::SemiCondensed => Self::SEMI_CONDENSED, + usvg::FontStretch::Normal => Self::NORMAL, + usvg::FontStretch::SemiExpanded => Self::SEMI_EXPANDED, + usvg::FontStretch::Expanded => Self::EXPANDED, + usvg::FontStretch::ExtraExpanded => Self::EXTRA_EXPANDED, + usvg::FontStretch::UltraExpanded => Self::ULTRA_EXPANDED, + } + } +} + +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!(FontStretch::EXPANDED.repr(), "125%") + } +} diff --git a/crates/typst-library/src/text/item.rs b/crates/typst-library/src/text/item.rs new file mode 100644 index 00000000..ed559aec --- /dev/null +++ b/crates/typst-library/src/text/item.rs @@ -0,0 +1,118 @@ +use std::fmt::{self, Debug, Formatter}; +use std::ops::Range; + +use ecow::EcoString; +use typst_syntax::Span; + +use crate::layout::{Abs, Em}; +use crate::text::{is_default_ignorable, Font, Lang, Region}; +use crate::visualize::{FixedStroke, Paint}; + +/// 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, + /// Glyph stroke. + pub stroke: Option<FixedStroke>, + /// The natural language of the text. + pub lang: Lang, + /// The region of the text. + pub region: Option<Region>, + /// The item's plain text. + pub text: EcoString, + /// The glyphs. The number of glyphs may be different from the number of + /// characters in the plain text due to e.g. ligatures. + 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. The range's length may + /// be more than one due to multi-byte UTF-8 encoding or ligatures. + 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) + } +} + +/// A slice of a [`TextItem`]. +pub struct TextItemView<'a> { + /// The whole item this is a part of + pub item: &'a TextItem, + /// The glyphs of this slice + pub glyph_range: Range<usize>, +} + +impl<'a> TextItemView<'a> { + /// Build a TextItemView for the whole contents of a TextItem. + pub fn full(text: &'a TextItem) -> Self { + Self::from_glyph_range(text, 0..text.glyphs.len()) + } + + /// Build a new [`TextItemView`] from a [`TextItem`] and a range of glyphs. + pub fn from_glyph_range(text: &'a TextItem, glyph_range: Range<usize>) -> Self { + TextItemView { item: text, glyph_range } + } + + /// Returns an iterator over the glyphs of the slice. + /// + /// Note that the ranges are not remapped. They still point into the + /// original text. + pub fn glyphs(&self) -> &[Glyph] { + &self.item.glyphs[self.glyph_range.clone()] + } + + /// The plain text for the given glyph from `glyphs()`. This is an + /// approximation since glyphs do not correspond 1-1 with codepoints. + pub fn glyph_text(&self, glyph: &Glyph) -> EcoString { + // Trim default ignorables which might have ended up in the glyph's + // cluster. Keep interior ones so that joined emojis work. All of this + // is a hack and needs to be reworked. See + // https://github.com/typst/typst/pull/5099 + self.item.text[glyph.range()] + .trim_matches(is_default_ignorable) + .into() + } + + /// The total width of this text slice + pub fn width(&self) -> Abs { + self.glyphs() + .iter() + .map(|g| g.x_advance) + .sum::<Em>() + .at(self.item.size) + } +} diff --git a/crates/typst-library/src/text/lang.rs b/crates/typst-library/src/text/lang.rs new file mode 100644 index 00000000..64ab1a7c --- /dev/null +++ b/crates/typst-library/src/text/lang.rs @@ -0,0 +1,317 @@ +use std::collections::HashMap; +use std::str::FromStr; + +use ecow::{eco_format, EcoString}; + +use crate::diag::Hint; +use crate::foundations::{cast, StyleChain}; +use crate::layout::Dir; +use crate::text::TextElem; + +macro_rules! translation { + ($lang:literal) => { + ($lang, include_str!(concat!("../../translations/", $lang, ".txt"))) + }; +} + +const TRANSLATIONS: [(&str, &str); 36] = [ + translation!("ar"), + translation!("ca"), + translation!("cs"), + translation!("da"), + translation!("de"), + translation!("en"), + translation!("es"), + translation!("et"), + translation!("fi"), + translation!("fr"), + translation!("gl"), + translation!("gr"), + translation!("he"), + translation!("hu"), + translation!("is"), + translation!("it"), + translation!("ja"), + translation!("la"), + translation!("nb"), + translation!("nl"), + translation!("nn"), + translation!("pl"), + translation!("pt-PT"), + translation!("pt"), + translation!("ro"), + translation!("ru"), + translation!("sl"), + translation!("sq"), + translation!("sr"), + translation!("sv"), + translation!("tl"), + translation!("tr"), + translation!("ua"), + translation!("vi"), + translation!("zh-TW"), + translation!("zh"), +]; + +/// 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 CATALAN: Self = Self(*b"ca ", 2); + pub const CHINESE: Self = Self(*b"zh ", 2); + pub const CROATIAN: Self = Self(*b"hr ", 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 ESTONIAN: Self = Self(*b"et ", 2); + pub const FILIPINO: Self = Self(*b"tl ", 2); + pub const FINNISH: Self = Self(*b"fi ", 2); + pub const FRENCH: Self = Self(*b"fr ", 2); + pub const GALICIAN: Self = Self(*b"gl ", 2); + pub const GERMAN: Self = Self(*b"de ", 2); + pub const GREEK: Self = Self(*b"gr ", 2); + pub const HEBREW: Self = Self(*b"he ", 2); + pub const HUNGARIAN: Self = Self(*b"hu ", 2); + pub const ICELANDIC: Self = Self(*b"is ", 2); + pub const ITALIAN: Self = Self(*b"it ", 2); + pub const JAPANESE: Self = Self(*b"ja ", 2); + pub const LATIN: Self = Self(*b"la ", 2); + pub const LOWER_SORBIAN: Self = Self(*b"dsb", 3); + 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 ROMANIAN: Self = Self(*b"ro ", 2); + pub const RUSSIAN: Self = Self(*b"ru ", 2); + pub const SERBIAN: Self = Self(*b"sr ", 2); + pub const SLOVAK: Self = Self(*b"sk ", 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 => { + let result = Self::from_str(&string); + if result.is_err() { + if let Some((lang, region)) = string.split_once('-') { + if Lang::from_str(lang).is_ok() && Region::from_str(region).is_ok() { + return result + .hint(eco_format!( + "you should leave only \"{}\" in the `lang` parameter and specify \"{}\" in the `region` parameter", + lang, region, + )); + } + } + } + + result? + } +} + +/// 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)?, +} + +/// An ISO 15924-type script identifier. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct WritingScript([u8; 4], u8); + +impl WritingScript { + /// Return the script 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() + } + + /// Return the description of the script as raw bytes. + pub fn as_bytes(&self) -> &[u8; 4] { + &self.0 + } +} + +impl FromStr for WritingScript { + type Err = &'static str; + + /// Construct a region from its ISO 15924 code. + fn from_str(iso: &str) -> Result<Self, Self::Err> { + let len = iso.len(); + if matches!(len, 3..=4) && iso.is_ascii() { + let mut bytes = [b' '; 4]; + bytes[..len].copy_from_slice(iso.as_bytes()); + bytes.make_ascii_lowercase(); + Ok(Self(bytes, len as u8)) + } else { + Err("expected three or four letter script code (ISO 15924 or 'math')") + } + } +} + +cast! { + WritingScript, + self => self.as_str().into_value(), + string: EcoString => Self::from_str(&string)?, +} + +/// The name with which an element is referenced. +pub trait LocalName { + /// The key of an element in order to get its localized name. + const KEY: &'static str; + + /// Get the name in the given language and (optionally) region. + fn local_name(lang: Lang, region: Option<Region>) -> &'static str { + localized_str(lang, region, Self::KEY) + } + + /// Gets the local name from the style chain. + fn local_name_in(styles: StyleChain) -> &'static str + where + Self: Sized, + { + Self::local_name(TextElem::lang_in(styles), TextElem::region_in(styles)) + } +} + +/// Retrieves the localized string for a given language and region. +/// Silently falls back to English if no fitting string exists for +/// the given language + region. Panics if no fitting string exists +/// in both given language + region and English. +#[comemo::memoize] +pub fn localized_str(lang: Lang, region: Option<Region>, key: &str) -> &'static str { + let lang_region_bundle = parse_language_bundle(lang, region).unwrap(); + if let Some(str) = lang_region_bundle.get(key) { + return str; + } + let lang_bundle = parse_language_bundle(lang, None).unwrap(); + if let Some(str) = lang_bundle.get(key) { + return str; + } + let english_bundle = parse_language_bundle(Lang::ENGLISH, None).unwrap(); + english_bundle.get(key).unwrap() +} + +/// Parses the translation file for a given language and region. +/// Only returns an error if the language file is malformed. +#[comemo::memoize] +fn parse_language_bundle( + lang: Lang, + region: Option<Region>, +) -> Result<HashMap<&'static str, &'static str>, &'static str> { + let language_tuple = TRANSLATIONS.iter().find(|it| it.0 == lang_str(lang, region)); + let Some((_lang_name, language_file)) = language_tuple else { + return Ok(HashMap::new()); + }; + + let mut bundle = HashMap::new(); + let lines = language_file.trim().lines(); + for line in lines { + if line.trim().starts_with('#') { + continue; + } + let (key, val) = line + .split_once('=') + .ok_or("malformed translation file: line without \"=\"")?; + let (key, val) = (key.trim(), val.trim()); + if val.is_empty() { + return Err("malformed translation file: empty translation value"); + } + let duplicate = bundle.insert(key.trim(), val.trim()); + if duplicate.is_some() { + return Err("malformed translation file: duplicate key"); + } + } + Ok(bundle) +} + +/// Convert language + region to a string to be able to get a file name. +fn lang_str(lang: Lang, region: Option<Region>) -> EcoString { + EcoString::from(lang.as_str()) + + region.map_or_else(EcoString::new, |r| EcoString::from("-") + r.as_str()) +} + +#[cfg(test)] +mod tests { + use typst_utils::option_eq; + + use super::*; + + #[test] + fn test_region_option_eq() { + let region = Some(Region([b'U', b'S'])); + assert!(option_eq(region, "US")); + assert!(!option_eq(region, "AB")); + } +} diff --git a/crates/typst-library/src/text/linebreak.rs b/crates/typst-library/src/text/linebreak.rs new file mode 100644 index 00000000..0519e1c4 --- /dev/null +++ b/crates/typst-library/src/text/linebreak.rs @@ -0,0 +1,46 @@ +use typst_utils::singleton; + +use crate::foundations::{elem, Content, NativeElement}; + +/// Inserts a line break. +/// +/// Advances the paragraph to the next line. A single trailing line break at the +/// end of a paragraph is ignored, but more than one creates additional empty +/// lines. +/// +/// # Example +/// ```example +/// *Date:* 26.12.2022 \ +/// *Topic:* Infrastructure Test \ +/// *Severity:* High \ +/// ``` +/// +/// # Syntax +/// This function also has dedicated syntax: To insert a line break, simply write +/// a backslash followed by whitespace. This always creates an unjustified +/// break. +#[elem(title = "Line Break")] +pub struct LinebreakElem { + /// Whether to justify the line before the break. + /// + /// This is useful if you found a better line break opportunity in your + /// justified text than Typst did. + /// + /// ```example + /// #set par(justify: true) + /// #let jb = linebreak(justify: true) + /// + /// I have manually tuned the #jb + /// line breaks in this paragraph #jb + /// for an _interesting_ result. #jb + /// ``` + #[default(false)] + pub justify: bool, +} + +impl LinebreakElem { + /// Get the globally shared linebreak element. + pub fn shared() -> &'static Content { + singleton!(Content, LinebreakElem::new().pack()) + } +} diff --git a/crates/typst-library/src/text/lorem.rs b/crates/typst-library/src/text/lorem.rs new file mode 100644 index 00000000..5d01a550 --- /dev/null +++ b/crates/typst-library/src/text/lorem.rs @@ -0,0 +1,24 @@ +use crate::foundations::{func, Str}; + +/// Creates blind text. +/// +/// This function yields a Latin-like _Lorem Ipsum_ blind text with the given +/// number of words. The sequence of words generated by the function is always +/// the same but randomly chosen. As usual for blind texts, it does not make any +/// sense. Use it as a placeholder to try layouts. +/// +/// # Example +/// ```example +/// = Blind Text +/// #lorem(30) +/// +/// = More Blind Text +/// #lorem(15) +/// ``` +#[func(keywords = ["Blind Text"])] +pub fn lorem( + /// The length of the blind text in words. + words: usize, +) -> Str { + lipsum::lipsum(words).replace("--", "–").into() +} diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs new file mode 100644 index 00000000..acf100b5 --- /dev/null +++ b/crates/typst-library/src/text/mod.rs @@ -0,0 +1,1318 @@ +//! Text handling. + +mod case; +mod deco; +mod font; +mod item; +mod lang; +mod linebreak; +#[path = "lorem.rs"] +mod lorem_; +mod raw; +mod shift; +#[path = "smallcaps.rs"] +mod smallcaps_; +mod smartquote; +mod space; + +pub use self::case::*; +pub use self::deco::*; +pub use self::font::*; +pub use self::item::*; +pub use self::lang::*; +pub use self::linebreak::*; +pub use self::lorem_::*; +pub use self::raw::*; +pub use self::shift::*; +pub use self::smallcaps_::*; +pub use self::smartquote::*; +pub use self::space::*; + +use std::fmt::{self, Debug, Formatter}; + +use ecow::{eco_format, EcoString}; +use icu_properties::sets::CodePointSetData; +use icu_provider::AsDeserializingBufferProvider; +use icu_provider_blob::BlobDataProvider; +use once_cell::sync::Lazy; +use rustybuzz::Feature; +use smallvec::SmallVec; +use ttf_parser::Tag; +use typst_syntax::Spanned; + +use crate::diag::{bail, warning, HintedStrResult, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, category, dict, elem, Args, Array, Cast, Category, Construct, Content, Dict, + Fold, IntoValue, NativeElement, Never, NoneValue, Packed, PlainText, Repr, Resolve, + Scope, Set, Smart, StyleChain, +}; +use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel}; +use crate::model::ParElem; +use crate::visualize::{Color, Paint, RelativeTo, Stroke}; +use crate::World; + +/// Text styling. +/// +/// The [text function]($text) is of particular interest. +#[category] +pub static TEXT: Category; + +/// Hook up all `text` definitions. +pub(super) fn define(global: &mut Scope) { + global.category(TEXT); + global.define_elem::<TextElem>(); + global.define_elem::<LinebreakElem>(); + global.define_elem::<SmartQuoteElem>(); + global.define_elem::<SubElem>(); + global.define_elem::<SuperElem>(); + global.define_elem::<UnderlineElem>(); + global.define_elem::<OverlineElem>(); + global.define_elem::<StrikeElem>(); + global.define_elem::<HighlightElem>(); + global.define_elem::<SmallcapsElem>(); + global.define_elem::<RawElem>(); + global.define_func::<lower>(); + global.define_func::<upper>(); + global.define_func::<lorem>(); +} + +/// Customizes the look and layout of text in a variety of ways. +/// +/// This function is used frequently, both with set rules and directly. While +/// the set rule is often the simpler choice, calling the `text` function +/// directly can be useful when passing text as an argument to another function. +/// +/// # Example +/// ```example +/// #set text(18pt) +/// With a set rule. +/// +/// #emph(text(blue)[ +/// With a function call. +/// ]) +/// ``` +#[elem(Debug, Construct, PlainText, Repr)] +pub struct TextElem { + /// A font family name or priority list of font family names. + /// + /// When processing text, Typst tries all specified font families in order + /// until it finds a font that has the necessary glyphs. In the example + /// below, the font `Inria Serif` is preferred, but since it does not + /// contain Arabic glyphs, the arabic text uses `Noto Sans Arabic` instead. + /// + /// The collection of available fonts differs by platform: + /// + /// - In the web app, you can see the list of available fonts by clicking on + /// the "Ag" button. You can provide additional fonts by uploading `.ttf` + /// or `.otf` files into your project. They will be discovered + /// automatically. The priority is: project fonts > server fonts. + /// + /// - Locally, Typst uses your installed system fonts or embedded fonts in + /// the CLI, which are `Libertinus Serif`, `New Computer Modern`, + /// `New Computer Modern Math`, and `DejaVu Sans Mono`. In addition, you + /// can use the `--font-path` argument or `TYPST_FONT_PATHS` environment + /// variable to add directories that should be scanned for fonts. The + /// priority is: `--font-paths` > system fonts > embedded fonts. Run + /// `typst fonts` to see the fonts that Typst has discovered on your + /// system. Note that you can pass the `--ignore-system-fonts` parameter + /// to the CLI to ensure Typst won't search for system fonts. + /// + /// ```example + /// #set text(font: "PT Sans") + /// This is sans-serif. + /// + /// #set text(font: ( + /// "Inria Serif", + /// "Noto Sans Arabic", + /// )) + /// + /// This is Latin. \ + /// هذا عربي. + /// ``` + #[parse({ + let font_list: Option<Spanned<FontList>> = args.named("font")?; + if let Some(list) = &font_list { + check_font_list(engine, list); + } + font_list.map(|font_list| font_list.v) + })] + #[default(FontList(vec![FontFamily::new("Libertinus Serif")]))] + #[borrowed] + #[ghost] + pub font: FontList, + + /// Whether to allow last resort font fallback when the primary font list + /// contains no match. This lets Typst search through all available fonts + /// for the most similar one that has the necessary glyphs. + /// + /// _Note:_ Currently, there are no warnings when fallback is disabled and + /// no glyphs are found. Instead, your text shows up in the form of "tofus": + /// Small boxes that indicate the lack of an appropriate glyph. In the + /// future, you will be able to instruct Typst to issue warnings so you know + /// something is up. + /// + /// ```example + /// #set text(font: "Inria Serif") + /// هذا عربي + /// + /// #set text(fallback: false) + /// هذا عربي + /// ``` + #[default(true)] + #[ghost] + pub fallback: bool, + + /// The desired font style. + /// + /// When an italic style is requested and only an oblique one is available, + /// it is used. Similarly, the other way around, an italic style can stand + /// in for an oblique one. When neither an italic nor an oblique style is + /// available, Typst selects the normal style. Since most fonts are only + /// available either in an italic or oblique style, the difference between + /// italic and oblique style is rarely observable. + /// + /// If you want to emphasize your text, you should do so using the [emph] + /// function instead. This makes it easy to adapt the style later if you + /// change your mind about how to signify the emphasis. + /// + /// ```example + /// #text(font: "Libertinus Serif", style: "italic")[Italic] + /// #text(font: "DejaVu Sans", style: "oblique")[Oblique] + /// ``` + #[ghost] + pub style: FontStyle, + + /// The desired thickness of the font's glyphs. Accepts an integer between + /// `{100}` and `{900}` or one of the predefined weight names. When the + /// desired weight is not available, Typst selects the font from the family + /// that is closest in weight. + /// + /// If you want to strongly emphasize your text, you should do so using the + /// [strong] function instead. This makes it easy to adapt the style later + /// if you change your mind about how to signify the strong emphasis. + /// + /// ```example + /// #set text(font: "IBM Plex Sans") + /// + /// #text(weight: "light")[Light] \ + /// #text(weight: "regular")[Regular] \ + /// #text(weight: "medium")[Medium] \ + /// #text(weight: 500)[Medium] \ + /// #text(weight: "bold")[Bold] + /// ``` + #[ghost] + pub weight: FontWeight, + + /// The desired width of the glyphs. Accepts a ratio between `{50%}` and + /// `{200%}`. When the desired width is not available, Typst selects the + /// font from the family that is closest in stretch. This will only stretch + /// the text if a condensed or expanded version of the font is available. + /// + /// If you want to adjust the amount of space between characters instead of + /// stretching the glyphs itself, use the [`tracking`]($text.tracking) + /// property instead. + /// + /// ```example + /// #text(stretch: 75%)[Condensed] \ + /// #text(stretch: 100%)[Normal] + /// ``` + #[ghost] + pub stretch: FontStretch, + + /// The size of the glyphs. This value forms the basis of the `em` unit: + /// `{1em}` is equivalent to the font size. + /// + /// You can also give the font size itself in `em` units. Then, it is + /// relative to the previous font size. + /// + /// ```example + /// #set text(size: 20pt) + /// very #text(1.5em)[big] text + /// ``` + #[parse(args.named_or_find("size")?)] + #[fold] + #[default(TextSize(Abs::pt(11.0).into()))] + #[resolve] + #[ghost] + pub size: TextSize, + + /// The glyph fill paint. + /// + /// ```example + /// #set text(fill: red) + /// This text is red. + /// ``` + #[parse({ + let paint: Option<Spanned<Paint>> = args.named_or_find("fill")?; + if let Some(paint) = &paint { + if paint.v.relative() == Smart::Custom(RelativeTo::Self_) { + bail!( + paint.span, + "gradients and patterns on text must be relative to the parent"; + hint: "make sure to set `relative: auto` on your text fill" + ); + } + } + paint.map(|paint| paint.v) + })] + #[default(Color::BLACK.into())] + #[ghost] + pub fill: Paint, + + /// How to stroke the text. + /// + /// ```example + /// #text(stroke: 0.5pt + red)[Stroked] + /// ``` + #[resolve] + #[ghost] + pub stroke: Option<Stroke>, + + /// The amount of space that should be added between characters. + /// + /// ```example + /// #set text(tracking: 1.5pt) + /// Distant text. + /// ``` + #[resolve] + #[ghost] + pub tracking: Length, + + /// The amount of space between words. + /// + /// Can be given as an absolute length, but also relative to the width of + /// the space character in the font. + /// + /// If you want to adjust the amount of space between characters rather than + /// words, use the [`tracking`]($text.tracking) property instead. + /// + /// ```example + /// #set text(spacing: 200%) + /// Text with distant words. + /// ``` + #[resolve] + #[default(Rel::one())] + #[ghost] + pub spacing: Rel<Length>, + + /// Whether to automatically insert spacing between CJK and Latin characters. + /// + /// ```example + /// #set text(cjk-latin-spacing: auto) + /// 第4章介绍了基本的API。 + /// + /// #set text(cjk-latin-spacing: none) + /// 第4章介绍了基本的API。 + /// ``` + #[ghost] + pub cjk_latin_spacing: Smart<Option<Never>>, + + /// An amount to shift the text baseline by. + /// + /// ```example + /// A #text(baseline: 3pt)[lowered] + /// word. + /// ``` + #[resolve] + #[ghost] + pub baseline: Length, + + /// Whether certain glyphs can hang over into the margin in justified text. + /// This can make justification visually more pleasing. + /// + /// ```example + /// #set par(justify: true) + /// This justified text has a hyphen in + /// the paragraph's first line. Hanging + /// the hyphen slightly into the margin + /// results in a clearer paragraph edge. + /// + /// #set text(overhang: false) + /// This justified text has a hyphen in + /// the paragraph's first line. Hanging + /// the hyphen slightly into the margin + /// results in a clearer paragraph edge. + /// ``` + #[default(true)] + #[ghost] + pub overhang: bool, + + /// The top end of the conceptual frame around the text used for layout and + /// positioning. This affects the size of containers that hold text. + /// + /// ```example + /// #set rect(inset: 0pt) + /// #set text(size: 20pt) + /// + /// #set text(top-edge: "ascender") + /// #rect(fill: aqua)[Typst] + /// + /// #set text(top-edge: "cap-height") + /// #rect(fill: aqua)[Typst] + /// ``` + #[default(TopEdge::Metric(TopEdgeMetric::CapHeight))] + #[ghost] + pub top_edge: TopEdge, + + /// The bottom end of the conceptual frame around the text used for layout + /// and positioning. This affects the size of containers that hold text. + /// + /// ```example + /// #set rect(inset: 0pt) + /// #set text(size: 20pt) + /// + /// #set text(bottom-edge: "baseline") + /// #rect(fill: aqua)[Typst] + /// + /// #set text(bottom-edge: "descender") + /// #rect(fill: aqua)[Typst] + /// ``` + #[default(BottomEdge::Metric(BottomEdgeMetric::Baseline))] + #[ghost] + pub bottom_edge: BottomEdge, + + /// An [ISO 639-1/2/3 language code.](https://en.wikipedia.org/wiki/ISO_639) + /// + /// Setting the correct language affects various parts of Typst: + /// + /// - The text processing pipeline can make more informed choices. + /// - Hyphenation will use the correct patterns for the language. + /// - [Smart quotes]($smartquote) turns into the correct quotes for the + /// language. + /// - And all other things which are language-aware. + /// + /// ```example + /// #set text(lang: "de") + /// #outline() + /// + /// = Einleitung + /// In diesem Dokument, ... + /// ``` + #[default(Lang::ENGLISH)] + #[ghost] + pub lang: Lang, + + /// An [ISO 3166-1 alpha-2 region code.](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) + /// + /// This lets the text processing pipeline make more informed choices. + #[ghost] + pub region: Option<Region>, + + /// The OpenType writing script. + /// + /// The combination of `{lang}` and `{script}` determine how font features, + /// such as glyph substitution, are implemented. Frequently the value is a + /// modified (all-lowercase) ISO 15924 script identifier, and the `math` + /// writing script is used for features appropriate for mathematical + /// symbols. + /// + /// When set to `{auto}`, the default and recommended setting, an + /// appropriate script is chosen for each block of characters sharing a + /// common Unicode script property. + /// + /// ```example + /// #set text( + /// font: "Libertinus Serif", + /// size: 20pt, + /// ) + /// + /// #let scedilla = [Ş] + /// #scedilla // S with a cedilla + /// + /// #set text(lang: "ro", script: "latn") + /// #scedilla // S with a subscript comma + /// + /// #set text(lang: "ro", script: "grek") + /// #scedilla // S with a cedilla + /// ``` + #[ghost] + pub script: Smart<WritingScript>, + + /// The dominant direction for text and inline objects. Possible values are: + /// + /// - `{auto}`: Automatically infer the direction from the `lang` property. + /// - `{ltr}`: Layout text from left to right. + /// - `{rtl}`: Layout text from right to left. + /// + /// When writing in right-to-left scripts like Arabic or Hebrew, you should + /// set the [text language]($text.lang) or direction. While individual runs + /// of text are automatically layouted in the correct direction, setting the + /// dominant direction gives the bidirectional reordering algorithm the + /// necessary information to correctly place punctuation and inline objects. + /// Furthermore, setting the direction affects the alignment values `start` + /// and `end`, which are equivalent to `left` and `right` in `ltr` text and + /// the other way around in `rtl` text. + /// + /// If you set this to `rtl` and experience bugs or in some way bad looking + /// output, please get in touch with us through the + /// [Forum](https://forum.typst.app/), + /// [Discord server](https://discord.gg/2uDybryKPe), + /// or our [contact form](https://typst.app/contact). + /// + /// ```example + /// #set text(dir: rtl) + /// هذا عربي. + /// ``` + #[resolve] + #[ghost] + pub dir: TextDir, + + /// Whether to hyphenate text to improve line breaking. When `{auto}`, text + /// will be hyphenated if and only if justification is enabled. + /// + /// Setting the [text language]($text.lang) ensures that the correct + /// hyphenation patterns are used. + /// + /// ```example + /// #set page(width: 200pt) + /// + /// #set par(justify: true) + /// This text illustrates how + /// enabling hyphenation can + /// improve justification. + /// + /// #set text(hyphenate: false) + /// This text illustrates how + /// enabling hyphenation can + /// improve justification. + /// ``` + #[resolve] + #[ghost] + pub hyphenate: Hyphenate, + + /// The "cost" of various choices when laying out text. A higher cost means + /// the layout engine will make the choice less often. Costs are specified + /// as a ratio of the default cost, so `{50%}` will make text layout twice + /// as eager to make a given choice, while `{200%}` will make it half as + /// eager. + /// + /// Currently, the following costs can be customized: + /// - `hyphenation`: splitting a word across multiple lines + /// - `runt`: ending a paragraph with a line with a single word + /// - `widow`: leaving a single line of paragraph on the next page + /// - `orphan`: leaving single line of paragraph on the previous page + /// + /// Hyphenation is generally avoided by placing the whole word on the next + /// line, so a higher hyphenation cost can result in awkward justification + /// spacing. + /// + /// Runts are avoided by placing more or fewer words on previous lines, so a + /// higher runt cost can result in more awkward in justification spacing. + /// + /// Text layout prevents widows and orphans by default because they are + /// generally discouraged by style guides. However, in some contexts they + /// are allowed because the prevention method, which moves a line to the + /// next page, can result in an uneven number of lines between pages. The + /// `widow` and `orphan` costs allow disabling these modifications. + /// (Currently, `{0%}` allows widows/orphans; anything else, including the + /// default of `{100%}`, prevents them. More nuanced cost specification for + /// these modifications is planned for the future.) + /// + /// ```example + /// #set text(hyphenate: true, size: 11.4pt) + /// #set par(justify: true) + /// + /// #lorem(10) + /// + /// // Set hyphenation to ten times the normal cost. + /// #set text(costs: (hyphenation: 1000%)) + /// + /// #lorem(10) + /// ``` + #[fold] + pub costs: Costs, + + /// Whether to apply kerning. + /// + /// When enabled, specific letter pairings move closer together or further + /// apart for a more visually pleasing result. The example below + /// demonstrates how decreasing the gap between the "T" and "o" results in a + /// more natural look. Setting this to `{false}` disables kerning by turning + /// off the OpenType `kern` font feature. + /// + /// ```example + /// #set text(size: 25pt) + /// Totally + /// + /// #set text(kerning: false) + /// Totally + /// ``` + #[default(true)] + #[ghost] + pub kerning: bool, + + /// Whether to apply stylistic alternates. + /// + /// Sometimes fonts contain alternative glyphs for the same codepoint. + /// Setting this to `{true}` switches to these by enabling the OpenType + /// `salt` font feature. + /// + /// ```example + /// #set text( + /// font: "IBM Plex Sans", + /// size: 20pt, + /// ) + /// + /// 0, a, g, ß + /// + /// #set text(alternates: true) + /// 0, a, g, ß + /// ``` + #[default(false)] + #[ghost] + pub alternates: bool, + + /// Which stylistic sets to apply. Font designers can categorize alternative + /// glyphs forms into stylistic sets. As this value is highly font-specific, + /// you need to consult your font to know which sets are available. + /// + /// This can be set to an integer or an array of integers, all + /// of which must be between `{1}` and `{20}`, enabling the + /// corresponding OpenType feature(s) from `ss01` to `ss20`. + /// Setting this to `{none}` will disable all stylistic sets. + /// + /// ```example + /// #set text(font: "IBM Plex Serif") + /// ß vs #text(stylistic-set: 5)[ß] \ + /// 10 years ago vs #text(stylistic-set: (1, 2, 3))[10 years ago] + /// ``` + #[ghost] + pub stylistic_set: StylisticSets, + + /// Whether standard ligatures are active. + /// + /// Certain letter combinations like "fi" are often displayed as a single + /// merged glyph called a _ligature._ Setting this to `{false}` disables + /// these ligatures by turning off the OpenType `liga` and `clig` font + /// features. + /// + /// ```example + /// #set text(size: 20pt) + /// A fine ligature. + /// + /// #set text(ligatures: false) + /// A fine ligature. + /// ``` + #[default(true)] + #[ghost] + pub ligatures: bool, + + /// Whether ligatures that should be used sparingly are active. Setting this + /// to `{true}` enables the OpenType `dlig` font feature. + #[default(false)] + #[ghost] + pub discretionary_ligatures: bool, + + /// Whether historical ligatures are active. Setting this to `{true}` + /// enables the OpenType `hlig` font feature. + #[default(false)] + #[ghost] + pub historical_ligatures: bool, + + /// Which kind of numbers / figures to select. When set to `{auto}`, the + /// default numbers for the font are used. + /// + /// ```example + /// #set text(font: "Noto Sans", 20pt) + /// #set text(number-type: "lining") + /// Number 9. + /// + /// #set text(number-type: "old-style") + /// Number 9. + /// ``` + #[ghost] + pub number_type: Smart<NumberType>, + + /// The width of numbers / figures. When set to `{auto}`, the default + /// numbers for the font are used. + /// + /// ```example + /// #set text(font: "Noto Sans", 20pt) + /// #set text(number-width: "proportional") + /// A 12 B 34. \ + /// A 56 B 78. + /// + /// #set text(number-width: "tabular") + /// A 12 B 34. \ + /// A 56 B 78. + /// ``` + #[ghost] + pub number_width: Smart<NumberWidth>, + + /// Whether to have a slash through the zero glyph. Setting this to `{true}` + /// enables the OpenType `zero` font feature. + /// + /// ```example + /// 0, #text(slashed-zero: true)[0] + /// ``` + #[default(false)] + #[ghost] + pub slashed_zero: bool, + + /// Whether to turn numbers into fractions. Setting this to `{true}` + /// enables the OpenType `frac` font feature. + /// + /// It is not advisable to enable this property globally as it will mess + /// with all appearances of numbers after a slash (e.g., in URLs). Instead, + /// enable it locally when you want a fraction. + /// + /// ```example + /// 1/2 \ + /// #text(fractions: true)[1/2] + /// ``` + #[default(false)] + #[ghost] + pub fractions: bool, + + /// Raw OpenType features to apply. + /// + /// - If given an array of strings, sets the features identified by the + /// strings to `{1}`. + /// - If given a dictionary mapping to numbers, sets the features + /// identified by the keys to the values. + /// + /// ```example + /// // Enable the `frac` feature manually. + /// #set text(features: ("frac",)) + /// 1/2 + /// ``` + #[fold] + #[ghost] + pub features: FontFeatures, + + /// Content in which all text is styled according to the other arguments. + #[external] + #[required] + pub body: Content, + + /// The text. + #[required] + pub text: EcoString, + + /// The offset of the text in the text syntax node referenced by this + /// element's span. + #[internal] + #[ghost] + pub span_offset: usize, + + /// A delta to apply on the font weight. + #[internal] + #[fold] + #[ghost] + pub delta: WeightDelta, + + /// Whether the font style should be inverted. + #[internal] + #[fold] + #[default(ItalicToggle(false))] + #[ghost] + pub emph: ItalicToggle, + + /// Decorative lines. + #[internal] + #[fold] + #[ghost] + pub deco: SmallVec<[Decoration; 1]>, + + /// A case transformation that should be applied to the text. + #[internal] + #[ghost] + pub case: Option<Case>, + + /// Whether small capital glyphs should be used. ("smcp") + #[internal] + #[default(false)] + #[ghost] + pub smallcaps: bool, +} + +impl TextElem { + /// Create a new packed text element. + pub fn packed(text: impl Into<EcoString>) -> Content { + Self::new(text.into()).pack() + } +} + +impl Debug for TextElem { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Text({})", self.text) + } +} + +impl Repr for TextElem { + fn repr(&self) -> EcoString { + eco_format!("[{}]", self.text) + } +} + +impl Construct for TextElem { + fn construct(engine: &mut Engine, args: &mut Args) -> SourceResult<Content> { + // The text constructor is special: It doesn't create a text element. + // Instead, it leaves the passed argument structurally unchanged, but + // styles all text in it. + let styles = Self::set(engine, args)?; + let body = args.expect::<Content>("body")?; + Ok(body.styled_with_map(styles)) + } +} + +impl PlainText for Packed<TextElem> { + fn plain_text(&self, text: &mut EcoString) { + text.push_str(self.text()); + } +} + +/// A lowercased font family like "arial". +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct FontFamily(EcoString); + +impl FontFamily { + /// Create a named font family variant. + pub fn new(string: &str) -> Self { + Self(string.to_lowercase().into()) + } + + /// The lowercased family name. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl Debug for FontFamily { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +cast! { + FontFamily, + self => self.0.into_value(), + string: EcoString => Self::new(&string), +} + +/// Font family fallback list. +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct FontList(pub Vec<FontFamily>); + +impl<'a> IntoIterator for &'a FontList { + type IntoIter = std::slice::Iter<'a, FontFamily>; + type Item = &'a FontFamily; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +cast! { + FontList, + self => if self.0.len() == 1 { + self.0.into_iter().next().unwrap().0.into_value() + } else { + self.0.into_value() + }, + family: FontFamily => Self(vec![family]), + values: Array => Self(values.into_iter().map(|v| v.cast()).collect::<HintedStrResult<_>>()?), +} + +/// Resolve a prioritized iterator over the font families. +pub fn families(styles: StyleChain) -> impl Iterator<Item = &str> + Clone { + const FALLBACKS: &[&str] = &[ + "libertinus serif", + "twitter color emoji", + "noto color emoji", + "apple color emoji", + "segoe ui emoji", + ]; + + let tail = if TextElem::fallback_in(styles) { FALLBACKS } else { &[] }; + TextElem::font_in(styles) + .into_iter() + .map(|family| family.as_str()) + .chain(tail.iter().copied()) +} + +/// Resolve the font variant. +pub fn variant(styles: StyleChain) -> FontVariant { + let mut variant = FontVariant::new( + TextElem::style_in(styles), + TextElem::weight_in(styles), + TextElem::stretch_in(styles), + ); + + let WeightDelta(delta) = TextElem::delta_in(styles); + variant.weight = variant + .weight + .thicken(delta.clamp(i16::MIN as i64, i16::MAX as i64) as i16); + + if TextElem::emph_in(styles).0 { + variant.style = match variant.style { + FontStyle::Normal => FontStyle::Italic, + FontStyle::Italic => FontStyle::Normal, + FontStyle::Oblique => FontStyle::Normal, + } + } + + variant +} + +/// The size of text. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct TextSize(pub Length); + +impl Fold for TextSize { + fn fold(self, outer: Self) -> Self { + // Multiply the two linear functions. + Self(Length { + em: Em::new(self.0.em.get() * outer.0.em.get()), + abs: self.0.em.get() * outer.0.abs + self.0.abs, + }) + } +} + +impl Resolve for TextSize { + type Output = Abs; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.0.resolve(styles) + } +} + +cast! { + TextSize, + self => self.0.into_value(), + v: Length => Self(v), +} + +/// Specifies the top edge of text. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum TopEdge { + /// An edge specified via font metrics or bounding box. + Metric(TopEdgeMetric), + /// An edge specified as a length. + Length(Length), +} + +cast! { + TopEdge, + self => match self { + Self::Metric(metric) => metric.into_value(), + Self::Length(length) => length.into_value(), + }, + v: TopEdgeMetric => Self::Metric(v), + v: Length => Self::Length(v), +} + +/// Metrics that describe the top edge of text. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum TopEdgeMetric { + /// 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 top edge of the glyph's bounding box. + Bounds, +} + +impl TryInto<VerticalFontMetric> for TopEdgeMetric { + type Error = (); + + fn try_into(self) -> Result<VerticalFontMetric, Self::Error> { + match self { + Self::Ascender => Ok(VerticalFontMetric::Ascender), + Self::CapHeight => Ok(VerticalFontMetric::CapHeight), + Self::XHeight => Ok(VerticalFontMetric::XHeight), + Self::Baseline => Ok(VerticalFontMetric::Baseline), + _ => Err(()), + } + } +} + +/// Specifies the top edge of text. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum BottomEdge { + /// An edge specified via font metrics or bounding box. + Metric(BottomEdgeMetric), + /// An edge specified as a length. + Length(Length), +} + +cast! { + BottomEdge, + self => match self { + Self::Metric(metric) => metric.into_value(), + Self::Length(length) => length.into_value(), + }, + v: BottomEdgeMetric => Self::Metric(v), + v: Length => Self::Length(v), +} + +/// Metrics that describe the bottom edge of text. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum BottomEdgeMetric { + /// The baseline on which the letters rest. + Baseline, + /// The font's descender, which typically exceeds the depth of all glyphs. + Descender, + /// The bottom edge of the glyph's bounding box. + Bounds, +} + +impl TryInto<VerticalFontMetric> for BottomEdgeMetric { + type Error = (); + + fn try_into(self) -> Result<VerticalFontMetric, Self::Error> { + match self { + Self::Baseline => Ok(VerticalFontMetric::Baseline), + Self::Descender => Ok(VerticalFontMetric::Descender), + _ => Err(()), + } + } +} + +/// The direction of text and inline objects in their line. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct TextDir(pub Smart<Dir>); + +cast! { + TextDir, + self => self.0.into_value(), + v: Smart<Dir> => { + if v.is_custom_and(|dir| dir.axis() == Axis::Y) { + bail!("text direction must be horizontal"); + } + Self(v) + }, +} + +impl Resolve for TextDir { + type Output = Dir; + + fn resolve(self, styles: StyleChain) -> Self::Output { + match self.0 { + Smart::Auto => TextElem::lang_in(styles).dir(), + Smart::Custom(dir) => dir, + } + } +} + +/// Whether to hyphenate text. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Hyphenate(pub Smart<bool>); + +cast! { + Hyphenate, + self => self.0.into_value(), + v: Smart<bool> => Self(v), +} + +impl Resolve for Hyphenate { + type Output = bool; + + fn resolve(self, styles: StyleChain) -> Self::Output { + match self.0 { + Smart::Auto => ParElem::justify_in(styles), + Smart::Custom(v) => v, + } + } +} + +/// A set of stylistic sets to enable. +#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)] +pub struct StylisticSets(u32); + +impl StylisticSets { + /// Converts this set into a Typst array of values. + pub fn into_array(self) -> Array { + self.sets().map(IntoValue::into_value).collect() + } + + /// Returns whether this set contains a particular stylistic set. + pub fn has(self, ss: u8) -> bool { + self.0 & (1 << (ss as u32)) != 0 + } + + /// Returns an iterator over all stylistic sets to enable. + pub fn sets(self) -> impl Iterator<Item = u8> { + (1..=20).filter(move |i| self.has(*i)) + } +} + +cast! { + StylisticSets, + self => self.into_array().into_value(), + _: NoneValue => Self(0), + v: i64 => match v { + 1 ..= 20 => Self(1 << (v as u32)), + _ => bail!("stylistic set must be between 1 and 20"), + }, + v: Vec<i64> => { + let mut flags = 0; + for i in v { + match i { + 1 ..= 20 => flags |= 1 << (i as u32), + _ => bail!("stylistic set must be between 1 and 20"), + } + } + Self(flags) + }, +} + +/// Which kind of numbers / figures to select. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum NumberType { + /// Numbers that fit well with capital text (the OpenType `lnum` + /// font feature). + Lining, + /// Numbers that fit well into a flow of upper- and lowercase text (the + /// OpenType `onum` font feature). + OldStyle, +} + +/// The width of numbers / figures. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum NumberWidth { + /// Numbers with glyph-specific widths (the OpenType `pnum` font feature). + Proportional, + /// Numbers of equal width (the OpenType `tnum` font feature). + Tabular, +} + +/// OpenType font features settings. +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct FontFeatures(pub Vec<(Tag, u32)>); + +cast! { + FontFeatures, + self => self.0 + .into_iter() + .map(|(tag, num)| { + let bytes = tag.to_bytes(); + let key = std::str::from_utf8(&bytes).unwrap_or_default(); + (key.into(), num.into_value()) + }) + .collect::<Dict>() + .into_value(), + values: Array => Self(values + .into_iter() + .map(|v| { + let tag = v.cast::<EcoString>()?; + Ok((Tag::from_bytes_lossy(tag.as_bytes()), 1)) + }) + .collect::<HintedStrResult<_>>()?), + values: Dict => Self(values + .into_iter() + .map(|(k, v)| { + let num = v.cast::<u32>()?; + let tag = Tag::from_bytes_lossy(k.as_bytes()); + Ok((tag, num)) + }) + .collect::<HintedStrResult<_>>()?), +} + +impl Fold for FontFeatures { + fn fold(self, outer: Self) -> Self { + Self(self.0.fold(outer.0)) + } +} + +/// Collect the OpenType features to apply. +pub fn features(styles: StyleChain) -> Vec<Feature> { + let mut tags = vec![]; + let mut feat = |tag: &[u8; 4], value: u32| { + tags.push(Feature::new(Tag::from_bytes(tag), value, ..)); + }; + + // Features that are on by default in Harfbuzz are only added if disabled. + if !TextElem::kerning_in(styles) { + feat(b"kern", 0); + } + + // Features that are off by default in Harfbuzz are only added if enabled. + if TextElem::smallcaps_in(styles) { + feat(b"smcp", 1); + } + + if TextElem::alternates_in(styles) { + feat(b"salt", 1); + } + + for set in TextElem::stylistic_set_in(styles).sets() { + let storage = [b's', b's', b'0' + set / 10, b'0' + set % 10]; + feat(&storage, 1); + } + + if !TextElem::ligatures_in(styles) { + feat(b"liga", 0); + feat(b"clig", 0); + } + + if TextElem::discretionary_ligatures_in(styles) { + feat(b"dlig", 1); + } + + if TextElem::historical_ligatures_in(styles) { + feat(b"hlig", 1); + } + + match TextElem::number_type_in(styles) { + Smart::Auto => {} + Smart::Custom(NumberType::Lining) => feat(b"lnum", 1), + Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1), + } + + match TextElem::number_width_in(styles) { + Smart::Auto => {} + Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1), + Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1), + } + + if TextElem::slashed_zero_in(styles) { + feat(b"zero", 1); + } + + if TextElem::fractions_in(styles) { + feat(b"frac", 1); + } + + for (tag, value) in TextElem::features_in(styles).0 { + tags.push(Feature::new(tag, value, ..)) + } + + tags +} + +/// A toggle that turns on and off alternatingly if folded. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct ItalicToggle(pub bool); + +impl Fold for ItalicToggle { + fn fold(self, outer: Self) -> Self { + Self(self.0 ^ outer.0) + } +} + +/// A delta that is summed up when folded. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct WeightDelta(pub i64); + +impl Fold for WeightDelta { + fn fold(self, outer: Self) -> Self { + Self(outer.0 + self.0) + } +} + +/// Costs for various layout decisions. +/// +/// Costs are updated (prioritizing the later value) when folded. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +#[non_exhaustive] +pub struct Costs { + hyphenation: Option<Ratio>, + runt: Option<Ratio>, + widow: Option<Ratio>, + orphan: Option<Ratio>, +} + +impl Costs { + #[must_use] + pub fn hyphenation(&self) -> Ratio { + self.hyphenation.unwrap_or(Ratio::one()) + } + + #[must_use] + pub fn runt(&self) -> Ratio { + self.runt.unwrap_or(Ratio::one()) + } + + #[must_use] + pub fn widow(&self) -> Ratio { + self.widow.unwrap_or(Ratio::one()) + } + + #[must_use] + pub fn orphan(&self) -> Ratio { + self.orphan.unwrap_or(Ratio::one()) + } +} + +impl Fold for Costs { + #[inline] + fn fold(self, outer: Self) -> Self { + Self { + hyphenation: self.hyphenation.or(outer.hyphenation), + runt: self.runt.or(outer.runt), + widow: self.widow.or(outer.widow), + orphan: self.orphan.or(outer.orphan), + } + } +} + +cast! { + Costs, + self => dict![ + "hyphenation" => self.hyphenation(), + "runt" => self.runt(), + "widow" => self.widow(), + "orphan" => self.orphan(), + ].into_value(), + mut v: Dict => { + let ret = Self { + hyphenation: v.take("hyphenation").ok().map(|v| v.cast()).transpose()?, + runt: v.take("runt").ok().map(|v| v.cast()).transpose()?, + widow: v.take("widow").ok().map(|v| v.cast()).transpose()?, + orphan: v.take("orphan").ok().map(|v| v.cast()).transpose()?, + }; + v.finish(&["hyphenation", "runt", "widow", "orphan"])?; + ret + }, +} + +/// Whether a codepoint is Unicode `Default_Ignorable`. +pub fn is_default_ignorable(c: char) -> bool { + /// The set of Unicode default ignorables. + static DEFAULT_IGNORABLE_DATA: Lazy<CodePointSetData> = Lazy::new(|| { + icu_properties::sets::load_default_ignorable_code_point( + &BlobDataProvider::try_new_from_static_blob(typst_assets::icu::ICU) + .unwrap() + .as_deserializing(), + ) + .unwrap() + }); + DEFAULT_IGNORABLE_DATA.as_borrowed().contains(c) +} + +/// Checks for font families that are not available. +fn check_font_list(engine: &mut Engine, list: &Spanned<FontList>) { + let book = engine.world.book(); + for family in &list.v { + let found = book.contains_family(family.as_str()); + if family.as_str() == "linux libertine" { + let mut warning = warning!( + list.span, + "Typst's default font has changed from Linux Libertine to its successor Libertinus Serif"; + hint: "please set the font to `\"Libertinus Serif\"` instead" + ); + + if found { + warning.hint( + "Linux Libertine is available on your system - \ + you can ignore this warning if you are sure you want to use it", + ); + warning.hint("this warning will be removed in Typst 0.13"); + } + + engine.sink.warn(warning); + } else if !found { + engine.sink.warn(warning!( + list.span, + "unknown font family: {}", + family.as_str(), + )); + } + } +} diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs new file mode 100644 index 00000000..5ce77348 --- /dev/null +++ b/crates/typst-library/src/text/raw.rs @@ -0,0 +1,926 @@ +use std::hash::Hash; +use std::ops::Range; +use std::sync::Arc; + +use ecow::{eco_format, EcoString, EcoVec}; +use once_cell::sync::Lazy; +use once_cell::unsync::Lazy as UnsyncLazy; +use syntect::highlighting::{self as synt, Theme}; +use syntect::parsing::{SyntaxDefinition, SyntaxSet, SyntaxSetBuilder}; +use typst_syntax::{split_newlines, LinkedNode, Span, Spanned}; +use unicode_segmentation::UnicodeSegmentation; + +use super::Lang; +use crate::diag::{At, FileError, HintedStrResult, SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, scope, Args, Array, Bytes, Content, Fold, NativeElement, Packed, + PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, Value, +}; +use crate::layout::{BlockBody, BlockElem, Em, HAlignment}; +use crate::model::{Figurable, ParElem}; +use crate::text::{ + FontFamily, FontList, Hyphenate, LinebreakElem, LocalName, TextElem, TextSize, +}; +use crate::visualize::Color; +use crate::World; + +// Shorthand for highlighter closures. +type StyleFn<'a> = + &'a mut dyn FnMut(usize, &LinkedNode, Range<usize>, synt::Style) -> Content; +type LineFn<'a> = &'a mut dyn FnMut(usize, Range<usize>, &mut Vec<Content>); +type ThemeArgType = Smart<Option<EcoString>>; + +/// Raw text with optional syntax highlighting. +/// +/// Displays the text verbatim and in a monospace font. This is typically used +/// to embed computer code into your document. +/// +/// # Example +/// ````example +/// Adding `rbx` to `rcx` gives +/// the desired result. +/// +/// What is ```rust fn main()``` in Rust +/// would be ```c int main()``` in C. +/// +/// ```rust +/// fn main() { +/// println!("Hello World!"); +/// } +/// ``` +/// +/// This has ``` `backticks` ``` in it +/// (but the spaces are trimmed). And +/// ``` here``` the leading space is +/// also trimmed. +/// ```` +/// +/// You can also construct a [`raw`] element programmatically from a string (and +/// provide the language tag via the optional [`lang`]($raw.lang) argument). +/// ```example +/// #raw("fn " + "main() {}", lang: "rust") +/// ``` +/// +/// # Syntax +/// This function also has dedicated syntax. You can enclose text in 1 or 3+ +/// backticks (`` ` ``) to make it raw. Two backticks produce empty raw text. +/// This works both in markup and code. +/// +/// When you use three or more backticks, you can additionally specify a +/// language tag for syntax highlighting directly after the opening backticks. +/// Within raw blocks, everything (except for the language tag, if applicable) +/// is rendered as is, in particular, there are no escape sequences. +/// +/// The language tag is an identifier that directly follows the opening +/// backticks only if there are three or more backticks. If your text starts +/// with something that looks like an identifier, but no syntax highlighting is +/// needed, start the text with a single space (which will be trimmed) or use +/// the single backtick syntax. If your text should start or end with a +/// backtick, put a space before or after it (it will be trimmed). +#[elem( + scope, + title = "Raw Text / Code", + Synthesize, + Show, + ShowSet, + LocalName, + Figurable, + PlainText +)] +pub struct RawElem { + /// The raw text. + /// + /// You can also use raw blocks creatively to create custom syntaxes for + /// your automations. + /// + /// ````example + /// // Parse numbers in raw blocks with the + /// // `mydsl` tag and sum them up. + /// #show raw.where(lang: "mydsl"): it => { + /// let sum = 0 + /// for part in it.text.split("+") { + /// sum += int(part.trim()) + /// } + /// sum + /// } + /// + /// ```mydsl + /// 1 + 2 + 3 + 4 + 5 + /// ``` + /// ```` + #[required] + pub text: RawContent, + + /// Whether the raw text is displayed as a separate block. + /// + /// In markup mode, using one-backtick notation makes this `{false}`. + /// Using three-backtick notation makes it `{true}` if the enclosed content + /// contains at least one line break. + /// + /// ````example + /// // Display inline code in a small box + /// // that retains the correct baseline. + /// #show raw.where(block: false): box.with( + /// fill: luma(240), + /// inset: (x: 3pt, y: 0pt), + /// outset: (y: 3pt), + /// radius: 2pt, + /// ) + /// + /// // Display block code in a larger block + /// // with more padding. + /// #show raw.where(block: true): block.with( + /// fill: luma(240), + /// inset: 10pt, + /// radius: 4pt, + /// ) + /// + /// With `rg`, you can search through your files quickly. + /// This example searches the current directory recursively + /// for the text `Hello World`: + /// + /// ```bash + /// rg "Hello World" + /// ``` + /// ```` + #[default(false)] + pub block: bool, + + /// The language to syntax-highlight in. + /// + /// Apart from typical language tags known from Markdown, this supports the + /// `{"typ"}`, `{"typc"}`, and `{"typm"}` tags for + /// [Typst markup]($reference/syntax/#markup), + /// [Typst code]($reference/syntax/#code), and + /// [Typst math]($reference/syntax/#math), respectively. + /// + /// ````example + /// ```typ + /// This is *Typst!* + /// ``` + /// + /// This is ```typ also *Typst*```, but inline! + /// ```` + #[borrowed] + pub lang: Option<EcoString>, + + /// The horizontal alignment that each line in a raw block should have. + /// This option is ignored if this is not a raw block (if specified + /// `block: false` or single backticks were used in markup mode). + /// + /// By default, this is set to `{start}`, meaning that raw text is + /// aligned towards the start of the text direction inside the block + /// by default, regardless of the current context's alignment (allowing + /// you to center the raw block itself without centering the text inside + /// it, for example). + /// + /// ````example + /// #set raw(align: center) + /// + /// ```typc + /// let f(x) = x + /// code = "centered" + /// ``` + /// ```` + #[default(HAlignment::Start)] + pub align: HAlignment, + + /// One or multiple additional syntax definitions to load. The syntax + /// definitions should be in the + /// [`sublime-syntax` file format](https://www.sublimetext.com/docs/syntax.html). + /// + /// ````example + /// #set raw(syntaxes: "SExpressions.sublime-syntax") + /// + /// ```sexp + /// (defun factorial (x) + /// (if (zerop x) + /// ; with a comment + /// 1 + /// (* x (factorial (- x 1))))) + /// ``` + /// ```` + #[parse( + let (syntaxes, syntaxes_data) = parse_syntaxes(engine, args)?; + syntaxes + )] + #[fold] + pub syntaxes: SyntaxPaths, + + /// The raw file buffers of syntax definition files. + #[internal] + #[parse(syntaxes_data)] + #[fold] + pub syntaxes_data: Vec<Bytes>, + + /// The theme to use for syntax highlighting. Theme files should be in the + /// [`tmTheme` file format](https://www.sublimetext.com/docs/color_schemes_tmtheme.html). + /// + /// Applying a theme only affects the color of specifically highlighted + /// text. It does not consider the theme's foreground and background + /// properties, so that you retain control over the color of raw text. You + /// can apply the foreground color yourself with the [`text`] function and + /// the background with a [filled block]($block.fill). You could also use + /// the [`xml`] function to extract these properties from the theme. + /// + /// Additionally, you can set the theme to `{none}` to disable highlighting. + /// + /// ````example + /// #set raw(theme: "halcyon.tmTheme") + /// #show raw: it => block( + /// fill: rgb("#1d2433"), + /// inset: 8pt, + /// radius: 5pt, + /// text(fill: rgb("#a2aabc"), it) + /// ) + /// + /// ```typ + /// = Chapter 1 + /// #let hi = "Hello World" + /// ``` + /// ```` + #[parse( + let (theme_path, theme_data) = parse_theme(engine, args)?; + theme_path + )] + #[borrowed] + pub theme: ThemeArgType, + + /// The raw file buffer of syntax theme file. + #[internal] + #[parse(theme_data.map(Some))] + #[borrowed] + pub theme_data: Option<Bytes>, + + /// The size for a tab stop in spaces. A tab is replaced with enough spaces to + /// align with the next multiple of the size. + /// + /// ````example + /// #set raw(tab-size: 8) + /// ```tsv + /// Year Month Day + /// 2000 2 3 + /// 2001 2 1 + /// 2002 3 10 + /// ``` + /// ```` + #[default(2)] + pub tab_size: usize, + + /// The stylized lines of raw text. + /// + /// Made accessible for the [`raw.line` element]($raw.line). + /// Allows more styling control in `show` rules. + #[synthesized] + pub lines: Vec<Packed<RawLine>>, +} + +#[scope] +impl RawElem { + #[elem] + type RawLine; +} + +impl RawElem { + /// The supported language names and tags. + pub fn languages() -> Vec<(&'static str, Vec<&'static str>)> { + RAW_SYNTAXES + .syntaxes() + .iter() + .map(|syntax| { + ( + syntax.name.as_str(), + syntax.file_extensions.iter().map(|s| s.as_str()).collect(), + ) + }) + .chain([ + ("Typst", vec!["typ"]), + ("Typst (code)", vec!["typc"]), + ("Typst (math)", vec!["typm"]), + ]) + .collect() + } +} + +impl Synthesize for Packed<RawElem> { + fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> { + let seq = self.highlight(styles); + self.push_lines(seq); + Ok(()) + } +} + +impl Packed<RawElem> { + #[comemo::memoize] + fn highlight(&self, styles: StyleChain) -> Vec<Packed<RawLine>> { + let elem = self.as_ref(); + let lines = preprocess(elem.text(), styles, self.span()); + + let count = lines.len() as i64; + let lang = elem + .lang(styles) + .as_ref() + .as_ref() + .map(|s| s.to_lowercase()) + .or(Some("txt".into())); + + let extra_syntaxes = UnsyncLazy::new(|| { + load_syntaxes(&elem.syntaxes(styles), &elem.syntaxes_data(styles)).unwrap() + }); + let non_highlighted_result = |lines: EcoVec<(EcoString, Span)>| { + lines.into_iter().enumerate().map(|(i, (line, line_span))| { + Packed::new(RawLine::new( + i as i64 + 1, + count, + line.clone(), + TextElem::packed(line).spanned(line_span), + )) + .spanned(line_span) + }) + }; + + let theme = elem.theme(styles).as_ref().as_ref().map(|theme_path| { + theme_path.as_ref().map(|path| { + load_theme(path, elem.theme_data(styles).as_ref().as_ref().unwrap()) + .unwrap() + }) + }); + let theme: &Theme = match theme { + Smart::Auto => &RAW_THEME, + Smart::Custom(Some(ref theme)) => theme, + Smart::Custom(None) => return non_highlighted_result(lines).collect(), + }; + let foreground = theme.settings.foreground.unwrap_or(synt::Color::BLACK); + + let mut seq = vec![]; + if matches!(lang.as_deref(), Some("typ" | "typst" | "typc" | "typm")) { + let text = + lines.iter().map(|(s, _)| s.clone()).collect::<Vec<_>>().join("\n"); + let root = match lang.as_deref() { + Some("typc") => typst_syntax::parse_code(&text), + Some("typm") => typst_syntax::parse_math(&text), + _ => typst_syntax::parse(&text), + }; + + ThemedHighlighter::new( + &text, + LinkedNode::new(&root), + synt::Highlighter::new(theme), + &mut |i, _, range, style| { + // Find span and start of line. + // Note: Dedent is already applied to the text + let span = lines.get(i).map_or_else(Span::detached, |l| l.1); + let span_offset = text[..range.start] + .rfind('\n') + .map_or(0, |i| range.start - (i + 1)); + styled(&text[range], foreground, style, span, span_offset) + }, + &mut |i, range, line| { + let span = lines.get(i).map_or_else(Span::detached, |l| l.1); + seq.push( + Packed::new(RawLine::new( + (i + 1) as i64, + count, + EcoString::from(&text[range]), + Content::sequence(line.drain(..)), + )) + .spanned(span), + ); + }, + ) + .highlight(); + } else if let Some((syntax_set, syntax)) = lang.and_then(|token| { + RAW_SYNTAXES + .find_syntax_by_token(&token) + .map(|syntax| (&*RAW_SYNTAXES, syntax)) + .or_else(|| { + extra_syntaxes + .find_syntax_by_token(&token) + .map(|syntax| (&**extra_syntaxes, syntax)) + }) + }) { + let mut highlighter = syntect::easy::HighlightLines::new(syntax, theme); + for (i, (line, line_span)) in lines.into_iter().enumerate() { + let mut line_content = vec![]; + let mut span_offset = 0; + for (style, piece) in highlighter + .highlight_line(line.as_str(), syntax_set) + .into_iter() + .flatten() + { + line_content.push(styled( + piece, + foreground, + style, + line_span, + span_offset, + )); + span_offset += piece.len(); + } + + seq.push( + Packed::new(RawLine::new( + i as i64 + 1, + count, + line, + Content::sequence(line_content), + )) + .spanned(line_span), + ); + } + } else { + seq.extend(non_highlighted_result(lines)); + }; + + seq + } +} + +impl Show for Packed<RawElem> { + #[typst_macros::time(name = "raw", span = self.span())] + fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let lines = self.lines().map(|v| v.as_slice()).unwrap_or_default(); + + let mut seq = EcoVec::with_capacity((2 * lines.len()).saturating_sub(1)); + for (i, line) in lines.iter().enumerate() { + if i != 0 { + seq.push(LinebreakElem::shared().clone()); + } + + seq.push(line.clone().pack()); + } + + let mut realized = Content::sequence(seq); + if self.block(styles) { + // Align the text before inserting it into the block. + realized = realized.aligned(self.align(styles).into()); + realized = BlockElem::new() + .with_body(Some(BlockBody::Content(realized))) + .pack() + .spanned(self.span()); + } + + Ok(realized) + } +} + +impl ShowSet for Packed<RawElem> { + fn show_set(&self, styles: StyleChain) -> Styles { + let mut out = Styles::new(); + out.set(TextElem::set_overhang(false)); + out.set(TextElem::set_lang(Lang::ENGLISH)); + out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); + out.set(TextElem::set_size(TextSize(Em::new(0.8).into()))); + out.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")]))); + if self.block(styles) { + out.set(ParElem::set_shrink(false)); + } + out + } +} + +impl LocalName for Packed<RawElem> { + const KEY: &'static str = "raw"; +} + +impl Figurable for Packed<RawElem> {} + +impl PlainText for Packed<RawElem> { + fn plain_text(&self, text: &mut EcoString) { + text.push_str(&self.text().get()); + } +} + +/// The content of the raw text. +#[derive(Debug, Clone, Hash, PartialEq)] +pub enum RawContent { + /// From a string. + Text(EcoString), + /// From lines of text. + Lines(EcoVec<(EcoString, Span)>), +} + +impl RawContent { + /// Returns or synthesizes the text content of the raw text. + fn get(&self) -> EcoString { + match self.clone() { + RawContent::Text(text) => text, + RawContent::Lines(lines) => { + let mut lines = lines.into_iter().map(|(s, _)| s); + if lines.len() <= 1 { + lines.next().unwrap_or_default() + } else { + lines.collect::<Vec<_>>().join("\n").into() + } + } + } + } +} + +cast! { + RawContent, + self => self.get().into_value(), + v: EcoString => Self::Text(v), +} + +/// A highlighted line of raw text. +/// +/// This is a helper element that is synthesized by [`raw`] elements. +/// +/// It allows you to access various properties of the line, such as the line +/// number, the raw non-highlighted text, the highlighted text, and whether it +/// is the first or last line of the raw block. +#[elem(name = "line", title = "Raw Text / Code Line", Show, PlainText)] +pub struct RawLine { + /// The line number of the raw line inside of the raw block, starts at 1. + #[required] + pub number: i64, + + /// The total number of lines in the raw block. + #[required] + pub count: i64, + + /// The line of raw text. + #[required] + pub text: EcoString, + + /// The highlighted raw text. + #[required] + pub body: Content, +} + +impl Show for Packed<RawLine> { + #[typst_macros::time(name = "raw.line", span = self.span())] + fn show(&self, _: &mut Engine, _styles: StyleChain) -> SourceResult<Content> { + Ok(self.body().clone()) + } +} + +impl PlainText for Packed<RawLine> { + fn plain_text(&self, text: &mut EcoString) { + text.push_str(self.text()); + } +} + +/// Wrapper struct for the state required to highlight typst code. +struct ThemedHighlighter<'a> { + /// The code being highlighted. + code: &'a str, + /// The current node being highlighted. + node: LinkedNode<'a>, + /// The highlighter. + highlighter: synt::Highlighter<'a>, + /// The current scopes. + scopes: Vec<syntect::parsing::Scope>, + /// The current highlighted line. + current_line: Vec<Content>, + /// The range of the current line. + range: Range<usize>, + /// The current line number. + line: usize, + /// The function to style a piece of text. + style_fn: StyleFn<'a>, + /// The function to append a line. + line_fn: LineFn<'a>, +} + +impl<'a> ThemedHighlighter<'a> { + pub fn new( + code: &'a str, + top: LinkedNode<'a>, + highlighter: synt::Highlighter<'a>, + style_fn: StyleFn<'a>, + line_fn: LineFn<'a>, + ) -> Self { + Self { + code, + node: top, + highlighter, + range: 0..0, + scopes: Vec::new(), + current_line: Vec::new(), + line: 0, + style_fn, + line_fn, + } + } + + pub fn highlight(&mut self) { + self.highlight_inner(); + + if !self.current_line.is_empty() { + (self.line_fn)( + self.line, + self.range.start..self.code.len(), + &mut self.current_line, + ); + + self.current_line.clear(); + } + } + + fn highlight_inner(&mut self) { + if self.node.children().len() == 0 { + let style = self.highlighter.style_for_stack(&self.scopes); + let segment = &self.code[self.node.range()]; + + let mut len = 0; + for (i, line) in split_newlines(segment).into_iter().enumerate() { + if i != 0 { + (self.line_fn)( + self.line, + self.range.start..self.range.end + len - 1, + &mut self.current_line, + ); + self.range.start = self.range.end + len; + self.line += 1; + } + + let offset = self.node.range().start + len; + let token_range = offset..(offset + line.len()); + self.current_line.push((self.style_fn)( + self.line, + &self.node, + token_range, + style, + )); + + len += line.len() + 1; + } + + self.range.end += segment.len(); + } + + for child in self.node.children() { + let mut scopes = self.scopes.clone(); + if let Some(tag) = typst_syntax::highlight(&child) { + scopes.push(syntect::parsing::Scope::new(tag.tm_scope()).unwrap()) + } + + std::mem::swap(&mut scopes, &mut self.scopes); + self.node = child; + self.highlight_inner(); + std::mem::swap(&mut scopes, &mut self.scopes); + } + } +} + +fn preprocess( + text: &RawContent, + styles: StyleChain, + span: Span, +) -> EcoVec<(EcoString, Span)> { + if let RawContent::Lines(lines) = text { + if lines.iter().all(|(s, _)| !s.contains('\t')) { + return lines.clone(); + } + } + + let mut text = text.get(); + if text.contains('\t') { + let tab_size = RawElem::tab_size_in(styles); + text = align_tabs(&text, tab_size); + } + split_newlines(&text) + .into_iter() + .map(|line| (line.into(), span)) + .collect() +} + +/// Style a piece of text with a syntect style. +fn styled( + piece: &str, + foreground: synt::Color, + style: synt::Style, + span: Span, + span_offset: usize, +) -> Content { + let mut body = TextElem::packed(piece).spanned(span); + + if span_offset > 0 { + body = body.styled(TextElem::set_span_offset(span_offset)); + } + + if style.foreground != foreground { + body = body.styled(TextElem::set_fill(to_typst(style.foreground).into())); + } + + if style.font_style.contains(synt::FontStyle::BOLD) { + body = body.strong().spanned(span); + } + + if style.font_style.contains(synt::FontStyle::ITALIC) { + body = body.emph().spanned(span); + } + + if style.font_style.contains(synt::FontStyle::UNDERLINE) { + body = body.underlined().spanned(span); + } + + body +} + +fn to_typst(synt::Color { r, g, b, a }: synt::Color) -> Color { + Color::from_u8(r, g, b, a) +} + +fn to_syn(color: Color) -> synt::Color { + let [r, g, b, a] = color.to_rgb().to_vec4_u8(); + synt::Color { r, g, b, a } +} + +/// A list of raw syntax file paths. +#[derive(Debug, Default, Clone, PartialEq, Hash)] +pub struct SyntaxPaths(Vec<EcoString>); + +cast! { + SyntaxPaths, + self => self.0.into_value(), + v: EcoString => Self(vec![v]), + v: Array => Self(v.into_iter().map(Value::cast).collect::<HintedStrResult<_>>()?), +} + +impl Fold for SyntaxPaths { + fn fold(self, outer: Self) -> Self { + Self(self.0.fold(outer.0)) + } +} + +/// Load a syntax set from a list of syntax file paths. +#[comemo::memoize] +#[typst_macros::time(name = "load syntaxes")] +fn load_syntaxes(paths: &SyntaxPaths, bytes: &[Bytes]) -> StrResult<Arc<SyntaxSet>> { + let mut out = SyntaxSetBuilder::new(); + + // We might have multiple sublime-syntax/yaml files + for (path, bytes) in paths.0.iter().zip(bytes.iter()) { + let src = std::str::from_utf8(bytes).map_err(FileError::from)?; + out.add(SyntaxDefinition::load_from_str(src, false, None).map_err(|err| { + eco_format!("failed to parse syntax file `{path}` ({err})") + })?); + } + + Ok(Arc::new(out.build())) +} + +/// Function to parse the syntaxes argument. +/// Much nicer than having it be part of the `element` macro. +fn parse_syntaxes( + engine: &mut Engine, + args: &mut Args, +) -> SourceResult<(Option<SyntaxPaths>, Option<Vec<Bytes>>)> { + let Some(Spanned { v: paths, span }) = + args.named::<Spanned<SyntaxPaths>>("syntaxes")? + else { + return Ok((None, None)); + }; + + // Load syntax files. + let data = paths + .0 + .iter() + .map(|path| { + let id = span.resolve_path(path).at(span)?; + engine.world.file(id).at(span) + }) + .collect::<SourceResult<Vec<Bytes>>>()?; + + // Check that parsing works. + let _ = load_syntaxes(&paths, &data).at(span)?; + + Ok((Some(paths), Some(data))) +} + +#[comemo::memoize] +#[typst_macros::time(name = "load theme")] +fn load_theme(path: &str, bytes: &Bytes) -> StrResult<Arc<synt::Theme>> { + let mut cursor = std::io::Cursor::new(bytes.as_slice()); + + synt::ThemeSet::load_from_reader(&mut cursor) + .map(Arc::new) + .map_err(|err| eco_format!("failed to parse theme file `{path}` ({err})")) +} + +/// Function to parse the theme argument. +/// Much nicer than having it be part of the `element` macro. +fn parse_theme( + engine: &mut Engine, + args: &mut Args, +) -> SourceResult<(Option<ThemeArgType>, Option<Bytes>)> { + let Some(Spanned { v: path, span }) = args.named::<Spanned<ThemeArgType>>("theme")? + else { + // Argument `theme` not found. + return Ok((None, None)); + }; + + let Smart::Custom(path) = path else { + // Argument `theme` is `auto`. + return Ok((Some(Smart::Auto), None)); + }; + + let Some(path) = path else { + // Argument `theme` is `none`. + return Ok((Some(Smart::Custom(None)), None)); + }; + + // Load theme file. + let id = span.resolve_path(&path).at(span)?; + let data = engine.world.file(id).at(span)?; + + // Check that parsing works. + let _ = load_theme(&path, &data).at(span)?; + + Ok((Some(Smart::Custom(Some(path))), Some(data))) +} + +/// The syntect syntax definitions. +/// +/// Syntax set is generated from the syntaxes from the `bat` project +/// <https://github.com/sharkdp/bat/tree/master/assets/syntaxes> +pub static RAW_SYNTAXES: Lazy<syntect::parsing::SyntaxSet> = + Lazy::new(two_face::syntax::extra_no_newlines); + +/// The default theme used for syntax highlighting. +pub static RAW_THEME: Lazy<synt::Theme> = Lazy::new(|| synt::Theme { + name: Some("Typst Light".into()), + author: Some("The Typst Project Developers".into()), + settings: synt::ThemeSettings::default(), + scopes: vec![ + item("comment", Some("#8a8a8a"), None), + item("constant.character.escape", Some("#1d6c76"), None), + item("markup.bold", None, Some(synt::FontStyle::BOLD)), + item("markup.italic", None, Some(synt::FontStyle::ITALIC)), + item("markup.underline", None, Some(synt::FontStyle::UNDERLINE)), + item("markup.raw", Some("#818181"), None), + item("string.other.math.typst", None, None), + item("punctuation.definition.math", Some("#298e0d"), None), + item("keyword.operator.math", Some("#1d6c76"), None), + item("markup.heading, entity.name.section", None, Some(synt::FontStyle::BOLD)), + item( + "markup.heading.typst", + None, + Some(synt::FontStyle::BOLD | synt::FontStyle::UNDERLINE), + ), + item("punctuation.definition.list", Some("#8b41b1"), None), + item("markup.list.term", None, Some(synt::FontStyle::BOLD)), + item("entity.name.label, markup.other.reference", Some("#1d6c76"), None), + item("keyword, constant.language, variable.language", Some("#d73a49"), None), + item("storage.type, storage.modifier", Some("#d73a49"), None), + item("constant", Some("#b60157"), None), + item("string", Some("#298e0d"), None), + item("entity.name, variable.function, support", Some("#4b69c6"), None), + item("support.macro", Some("#16718d"), None), + item("meta.annotation", Some("#301414"), None), + item("entity.other, meta.interpolation", Some("#8b41b1"), None), + item("meta.diff.range", Some("#8b41b1"), None), + item("markup.inserted, meta.diff.header.to-file", Some("#298e0d"), None), + item("markup.deleted, meta.diff.header.from-file", Some("#d73a49"), None), + ], +}); + +/// Create a syntect theme item. +fn item( + scope: &str, + color: Option<&str>, + font_style: Option<synt::FontStyle>, +) -> synt::ThemeItem { + synt::ThemeItem { + scope: scope.parse().unwrap(), + style: synt::StyleModifier { + foreground: color.map(|s| to_syn(s.parse::<Color>().unwrap())), + background: None, + font_style, + }, + } +} + +/// Replace tabs with spaces to align with multiples of `tab_size`. +fn align_tabs(text: &str, tab_size: usize) -> EcoString { + let replacement = " ".repeat(tab_size); + let divisor = tab_size.max(1); + let amount = text.chars().filter(|&c| c == '\t').count(); + + let mut res = EcoString::with_capacity(text.len() - amount + amount * tab_size); + let mut column = 0; + + for grapheme in text.graphemes(true) { + match grapheme { + "\t" => { + let required = tab_size - column % divisor; + res.push_str(&replacement[..required]); + column += required; + } + "\n" => { + res.push_str(grapheme); + column = 0; + } + _ => { + res.push_str(grapheme); + column += 1; + } + } + } + + res +} diff --git a/crates/typst-library/src/text/shift.rs b/crates/typst-library/src/text/shift.rs new file mode 100644 index 00000000..003ecf47 --- /dev/null +++ b/crates/typst-library/src/text/shift.rs @@ -0,0 +1,210 @@ +use ecow::EcoString; + +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{elem, Content, Packed, SequenceElem, Show, StyleChain}; +use crate::layout::{Em, Length}; +use crate::text::{variant, SpaceElem, TextElem, TextSize}; +use crate::World; + +/// Renders text in subscript. +/// +/// The text is rendered smaller and its baseline is lowered. +/// +/// # Example +/// ```example +/// Revenue#sub[yearly] +/// ``` +#[elem(title = "Subscript", Show)] +pub struct SubElem { + /// Whether to prefer the dedicated subscript characters of the font. + /// + /// If this is enabled, Typst first tries to transform the text to subscript + /// codepoints. If that fails, it falls back to rendering lowered and shrunk + /// normal letters. + /// + /// ```example + /// N#sub(typographic: true)[1] + /// N#sub(typographic: false)[1] + /// ``` + #[default(true)] + pub typographic: bool, + + /// The baseline shift for synthetic subscripts. Does not apply if + /// `typographic` is true and the font has subscript codepoints for the + /// given `body`. + #[default(Em::new(0.2).into())] + pub baseline: Length, + + /// The font size for synthetic subscripts. Does not apply if + /// `typographic` is true and the font has subscript codepoints for the + /// given `body`. + #[default(TextSize(Em::new(0.6).into()))] + pub size: TextSize, + + /// The text to display in subscript. + #[required] + pub body: Content, +} + +impl Show for Packed<SubElem> { + #[typst_macros::time(name = "sub", span = self.span())] + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let body = self.body().clone(); + + if self.typographic(styles) { + if let Some(text) = convert_script(&body, true) { + if is_shapable(engine, &text, styles) { + return Ok(TextElem::packed(text)); + } + } + }; + + Ok(body + .styled(TextElem::set_baseline(self.baseline(styles))) + .styled(TextElem::set_size(self.size(styles)))) + } +} + +/// Renders text in superscript. +/// +/// The text is rendered smaller and its baseline is raised. +/// +/// # Example +/// ```example +/// 1#super[st] try! +/// ``` +#[elem(title = "Superscript", Show)] +pub struct SuperElem { + /// Whether to prefer the dedicated superscript characters of the font. + /// + /// If this is enabled, Typst first tries to transform the text to + /// superscript codepoints. If that fails, it falls back to rendering + /// raised and shrunk normal letters. + /// + /// ```example + /// N#super(typographic: true)[1] + /// N#super(typographic: false)[1] + /// ``` + #[default(true)] + pub typographic: bool, + + /// The baseline shift for synthetic superscripts. Does not apply if + /// `typographic` is true and the font has superscript codepoints for the + /// given `body`. + #[default(Em::new(-0.5).into())] + pub baseline: Length, + + /// The font size for synthetic superscripts. Does not apply if + /// `typographic` is true and the font has superscript codepoints for the + /// given `body`. + #[default(TextSize(Em::new(0.6).into()))] + pub size: TextSize, + + /// The text to display in superscript. + #[required] + pub body: Content, +} + +impl Show for Packed<SuperElem> { + #[typst_macros::time(name = "super", span = self.span())] + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let body = self.body().clone(); + + if self.typographic(styles) { + if let Some(text) = convert_script(&body, false) { + if is_shapable(engine, &text, styles) { + return Ok(TextElem::packed(text)); + } + } + }; + + Ok(body + .styled(TextElem::set_baseline(self.baseline(styles))) + .styled(TextElem::set_size(self.size(styles)))) + } +} + +/// Find and transform the text contained in `content` to the given script kind +/// if and only if it only consists of `Text`, `Space`, and `Empty` leaves. +fn convert_script(content: &Content, sub: bool) -> Option<EcoString> { + if content.is::<SpaceElem>() { + Some(' '.into()) + } else if let Some(elem) = content.to_packed::<TextElem>() { + if sub { + elem.text().chars().map(to_subscript_codepoint).collect() + } else { + elem.text().chars().map(to_superscript_codepoint).collect() + } + } else if let Some(sequence) = content.to_packed::<SequenceElem>() { + sequence + .children + .iter() + .map(|item| convert_script(item, sub)) + .collect() + } else { + None + } +} + +/// Checks whether the first retrievable family contains all code points of the +/// given string. +fn is_shapable(engine: &Engine, text: &str, styles: StyleChain) -> bool { + let world = engine.world; + for family in TextElem::font_in(styles) { + if let Some(font) = world + .book() + .select(family.as_str(), variant(styles)) + .and_then(|id| world.font(id)) + { + return text.chars().all(|c| font.ttf().glyph_index(c).is_some()); + } + } + + false +} + +/// Convert a character to its corresponding Unicode superscript. +fn to_superscript_codepoint(c: char) -> Option<char> { + match c { + '1' => Some('¹'), + '2' => Some('²'), + '3' => Some('³'), + '0' | '4'..='9' => char::from_u32(c as u32 - '0' as u32 + '⁰' as u32), + '+' => Some('⁺'), + '−' => Some('⁻'), + '=' => Some('⁼'), + '(' => Some('⁽'), + ')' => Some('⁾'), + 'n' => Some('ⁿ'), + 'i' => Some('ⁱ'), + ' ' => Some(' '), + _ => None, + } +} + +/// Convert a character to its corresponding Unicode subscript. +fn to_subscript_codepoint(c: char) -> Option<char> { + match c { + '0'..='9' => char::from_u32(c as u32 - '0' as u32 + '₀' as u32), + '+' => Some('₊'), + '−' => Some('₋'), + '=' => Some('₌'), + '(' => Some('₍'), + ')' => Some('₎'), + 'a' => Some('ₐ'), + 'e' => Some('ₑ'), + 'o' => Some('ₒ'), + 'x' => Some('ₓ'), + 'h' => Some('ₕ'), + 'k' => Some('ₖ'), + 'l' => Some('ₗ'), + 'm' => Some('ₘ'), + 'n' => Some('ₙ'), + 'p' => Some('ₚ'), + 's' => Some('ₛ'), + 't' => Some('ₜ'), + ' ' => Some(' '), + _ => None, + } +} diff --git a/crates/typst-library/src/text/smallcaps.rs b/crates/typst-library/src/text/smallcaps.rs new file mode 100644 index 00000000..bf003bd1 --- /dev/null +++ b/crates/typst-library/src/text/smallcaps.rs @@ -0,0 +1,58 @@ +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{elem, Content, Packed, Show, StyleChain}; +use crate::text::TextElem; + +/// Displays text in small capitals. +/// +/// # Example +/// ```example +/// Hello \ +/// #smallcaps[Hello] +/// ``` +/// +/// # Smallcaps fonts +/// By default, this enables the OpenType `smcp` feature for the font. Not all +/// fonts support this feature. Sometimes smallcaps are part of a dedicated +/// font. This is, for example, the case for the _Latin Modern_ family of fonts. +/// In those cases, you can use a show-set rule to customize the appearance of +/// the text in smallcaps: +/// +/// ```typ +/// #show smallcaps: set text(font: "Latin Modern Roman Caps") +/// ``` +/// +/// In the future, this function will support synthesizing smallcaps from normal +/// letters, but this is not yet implemented. +/// +/// # Smallcaps headings +/// You can use a [show rule]($styling/#show-rules) to apply smallcaps +/// formatting to all your headings. In the example below, we also center-align +/// our headings and disable the standard bold font. +/// +/// ```example +/// #set par(justify: true) +/// #set heading(numbering: "I.") +/// +/// #show heading: smallcaps +/// #show heading: set align(center) +/// #show heading: set text( +/// weight: "regular" +/// ) +/// +/// = Introduction +/// #lorem(40) +/// ``` +#[elem(title = "Small Capitals", Show)] +pub struct SmallcapsElem { + /// The content to display in small capitals. + #[required] + pub body: Content, +} + +impl Show for Packed<SmallcapsElem> { + #[typst_macros::time(name = "smallcaps", span = self.span())] + fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(self.body().clone().styled(TextElem::set_smallcaps(true))) + } +} diff --git a/crates/typst-library/src/text/smartquote.rs b/crates/typst-library/src/text/smartquote.rs new file mode 100644 index 00000000..5917550d --- /dev/null +++ b/crates/typst-library/src/text/smartquote.rs @@ -0,0 +1,387 @@ +use ecow::EcoString; +use typst_syntax::is_newline; +use unicode_segmentation::UnicodeSegmentation; + +use crate::diag::{bail, HintedStrResult, StrResult}; +use crate::foundations::{ + array, cast, dict, elem, Array, Dict, FromValue, Packed, PlainText, Smart, Str, +}; +use crate::layout::Dir; +use crate::text::{Lang, Region}; + +/// A language-aware quote that reacts to its context. +/// +/// Automatically turns into an appropriate opening or closing quote based on +/// the active [text language]($text.lang). +/// +/// # Example +/// ```example +/// "This is in quotes." +/// +/// #set text(lang: "de") +/// "Das ist in Anführungszeichen." +/// +/// #set text(lang: "fr") +/// "C'est entre guillemets." +/// ``` +/// +/// # Syntax +/// This function also has dedicated syntax: The normal quote characters +/// (`'` and `"`). Typst automatically makes your quotes smart. +#[elem(name = "smartquote", PlainText)] +pub struct SmartQuoteElem { + /// Whether this should be a double quote. + #[default(true)] + pub double: bool, + + /// Whether smart quotes are enabled. + /// + /// To disable smartness for a single quote, you can also escape it with a + /// backslash. + /// + /// ```example + /// #set smartquote(enabled: false) + /// + /// These are "dumb" quotes. + /// ``` + #[default(true)] + pub enabled: bool, + + /// Whether to use alternative quotes. + /// + /// Does nothing for languages that don't have alternative quotes, or if + /// explicit quotes were set. + /// + /// ```example + /// #set text(lang: "de") + /// #set smartquote(alternative: true) + /// + /// "Das ist in anderen Anführungszeichen." + /// ``` + #[default(false)] + pub alternative: bool, + + /// The quotes to use. + /// + /// - When set to `{auto}`, the appropriate single quotes for the + /// [text language]($text.lang) will be used. This is the default. + /// - Custom quotes can be passed as a string, array, or dictionary of either + /// - [string]($str): a string consisting of two characters containing the + /// opening and closing double quotes (characters here refer to Unicode + /// grapheme clusters) + /// - [array]: an array containing the opening and closing double quotes + /// - [dictionary]: an array containing the double and single quotes, each + /// specified as either `{auto}`, string, or array + /// + /// ```example + /// #set text(lang: "de") + /// 'Das sind normale Anführungszeichen.' + /// + /// #set smartquote(quotes: "()") + /// "Das sind eigene Anführungszeichen." + /// + /// #set smartquote(quotes: (single: ("[[", "]]"), double: auto)) + /// 'Das sind eigene Anführungszeichen.' + /// ``` + #[borrowed] + pub quotes: Smart<SmartQuoteDict>, +} + +impl PlainText for Packed<SmartQuoteElem> { + fn plain_text(&self, text: &mut EcoString) { + if self.double.unwrap_or(true) { + text.push_str("\""); + } else { + text.push_str("'"); + } + } +} + +/// A smart quote substitutor with zero lookahead. +#[derive(Debug, Clone)] +pub struct SmartQuoter { + /// The amount of quotes that have been opened. + depth: u8, + /// Each bit indicates whether the quote at this nesting depth is a double. + /// Maximum supported depth is thus 32. + kinds: u32, +} + +impl SmartQuoter { + /// Start quoting. + pub fn new() -> Self { + Self { depth: 0, kinds: 0 } + } + + /// Determine which smart quote to substitute given this quoter's nesting + /// state and the character immediately preceding the quote. + pub fn quote<'a>( + &mut self, + before: Option<char>, + quotes: &SmartQuotes<'a>, + double: bool, + ) -> &'a str { + let opened = self.top(); + let before = before.unwrap_or(' '); + + // If we are after a number and haven't most recently opened a quote of + // this kind, produce a prime. Otherwise, we prefer a closing quote. + if before.is_numeric() && opened != Some(double) { + return if double { "″" } else { "′" }; + } + + // If we have a single smart quote, didn't recently open a single + // quotation, and are after an alphabetic char or an object (e.g. a + // math equation), interpret this as an apostrophe. + if !double + && opened != Some(false) + && (before.is_alphabetic() || before == '\u{FFFC}') + { + return "’"; + } + + // If the most recently opened quotation is of this kind and the + // previous char does not indicate a nested quotation, close it. + if opened == Some(double) + && !before.is_whitespace() + && !is_newline(before) + && !is_opening_bracket(before) + { + self.pop(); + return quotes.close(double); + } + + // Otherwise, open a new the quotation. + self.push(double); + quotes.open(double) + } + + /// The top of our quotation stack. Returns `Some(double)` for the most + /// recently opened quote or `None` if we didn't open one. + fn top(&self) -> Option<bool> { + self.depth.checked_sub(1).map(|i| (self.kinds >> i) & 1 == 1) + } + + /// Push onto the quotation stack. + fn push(&mut self, double: bool) { + if self.depth < 32 { + self.kinds |= (double as u32) << self.depth; + self.depth += 1; + } + } + + /// Pop from the quotation stack. + fn pop(&mut self) { + self.depth -= 1; + self.kinds &= (1 << self.depth) - 1; + } +} + +impl Default for SmartQuoter { + fn default() -> Self { + Self::new() + } +} + +/// Whether the character is an opening bracket, parenthesis, or brace. +fn is_opening_bracket(c: char) -> bool { + matches!(c, '(' | '{' | '[') +} + +/// Decides which quotes to substitute smart quotes with. +pub struct SmartQuotes<'s> { + /// The opening single quote. + pub single_open: &'s str, + /// The closing single quote. + pub single_close: &'s str, + /// The opening double quote. + pub double_open: &'s str, + /// The closing double quote. + pub double_close: &'s str, +} + +impl<'s> SmartQuotes<'s> { + /// Create a new `Quotes` struct with the given quotes, optionally falling + /// back to the defaults for a language and region. + /// + /// The language should be specified as an all-lowercase ISO 639-1 code, the + /// region as an all-uppercase ISO 3166-alpha2 code. + /// + /// Currently, the supported languages are: English, Czech, Danish, German, + /// Swiss / Liechtensteinian German, Estonian, Icelandic, Italian, Latin, + /// Lithuanian, Latvian, Slovak, Slovenian, Spanish, Bosnian, Finnish, + /// Swedish, French, Swiss French, Hungarian, Polish, Romanian, Japanese, + /// Traditional Chinese, Russian, Norwegian, and Hebrew. + /// + /// For unknown languages, the English quotes are used as fallback. + pub fn get( + quotes: &'s Smart<SmartQuoteDict>, + lang: Lang, + region: Option<Region>, + alternative: bool, + ) -> Self { + let region = region.as_ref().map(Region::as_str); + + let default = ("‘", "’", "“", "”"); + let low_high = ("‚", "‘", "„", "“"); + + let (single_open, single_close, double_open, double_close) = match lang.as_str() { + "de" if matches!(region, Some("CH" | "LI")) => match alternative { + false => ("‹", "›", "«", "»"), + true => low_high, + }, + "fr" if matches!(region, Some("CH")) => match alternative { + false => ("‹\u{202F}", "\u{202F}›", "«\u{202F}", "\u{202F}»"), + true => default, + }, + "cs" | "da" | "de" | "sk" | "sl" if alternative => ("›", "‹", "»", "«"), + "cs" | "de" | "et" | "is" | "lt" | "lv" | "sk" | "sl" => low_high, + "da" => ("‘", "’", "“", "”"), + "fr" | "ru" if alternative => default, + "fr" => ("‹\u{00A0}", "\u{00A0}›", "«\u{00A0}", "\u{00A0}»"), + "fi" | "sv" if alternative => ("’", "’", "»", "»"), + "bs" | "fi" | "sv" => ("’", "’", "”", "”"), + "it" if alternative => default, + "la" if alternative => ("“", "”", "«\u{202F}", "\u{202F}»"), + "it" | "la" => ("“", "”", "«", "»"), + "es" if matches!(region, Some("ES") | None) => ("“", "”", "«", "»"), + "hu" | "pl" | "ro" => ("’", "’", "„", "”"), + "no" | "nb" | "nn" if alternative => low_high, + "ru" | "no" | "nb" | "nn" | "ua" => ("’", "’", "«", "»"), + "gr" => ("‘", "’", "«", "»"), + "he" => ("’", "’", "”", "”"), + _ if lang.dir() == Dir::RTL => ("’", "‘", "”", "“"), + _ => default, + }; + + fn inner_or_default<'s>( + quotes: Smart<&'s SmartQuoteDict>, + f: impl FnOnce(&'s SmartQuoteDict) -> Smart<&'s SmartQuoteSet>, + default: [&'s str; 2], + ) -> [&'s str; 2] { + match quotes.and_then(f) { + Smart::Auto => default, + Smart::Custom(SmartQuoteSet { open, close }) => { + [open, close].map(|s| s.as_str()) + } + } + } + + let quotes = quotes.as_ref(); + let [single_open, single_close] = + inner_or_default(quotes, |q| q.single.as_ref(), [single_open, single_close]); + let [double_open, double_close] = + inner_or_default(quotes, |q| q.double.as_ref(), [double_open, double_close]); + + Self { + single_open, + single_close, + double_open, + double_close, + } + } + + /// The opening quote. + pub fn open(&self, double: bool) -> &'s str { + if double { + self.double_open + } else { + self.single_open + } + } + + /// The closing quote. + pub fn close(&self, double: bool) -> &'s str { + if double { + self.double_close + } else { + self.single_close + } + } +} + +/// An opening and closing quote. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct SmartQuoteSet { + open: EcoString, + close: EcoString, +} + +cast! { + SmartQuoteSet, + self => array![self.open, self.close].into_value(), + value: Array => { + let [open, close] = array_to_set(value)?; + Self { open, close } + }, + value: Str => { + let [open, close] = str_to_set(value.as_str())?; + Self { open, close } + }, +} + +fn str_to_set(value: &str) -> StrResult<[EcoString; 2]> { + let mut iter = value.graphemes(true); + match (iter.next(), iter.next(), iter.next()) { + (Some(open), Some(close), None) => Ok([open.into(), close.into()]), + _ => { + let count = value.graphemes(true).count(); + bail!( + "expected 2 characters, found {count} character{}", + if count > 1 { "s" } else { "" } + ); + } + } +} + +fn array_to_set(value: Array) -> HintedStrResult<[EcoString; 2]> { + let value = value.as_slice(); + if value.len() != 2 { + bail!( + "expected 2 quotes, found {} quote{}", + value.len(), + if value.len() > 1 { "s" } else { "" } + ); + } + + let open: EcoString = value[0].clone().cast()?; + let close: EcoString = value[1].clone().cast()?; + + Ok([open, close]) +} + +/// A dict of single and double quotes. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct SmartQuoteDict { + double: Smart<SmartQuoteSet>, + single: Smart<SmartQuoteSet>, +} + +cast! { + SmartQuoteDict, + self => dict! { "double" => self.double, "single" => self.single }.into_value(), + mut value: Dict => { + let keys = ["double", "single"]; + + let double = value + .take("double") + .ok() + .map(FromValue::from_value) + .transpose()? + .unwrap_or(Smart::Auto); + let single = value + .take("single") + .ok() + .map(FromValue::from_value) + .transpose()? + .unwrap_or(Smart::Auto); + + value.finish(&keys)?; + + Self { single, double } + }, + value: SmartQuoteSet => Self { + double: Smart::Custom(value), + single: Smart::Auto, + }, +} diff --git a/crates/typst-library/src/text/space.rs b/crates/typst-library/src/text/space.rs new file mode 100644 index 00000000..38a55482 --- /dev/null +++ b/crates/typst-library/src/text/space.rs @@ -0,0 +1,31 @@ +use ecow::EcoString; +use typst_utils::singleton; + +use crate::foundations::{ + elem, Content, NativeElement, Packed, PlainText, Repr, Unlabellable, +}; + +/// A text space. +#[elem(Unlabellable, PlainText, Repr)] +pub struct SpaceElem {} + +impl SpaceElem { + /// Get the globally shared space element. + pub fn shared() -> &'static Content { + singleton!(Content, SpaceElem::new().pack()) + } +} + +impl Repr for SpaceElem { + fn repr(&self) -> EcoString { + "[ ]".into() + } +} + +impl Unlabellable for Packed<SpaceElem> {} + +impl PlainText for Packed<SpaceElem> { + fn plain_text(&self, text: &mut EcoString) { + text.push(' '); + } +} diff --git a/crates/typst-library/src/visualize/color.rs b/crates/typst-library/src/visualize/color.rs new file mode 100644 index 00000000..1a279fbb --- /dev/null +++ b/crates/typst-library/src/visualize/color.rs @@ -0,0 +1,2013 @@ +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::str::FromStr; + +use ecow::{eco_format, EcoString, EcoVec}; +use once_cell::sync::Lazy; +use palette::encoding::{self, Linear}; +use palette::{ + Alpha, Darken, Desaturate, FromColor, Lighten, OklabHue, RgbHue, Saturate, ShiftHue, +}; +use qcms::Profile; +use typst_syntax::{Span, Spanned}; + +use crate::diag::{bail, At, SourceResult, StrResult}; +use crate::foundations::{ + array, cast, func, repr, scope, ty, Args, Array, IntoValue, Module, Repr, Scope, Str, + Value, +}; +use crate::layout::{Angle, Ratio}; + +// Type aliases for `palette` internal types in f32. +pub type Oklab = palette::oklab::Oklaba<f32>; +pub type Oklch = palette::oklch::Oklcha<f32>; +pub type LinearRgb = palette::rgb::Rgba<Linear<encoding::Srgb>, f32>; +pub type Rgb = palette::rgb::Rgba<encoding::Srgb, f32>; +pub type Hsl = palette::hsl::Hsla<encoding::Srgb, f32>; +pub type Hsv = palette::hsv::Hsva<encoding::Srgb, f32>; +pub type Luma = palette::luma::Lumaa<encoding::Srgb, f32>; + +/// The ICC profile used to convert from CMYK to RGB. +/// +/// This is a minimal CMYK profile that only contains the necessary information +/// to convert from CMYK to RGB. It is based on the CGATS TR 001-1995 +/// specification. See +/// <https://github.com/saucecontrol/Compact-ICC-Profiles#cmyk>. +static CMYK_TO_XYZ: Lazy<Box<Profile>> = + Lazy::new(|| Profile::new_from_slice(typst_assets::icc::CMYK_TO_XYZ, false).unwrap()); + +/// The target sRGB profile. +static SRGB_PROFILE: Lazy<Box<Profile>> = Lazy::new(|| { + let mut out = Profile::new_sRGB(); + out.precache_output_transform(); + out +}); + +static TO_SRGB: Lazy<qcms::Transform> = Lazy::new(|| { + qcms::Transform::new_to( + &CMYK_TO_XYZ, + &SRGB_PROFILE, + qcms::DataType::CMYK, + qcms::DataType::RGB8, + // Our input profile only supports perceptual intent. + qcms::Intent::Perceptual, + ) + .unwrap() +}); + +/// A color in a specific color space. +/// +/// Typst supports: +/// - sRGB through the [`rgb` function]($color.rgb) +/// - Device CMYK through [`cmyk` function]($color.cmyk) +/// - D65 Gray through the [`luma` function]($color.luma) +/// - Oklab through the [`oklab` function]($color.oklab) +/// - Oklch through the [`oklch` function]($color.oklch) +/// - Linear RGB through the [`color.linear-rgb` function]($color.linear-rgb) +/// - HSL through the [`color.hsl` function]($color.hsl) +/// - HSV through the [`color.hsv` function]($color.hsv) +/// +/// +/// # Example +/// +/// ```example +/// #rect(fill: aqua) +/// ``` +/// +/// # Predefined colors +/// Typst defines the following built-in colors: +/// +/// | Color | Definition | +/// |-----------|:-------------------| +/// | `black` | `{luma(0)}` | +/// | `gray` | `{luma(170)}` | +/// | `silver` | `{luma(221)}` | +/// | `white` | `{luma(255)}` | +/// | `navy` | `{rgb("#001f3f")}` | +/// | `blue` | `{rgb("#0074d9")}` | +/// | `aqua` | `{rgb("#7fdbff")}` | +/// | `teal` | `{rgb("#39cccc")}` | +/// | `eastern` | `{rgb("#239dad")}` | +/// | `purple` | `{rgb("#b10dc9")}` | +/// | `fuchsia` | `{rgb("#f012be")}` | +/// | `maroon` | `{rgb("#85144b")}` | +/// | `red` | `{rgb("#ff4136")}` | +/// | `orange` | `{rgb("#ff851b")}` | +/// | `yellow` | `{rgb("#ffdc00")}` | +/// | `olive` | `{rgb("#3d9970")}` | +/// | `green` | `{rgb("#2ecc40")}` | +/// | `lime` | `{rgb("#01ff70")}` | +/// +/// The predefined colors and the most important color constructors are +/// available globally and also in the color type's scope, so you can write +/// either `color.red` or just `red`. +/// +/// ```preview +/// #let colors = ( +/// "black", "gray", "silver", "white", +/// "navy", "blue", "aqua", "teal", +/// "eastern", "purple", "fuchsia", +/// "maroon", "red", "orange", "yellow", +/// "olive", "green", "lime", +/// ) +/// +/// #set text(font: "PT Sans") +/// #set page(width: auto) +/// #grid( +/// columns: 9, +/// gutter: 10pt, +/// ..colors.map(name => { +/// let col = eval(name) +/// let luminance = luma(col).components().first() +/// set text(fill: white) if luminance < 50% +/// set square(stroke: black) if col == white +/// set align(center + horizon) +/// square(size: 50pt, fill: col, name) +/// }) +/// ) +/// ``` +/// +/// # Predefined color maps +/// Typst also includes a number of preset color maps that can be used for +/// [gradients]($gradient.linear). These are simply arrays of colors defined in +/// the module `color.map`. +/// +/// ```example +/// #circle(fill: gradient.linear(..color.map.crest)) +/// ``` +/// +/// | Map | Details | +/// |------------|:------------------------------------------------------------| +/// | `turbo` | A perceptually uniform rainbow-like color map. Read [this blog post](https://ai.googleblog.com/2019/08/turbo-improved-rainbow-colormap-for.html) for more details. | +/// | `cividis` | A blue to gray to yellow color map. See [this blog post](https://bids.github.io/colormap/) for more details. | +/// | `rainbow` | Cycles through the full color spectrum. This color map is best used by setting the interpolation color space to [HSL]($color.hsl). The rainbow gradient is **not suitable** for data visualization because it is not perceptually uniform, so the differences between values become unclear to your readers. It should only be used for decorative purposes. | +/// | `spectral` | Red to yellow to blue color map. | +/// | `viridis` | A purple to teal to yellow color map. | +/// | `inferno` | A black to red to yellow color map. | +/// | `magma` | A black to purple to yellow color map. | +/// | `plasma` | A purple to pink to yellow color map. | +/// | `rocket` | A black to red to white color map. | +/// | `mako` | A black to teal to yellow color map. | +/// | `vlag` | A light blue to white to red color map. | +/// | `icefire` | A light teal to black to yellow color map. | +/// | `flare` | A orange to purple color map that is perceptually uniform. | +/// | `crest` | A blue to white to red color map. | +/// +/// Some popular presets are not included because they are not available under a +/// free licence. Others, like +/// [Jet](https://jakevdp.github.io/blog/2014/10/16/how-bad-is-your-colormap/), +/// are not included because they are not color blind friendly. Feel free to use +/// or create a package with other presets that are useful to you! +/// +/// ```preview +/// #set page(width: auto, height: auto) +/// #set text(font: "PT Sans", size: 8pt) +/// +/// #let maps = ( +/// "turbo", "cividis", "rainbow", "spectral", +/// "viridis", "inferno", "magma", "plasma", +/// "rocket", "mako", "vlag", "icefire", +/// "flare", "crest", +/// ) +/// +/// #stack(dir: ltr, spacing: 3pt, ..maps.map((name) => { +/// let map = eval("color.map." + name) +/// stack( +/// dir: ttb, +/// block( +/// width: 15pt, +/// height: 100pt, +/// fill: gradient.linear(..map, angle: 90deg), +/// ), +/// block( +/// width: 15pt, +/// height: 32pt, +/// move(dy: 8pt, rotate(90deg, name)), +/// ), +/// ) +/// })) +/// ``` +#[ty(scope, cast)] +#[derive(Copy, Clone)] +pub enum Color { + /// A 32-bit luma color. + Luma(Luma), + /// A 32-bit L\*a\*b\* color in the Oklab color space. + Oklab(Oklab), + /// A 32-bit LCh color in the Oklab color space. + Oklch(Oklch), + /// A 32-bit RGB color. + Rgb(Rgb), + /// A 32-bit linear RGB color. + LinearRgb(LinearRgb), + /// A 32-bit CMYK color. + Cmyk(Cmyk), + /// A 32-bit HSL color. + Hsl(Hsl), + /// A 32-bit HSV color. + Hsv(Hsv), +} + +#[scope] +impl Color { + /// The module of preset color maps. + pub const MAP: fn() -> Module = || typst_utils::singleton!(Module, map()).clone(); + + pub const BLACK: Self = Self::Luma(Luma::new(0.0, 1.0)); + pub const GRAY: Self = Self::Luma(Luma::new(0.6666666, 1.0)); + pub const WHITE: Self = Self::Luma(Luma::new(1.0, 1.0)); + pub const SILVER: Self = Self::Luma(Luma::new(0.8666667, 1.0)); + pub const NAVY: Self = Self::Rgb(Rgb::new(0.0, 0.121569, 0.247059, 1.0)); + pub const BLUE: Self = Self::Rgb(Rgb::new(0.0, 0.454902, 0.85098, 1.0)); + pub const AQUA: Self = Self::Rgb(Rgb::new(0.4980392, 0.858823, 1.0, 1.0)); + pub const TEAL: Self = Self::Rgb(Rgb::new(0.223529, 0.8, 0.8, 1.0)); + pub const EASTERN: Self = Self::Rgb(Rgb::new(0.13725, 0.615686, 0.678431, 1.0)); + pub const PURPLE: Self = Self::Rgb(Rgb::new(0.694118, 0.050980, 0.788235, 1.0)); + pub const FUCHSIA: Self = Self::Rgb(Rgb::new(0.941177, 0.070588, 0.745098, 1.0)); + pub const MAROON: Self = Self::Rgb(Rgb::new(0.521569, 0.078431, 0.294118, 1.0)); + pub const RED: Self = Self::Rgb(Rgb::new(1.0, 0.254902, 0.211765, 1.0)); + pub const ORANGE: Self = Self::Rgb(Rgb::new(1.0, 0.521569, 0.105882, 1.0)); + pub const YELLOW: Self = Self::Rgb(Rgb::new(1.0, 0.8627451, 0.0, 1.0)); + pub const OLIVE: Self = Self::Rgb(Rgb::new(0.239216, 0.6, 0.4392157, 1.0)); + pub const GREEN: Self = Self::Rgb(Rgb::new(0.1803922, 0.8, 0.2509804, 1.0)); + pub const LIME: Self = Self::Rgb(Rgb::new(0.0039216, 1.0, 0.4392157, 1.0)); + + /// Create a grayscale color. + /// + /// A grayscale color is represented internally by a single `lightness` + /// component. + /// + /// These components are also available using the + /// [`components`]($color.components) method. + /// + /// ```example + /// #for x in range(250, step: 50) { + /// box(square(fill: luma(x))) + /// } + /// ``` + #[func] + pub fn luma( + /// The real arguments (the other arguments are just for the docs, this + /// function is a bit involved, so we parse the arguments manually). + args: &mut Args, + /// The lightness component. + #[external] + lightness: Component, + /// The alpha component. + #[external] + alpha: RatioComponent, + /// Alternatively: The color to convert to grayscale. + /// + /// If this is given, the `lightness` should not be given. + #[external] + color: Color, + ) -> SourceResult<Color> { + Ok(if let Some(color) = args.find::<Color>()? { + color.to_luma() + } else { + let Component(gray) = + args.expect("gray component").unwrap_or(Component(Ratio::one())); + let RatioComponent(alpha) = + args.eat()?.unwrap_or(RatioComponent(Ratio::one())); + Self::Luma(Luma::new(gray.get() as f32, alpha.get() as f32)) + }) + } + + /// Create an [Oklab](https://bottosson.github.io/posts/oklab/) color. + /// + /// This color space is well suited for the following use cases: + /// - Color manipulation such as saturating while keeping perceived hue + /// - Creating grayscale images with uniform perceived lightness + /// - Creating smooth and uniform color transition and gradients + /// + /// A linear Oklab color is represented internally by an array of four + /// components: + /// - lightness ([`ratio`]) + /// - a ([`float`] or [`ratio`]. + /// Ratios are relative to `{0.4}`; meaning `{50%}` is equal to `{0.2}`) + /// - b ([`float`] or [`ratio`]. + /// Ratios are relative to `{0.4}`; meaning `{50%}` is equal to `{0.2}`) + /// - alpha ([`ratio`]) + /// + /// These components are also available using the + /// [`components`]($color.components) method. + /// + /// ```example + /// #square( + /// fill: oklab(27%, 20%, -3%, 50%) + /// ) + /// ``` + #[func] + pub fn oklab( + /// The real arguments (the other arguments are just for the docs, this + /// function is a bit involved, so we parse the arguments manually). + args: &mut Args, + /// The lightness component. + #[external] + lightness: RatioComponent, + /// The a ("green/red") component. + #[external] + a: ChromaComponent, + /// The b ("blue/yellow") component. + #[external] + b: ChromaComponent, + /// The alpha component. + #[external] + alpha: RatioComponent, + /// Alternatively: The color to convert to Oklab. + /// + /// If this is given, the individual components should not be given. + #[external] + color: Color, + ) -> SourceResult<Color> { + Ok(if let Some(color) = args.find::<Color>()? { + color.to_oklab() + } else { + let RatioComponent(l) = args.expect("lightness component")?; + let ChromaComponent(a) = args.expect("A component")?; + let ChromaComponent(b) = args.expect("B component")?; + let RatioComponent(alpha) = + args.eat()?.unwrap_or(RatioComponent(Ratio::one())); + Self::Oklab(Oklab::new(l.get() as f32, a, b, alpha.get() as f32)) + }) + } + + /// Create an [Oklch](https://bottosson.github.io/posts/oklab/) color. + /// + /// This color space is well suited for the following use cases: + /// - Color manipulation involving lightness, chroma, and hue + /// - Creating grayscale images with uniform perceived lightness + /// - Creating smooth and uniform color transition and gradients + /// + /// A linear Oklch color is represented internally by an array of four + /// components: + /// - lightness ([`ratio`]) + /// - chroma ([`float`] or [`ratio`]. + /// Ratios are relative to `{0.4}`; meaning `{50%}` is equal to `{0.2}`) + /// - hue ([`angle`]) + /// - alpha ([`ratio`]) + /// + /// These components are also available using the + /// [`components`]($color.components) method. + /// + /// ```example + /// #square( + /// fill: oklch(40%, 0.2, 160deg, 50%) + /// ) + /// ``` + #[func] + pub fn oklch( + /// The real arguments (the other arguments are just for the docs, this + /// function is a bit involved, so we parse the arguments manually). + args: &mut Args, + /// The lightness component. + #[external] + lightness: RatioComponent, + /// The chroma component. + #[external] + chroma: ChromaComponent, + /// The hue component. + #[external] + hue: Angle, + /// The alpha component. + #[external] + alpha: RatioComponent, + /// Alternatively: The color to convert to Oklch. + /// + /// If this is given, the individual components should not be given. + #[external] + color: Color, + ) -> SourceResult<Color> { + Ok(if let Some(color) = args.find::<Color>()? { + color.to_oklch() + } else { + let RatioComponent(l) = args.expect("lightness component")?; + let ChromaComponent(c) = args.expect("chroma component")?; + let h: Angle = args.expect("hue component")?; + let RatioComponent(alpha) = + args.eat()?.unwrap_or(RatioComponent(Ratio::one())); + Self::Oklch(Oklch::new( + l.get() as f32, + c, + OklabHue::from_degrees(h.to_deg() as f32), + alpha.get() as f32, + )) + }) + } + + /// Create an RGB(A) color with linear luma. + /// + /// This color space is similar to sRGB, but with the distinction that the + /// color component are not gamma corrected. This makes it easier to perform + /// color operations such as blending and interpolation. Although, you + /// should prefer to use the [`oklab` function]($color.oklab) for these. + /// + /// A linear RGB(A) color is represented internally by an array of four + /// components: + /// - red ([`ratio`]) + /// - green ([`ratio`]) + /// - blue ([`ratio`]) + /// - alpha ([`ratio`]) + /// + /// These components are also available using the + /// [`components`]($color.components) method. + /// + /// ```example + /// #square(fill: color.linear-rgb( + /// 30%, 50%, 10%, + /// )) + /// ``` + #[func(title = "Linear RGB")] + pub fn linear_rgb( + /// The real arguments (the other arguments are just for the docs, this + /// function is a bit involved, so we parse the arguments manually). + args: &mut Args, + /// The red component. + #[external] + red: Component, + /// The green component. + #[external] + green: Component, + /// The blue component. + #[external] + blue: Component, + /// The alpha component. + #[external] + alpha: Component, + /// Alternatively: The color to convert to linear RGB(A). + /// + /// If this is given, the individual components should not be given. + #[external] + color: Color, + ) -> SourceResult<Color> { + Ok(if let Some(color) = args.find::<Color>()? { + color.to_linear_rgb() + } else { + let Component(r) = args.expect("red component")?; + let Component(g) = args.expect("green component")?; + let Component(b) = args.expect("blue component")?; + let Component(a) = args.eat()?.unwrap_or(Component(Ratio::one())); + Self::LinearRgb(LinearRgb::new( + r.get() as f32, + g.get() as f32, + b.get() as f32, + a.get() as f32, + )) + }) + } + + /// Create an RGB(A) color. + /// + /// The color is specified in the sRGB color space. + /// + /// An RGB(A) color is represented internally by an array of four components: + /// - red ([`ratio`]) + /// - green ([`ratio`]) + /// - blue ([`ratio`]) + /// - alpha ([`ratio`]) + /// + /// These components are also available using the [`components`]($color.components) + /// method. + /// + /// ```example + /// #square(fill: rgb("#b1f2eb")) + /// #square(fill: rgb(87, 127, 230)) + /// #square(fill: rgb(25%, 13%, 65%)) + /// ``` + #[func(title = "RGB")] + pub fn rgb( + /// The real arguments (the other arguments are just for the docs, this + /// function is a bit involved, so we parse the arguments manually). + args: &mut Args, + /// The red component. + #[external] + red: Component, + /// The green component. + #[external] + green: Component, + /// The blue component. + #[external] + blue: Component, + /// The alpha component. + #[external] + alpha: Component, + /// Alternatively: The color in hexadecimal notation. + /// + /// Accepts three, four, six or eight hexadecimal digits and optionally + /// a leading hash. + /// + /// If this is given, the individual components should not be given. + /// + /// ```example + /// #text(16pt, rgb("#239dad"))[ + /// *Typst* + /// ] + /// ``` + #[external] + hex: Str, + /// Alternatively: The color to convert to RGB(a). + /// + /// If this is given, the individual components should not be given. + #[external] + color: Color, + ) -> SourceResult<Color> { + Ok(if let Some(string) = args.find::<Spanned<Str>>()? { + Self::from_str(&string.v).at(string.span)? + } else if let Some(color) = args.find::<Color>()? { + color.to_rgb() + } else { + let Component(r) = args.expect("red component")?; + let Component(g) = args.expect("green component")?; + let Component(b) = args.expect("blue component")?; + let Component(a) = args.eat()?.unwrap_or(Component(Ratio::one())); + Self::Rgb(Rgb::new( + r.get() as f32, + g.get() as f32, + b.get() as f32, + a.get() as f32, + )) + }) + } + + /// Create a CMYK color. + /// + /// This is useful if you want to target a specific printer. The conversion + /// to RGB for display preview might differ from how your printer reproduces + /// the color. + /// + /// A CMYK color is represented internally by an array of four components: + /// - cyan ([`ratio`]) + /// - magenta ([`ratio`]) + /// - yellow ([`ratio`]) + /// - key ([`ratio`]) + /// + /// These components are also available using the + /// [`components`]($color.components) method. + /// + /// Note that CMYK colors are not currently supported when PDF/A output is + /// enabled. + /// + /// ```example + /// #square( + /// fill: cmyk(27%, 0%, 3%, 5%) + /// ) + /// ``` + #[func(title = "CMYK")] + pub fn cmyk( + /// The real arguments (the other arguments are just for the docs, this + /// function is a bit involved, so we parse the arguments manually). + args: &mut Args, + /// The cyan component. + #[external] + cyan: RatioComponent, + /// The magenta component. + #[external] + magenta: RatioComponent, + /// The yellow component. + #[external] + yellow: RatioComponent, + /// The key component. + #[external] + key: RatioComponent, + /// Alternatively: The color to convert to CMYK. + /// + /// If this is given, the individual components should not be given. + #[external] + color: Color, + ) -> SourceResult<Color> { + Ok(if let Some(color) = args.find::<Color>()? { + color.to_cmyk() + } else { + let RatioComponent(c) = args.expect("cyan component")?; + let RatioComponent(m) = args.expect("magenta component")?; + let RatioComponent(y) = args.expect("yellow component")?; + let RatioComponent(k) = args.expect("key/black component")?; + Self::Cmyk(Cmyk::new( + c.get() as f32, + m.get() as f32, + y.get() as f32, + k.get() as f32, + )) + }) + } + + /// Create an HSL color. + /// + /// This color space is useful for specifying colors by hue, saturation and + /// lightness. It is also useful for color manipulation, such as saturating + /// while keeping perceived hue. + /// + /// An HSL color is represented internally by an array of four components: + /// - hue ([`angle`]) + /// - saturation ([`ratio`]) + /// - lightness ([`ratio`]) + /// - alpha ([`ratio`]) + /// + /// These components are also available using the + /// [`components`]($color.components) method. + /// + /// ```example + /// #square( + /// fill: color.hsl(30deg, 50%, 60%) + /// ) + /// ``` + #[func(title = "HSL")] + pub fn hsl( + /// The real arguments (the other arguments are just for the docs, this + /// function is a bit involved, so we parse the arguments manually). + args: &mut Args, + /// The hue angle. + #[external] + hue: Angle, + /// The saturation component. + #[external] + saturation: Component, + /// The lightness component. + #[external] + lightness: Component, + /// The alpha component. + #[external] + alpha: Component, + /// Alternatively: The color to convert to HSL. + /// + /// If this is given, the individual components should not be given. + #[external] + color: Color, + ) -> SourceResult<Color> { + Ok(if let Some(color) = args.find::<Color>()? { + color.to_hsl() + } else { + let h: Angle = args.expect("hue component")?; + let Component(s) = args.expect("saturation component")?; + let Component(l) = args.expect("lightness component")?; + let Component(a) = args.eat()?.unwrap_or(Component(Ratio::one())); + Self::Hsl(Hsl::new( + RgbHue::from_degrees(h.to_deg() as f32), + s.get() as f32, + l.get() as f32, + a.get() as f32, + )) + }) + } + + /// Create an HSV color. + /// + /// This color space is useful for specifying colors by hue, saturation and + /// value. It is also useful for color manipulation, such as saturating + /// while keeping perceived hue. + /// + /// An HSV color is represented internally by an array of four components: + /// - hue ([`angle`]) + /// - saturation ([`ratio`]) + /// - value ([`ratio`]) + /// - alpha ([`ratio`]) + /// + /// These components are also available using the + /// [`components`]($color.components) method. + /// + /// ```example + /// #square( + /// fill: color.hsv(30deg, 50%, 60%) + /// ) + /// ``` + #[func(title = "HSV")] + pub fn hsv( + /// The real arguments (the other arguments are just for the docs, this + /// function is a bit involved, so we parse the arguments manually). + args: &mut Args, + /// The hue angle. + #[external] + hue: Angle, + /// The saturation component. + #[external] + saturation: Component, + /// The value component. + #[external] + value: Component, + /// The alpha component. + #[external] + alpha: Component, + /// Alternatively: The color to convert to HSL. + /// + /// If this is given, the individual components should not be given. + #[external] + color: Color, + ) -> SourceResult<Color> { + Ok(if let Some(color) = args.find::<Color>()? { + color.to_hsv() + } else { + let h: Angle = args.expect("hue component")?; + let Component(s) = args.expect("saturation component")?; + let Component(v) = args.expect("value component")?; + let Component(a) = args.eat()?.unwrap_or(Component(Ratio::one())); + Self::Hsv(Hsv::new( + RgbHue::from_degrees(h.to_deg() as f32), + s.get() as f32, + v.get() as f32, + a.get() as f32, + )) + }) + } + + /// Extracts the components of this color. + /// + /// The size and values of this array depends on the color space. You can + /// obtain the color space using [`space`]($color.space). Below is a table + /// of the color spaces and their components: + /// + /// | Color space | C1 | C2 | C3 | C4 | + /// |-------------------------|-----------|------------|-----------|--------| + /// | [`luma`]($color.luma) | Lightness | | | | + /// | [`oklab`]($color.oklab) | Lightness | `a` | `b` | Alpha | + /// | [`oklch`]($color.oklch) | Lightness | Chroma | Hue | Alpha | + /// | [`linear-rgb`]($color.linear-rgb) | Red | Green | Blue | Alpha | + /// | [`rgb`]($color.rgb) | Red | Green | Blue | Alpha | + /// | [`cmyk`]($color.cmyk) | Cyan | Magenta | Yellow | Key | + /// | [`hsl`]($color.hsl) | Hue | Saturation | Lightness | Alpha | + /// | [`hsv`]($color.hsv) | Hue | Saturation | Value | Alpha | + /// + /// For the meaning and type of each individual value, see the documentation + /// of the corresponding color space. The alpha component is optional and + /// only included if the `alpha` argument is `true`. The length of the + /// returned array depends on the number of components and whether the alpha + /// component is included. + /// + /// ```example + /// // note that the alpha component is included by default + /// #rgb(40%, 60%, 80%).components() + /// ``` + #[func] + pub fn components( + self, + /// Whether to include the alpha component. + #[named] + #[default(true)] + alpha: bool, + ) -> Array { + let mut components = match self { + Self::Luma(c) => { + array![Ratio::new(c.luma.into()), Ratio::new(c.alpha.into())] + } + Self::Oklab(c) => { + array![ + Ratio::new(c.l.into()), + f64::from(c.a), + f64::from(c.b), + Ratio::new(c.alpha.into()) + ] + } + Self::Oklch(c) => { + array![ + Ratio::new(c.l.into()), + f64::from(c.chroma), + hue_angle(c.hue.into_degrees()), + Ratio::new(c.alpha.into()), + ] + } + Self::LinearRgb(c) => { + array![ + Ratio::new(c.red.into()), + Ratio::new(c.green.into()), + Ratio::new(c.blue.into()), + Ratio::new(c.alpha.into()), + ] + } + Self::Rgb(c) => { + array![ + Ratio::new(c.red.into()), + Ratio::new(c.green.into()), + Ratio::new(c.blue.into()), + Ratio::new(c.alpha.into()), + ] + } + Self::Cmyk(c) => { + array![ + Ratio::new(c.c.into()), + Ratio::new(c.m.into()), + Ratio::new(c.y.into()), + Ratio::new(c.k.into()) + ] + } + Self::Hsl(c) => { + array![ + hue_angle(c.hue.into_degrees()), + Ratio::new(c.saturation.into()), + Ratio::new(c.lightness.into()), + Ratio::new(c.alpha.into()), + ] + } + Self::Hsv(c) => { + array![ + hue_angle(c.hue.into_degrees()), + Ratio::new(c.saturation.into()), + Ratio::new(c.value.into()), + Ratio::new(c.alpha.into()), + ] + } + }; + // Remove the alpha component if the corresponding argument was set. + if !alpha && !matches!(self, Self::Cmyk(_)) { + let _ = components.pop(); + } + components + } + + /// Returns the constructor function for this color's space: + /// - [`luma`]($color.luma) + /// - [`oklab`]($color.oklab) + /// - [`oklch`]($color.oklch) + /// - [`linear-rgb`]($color.linear-rgb) + /// - [`rgb`]($color.rgb) + /// - [`cmyk`]($color.cmyk) + /// - [`hsl`]($color.hsl) + /// - [`hsv`]($color.hsv) + /// + /// ```example + /// #let color = cmyk(1%, 2%, 3%, 4%) + /// #(color.space() == cmyk) + /// ``` + #[func] + pub fn space(self) -> ColorSpace { + match self { + Self::Luma(_) => ColorSpace::D65Gray, + Self::Oklab(_) => ColorSpace::Oklab, + Self::Oklch(_) => ColorSpace::Oklch, + Self::LinearRgb(_) => ColorSpace::LinearRgb, + Self::Rgb(_) => ColorSpace::Srgb, + Self::Cmyk(_) => ColorSpace::Cmyk, + Self::Hsl(_) => ColorSpace::Hsl, + Self::Hsv(_) => ColorSpace::Hsv, + } + } + + /// Returns the color's RGB(A) hex representation (such as `#ffaa32` or + /// `#020304fe`). The alpha component (last two digits in `#020304fe`) is + /// omitted if it is equal to `ff` (255 / 100%). + #[func] + pub fn to_hex(self) -> EcoString { + let [r, g, b, a] = self.to_rgb().to_vec4_u8(); + if a != 255 { + eco_format!("#{:02x}{:02x}{:02x}{:02x}", r, g, b, a) + } else { + eco_format!("#{:02x}{:02x}{:02x}", r, g, b) + } + } + + /// Lightens a color by a given factor. + #[func] + pub fn lighten( + self, + /// The factor to lighten the color by. + factor: Ratio, + ) -> Color { + let factor = factor.get() as f32; + match self { + Self::Luma(c) => Self::Luma(c.lighten(factor)), + Self::Oklab(c) => Self::Oklab(c.lighten(factor)), + Self::Oklch(c) => Self::Oklch(c.lighten(factor)), + Self::LinearRgb(c) => Self::LinearRgb(c.lighten(factor)), + Self::Rgb(c) => Self::Rgb(c.lighten(factor)), + Self::Cmyk(c) => Self::Cmyk(c.lighten(factor)), + Self::Hsl(c) => Self::Hsl(c.lighten(factor)), + Self::Hsv(c) => Self::Hsv(c.lighten(factor)), + } + } + + /// Darkens a color by a given factor. + #[func] + pub fn darken( + self, + /// The factor to darken the color by. + factor: Ratio, + ) -> Color { + let factor = factor.get() as f32; + match self { + Self::Luma(c) => Self::Luma(c.darken(factor)), + Self::Oklab(c) => Self::Oklab(c.darken(factor)), + Self::Oklch(c) => Self::Oklch(c.darken(factor)), + Self::LinearRgb(c) => Self::LinearRgb(c.darken(factor)), + Self::Rgb(c) => Self::Rgb(c.darken(factor)), + Self::Cmyk(c) => Self::Cmyk(c.darken(factor)), + Self::Hsl(c) => Self::Hsl(c.darken(factor)), + Self::Hsv(c) => Self::Hsv(c.darken(factor)), + } + } + + /// Increases the saturation of a color by a given factor. + #[func] + pub fn saturate( + self, + /// The call span + span: Span, + /// The factor to saturate the color by. + factor: Ratio, + ) -> SourceResult<Color> { + Ok(match self { + Self::Luma(_) => { + bail!( + span, "cannot saturate grayscale color"; + hint: "try converting your color to RGB first" + ); + } + Self::Oklab(_) => self.to_hsv().saturate(span, factor)?.to_oklab(), + Self::Oklch(_) => self.to_hsv().saturate(span, factor)?.to_oklch(), + Self::LinearRgb(_) => self.to_hsv().saturate(span, factor)?.to_linear_rgb(), + Self::Rgb(_) => self.to_hsv().saturate(span, factor)?.to_rgb(), + Self::Cmyk(_) => self.to_hsv().saturate(span, factor)?.to_cmyk(), + Self::Hsl(c) => Self::Hsl(c.saturate(factor.get() as f32)), + Self::Hsv(c) => Self::Hsv(c.saturate(factor.get() as f32)), + }) + } + + /// Decreases the saturation of a color by a given factor. + #[func] + pub fn desaturate( + self, + /// The call span + span: Span, + /// The factor to desaturate the color by. + factor: Ratio, + ) -> SourceResult<Color> { + Ok(match self { + Self::Luma(_) => { + bail!( + span, "cannot desaturate grayscale color"; + hint: "try converting your color to RGB first" + ); + } + Self::Oklab(_) => self.to_hsv().desaturate(span, factor)?.to_oklab(), + Self::Oklch(_) => self.to_hsv().desaturate(span, factor)?.to_oklch(), + Self::LinearRgb(_) => self.to_hsv().desaturate(span, factor)?.to_linear_rgb(), + Self::Rgb(_) => self.to_hsv().desaturate(span, factor)?.to_rgb(), + Self::Cmyk(_) => self.to_hsv().desaturate(span, factor)?.to_cmyk(), + Self::Hsl(c) => Self::Hsl(c.desaturate(factor.get() as f32)), + Self::Hsv(c) => Self::Hsv(c.desaturate(factor.get() as f32)), + }) + } + + /// Produces the complementary color using a provided color space. + /// You can think of it as the opposite side on a color wheel. + /// + /// ```example + /// #square(fill: yellow) + /// #square(fill: yellow.negate()) + /// #square(fill: yellow.negate(space: rgb)) + /// ``` + #[func] + pub fn negate( + self, + /// The color space used for the transformation. By default, a perceptual color space is used. + #[named] + #[default(ColorSpace::Oklab)] + space: ColorSpace, + ) -> Color { + let result = match self.to_space(space) { + Self::Luma(c) => Self::Luma(Luma::new(1.0 - c.luma, c.alpha)), + Self::Oklab(c) => Self::Oklab(Oklab::new(1.0 - c.l, -c.a, -c.b, c.alpha)), + Self::Oklch(c) => Self::Oklch(Oklch::new( + 1.0 - c.l, + c.chroma, + OklabHue::from_degrees(c.hue.into_degrees() + 180.0), + c.alpha, + )), + Self::LinearRgb(c) => Self::LinearRgb(LinearRgb::new( + 1.0 - c.red, + 1.0 - c.green, + 1.0 - c.blue, + c.alpha, + )), + Self::Rgb(c) => { + Self::Rgb(Rgb::new(1.0 - c.red, 1.0 - c.green, 1.0 - c.blue, c.alpha)) + } + Self::Cmyk(c) => Self::Cmyk(Cmyk::new(1.0 - c.c, 1.0 - c.m, 1.0 - c.y, c.k)), + Self::Hsl(c) => Self::Hsl(Hsl::new( + RgbHue::from_degrees(c.hue.into_degrees() + 180.0), + c.saturation, + c.lightness, + c.alpha, + )), + Self::Hsv(c) => Self::Hsv(Hsv::new( + RgbHue::from_degrees(c.hue.into_degrees() + 180.0), + c.saturation, + c.value, + c.alpha, + )), + }; + result.to_space(self.space()) + } + + /// Rotates the hue of the color by a given angle. + #[func] + pub fn rotate( + self, + /// The call span + span: Span, + /// The angle to rotate the hue by. + angle: Angle, + /// The color space used to rotate. By default, this happens in a perceptual + /// color space ([`oklch`]($color.oklch)). + #[named] + #[default(ColorSpace::Oklch)] + space: ColorSpace, + ) -> SourceResult<Color> { + Ok(match space { + ColorSpace::Oklch => { + let Self::Oklch(oklch) = self.to_oklch() else { + unreachable!(); + }; + let rotated = oklch.shift_hue(angle.to_deg() as f32); + Self::Oklch(rotated).to_space(self.space()) + } + ColorSpace::Hsl => { + let Self::Hsl(hsl) = self.to_hsl() else { + unreachable!(); + }; + let rotated = hsl.shift_hue(angle.to_deg() as f32); + Self::Hsl(rotated).to_space(self.space()) + } + ColorSpace::Hsv => { + let Self::Hsv(hsv) = self.to_hsv() else { + unreachable!(); + }; + let rotated = hsv.shift_hue(angle.to_deg() as f32); + Self::Hsv(rotated).to_space(self.space()) + } + _ => bail!(span, "this colorspace does not support hue rotation"), + }) + } + + /// Create a color by mixing two or more colors. + /// + /// In color spaces with a hue component (hsl, hsv, oklch), only two colors + /// can be mixed at once. Mixing more than two colors in such a space will + /// result in an error! + /// + /// ```example + /// #set block(height: 20pt, width: 100%) + /// #block(fill: red.mix(blue)) + /// #block(fill: red.mix(blue, space: rgb)) + /// #block(fill: color.mix(red, blue, white)) + /// #block(fill: color.mix((red, 70%), (blue, 30%))) + /// ``` + #[func] + pub fn mix( + /// The colors, optionally with weights, specified as a pair (array of + /// length two) of color and weight (float or ratio). + /// + /// The weights do not need to add to `{100%}`, they are relative to the + /// sum of all weights. + #[variadic] + colors: Vec<WeightedColor>, + /// The color space to mix in. By default, this happens in a perceptual + /// color space ([`oklab`]($color.oklab)). + #[named] + #[default(ColorSpace::Oklab)] + space: ColorSpace, + ) -> StrResult<Color> { + Self::mix_iter(colors, space) + } + + /// Makes a color more transparent by a given factor. + /// + /// This method is relative to the existing alpha value. + /// If the scale is positive, calculates `alpha - alpha * scale`. + /// Negative scales behave like `color.opacify(-scale)`. + /// + /// ```example + /// #block(fill: red)[opaque] + /// #block(fill: red.transparentize(50%))[half red] + /// #block(fill: red.transparentize(75%))[quarter red] + /// ``` + #[func] + pub fn transparentize( + self, + /// The factor to change the alpha value by. + scale: Ratio, + ) -> StrResult<Color> { + self.scale_alpha(-scale) + } + + /// Makes a color more opaque by a given scale. + /// + /// This method is relative to the existing alpha value. + /// If the scale is positive, calculates `alpha + scale - alpha * scale`. + /// Negative scales behave like `color.transparentize(-scale)`. + /// + /// ```example + /// #let half-red = red.transparentize(50%) + /// #block(fill: half-red.opacify(100%))[opaque] + /// #block(fill: half-red.opacify(50%))[three quarters red] + /// #block(fill: half-red.opacify(-50%))[one quarter red] + /// ``` + #[func] + pub fn opacify( + self, + /// The scale to change the alpha value by. + scale: Ratio, + ) -> StrResult<Color> { + self.scale_alpha(scale) + } +} + +impl Color { + /// Same as [`Color::mix`], but takes an iterator instead of a vector. + pub fn mix_iter( + colors: impl IntoIterator< + Item = WeightedColor, + IntoIter = impl ExactSizeIterator<Item = WeightedColor>, + >, + space: ColorSpace, + ) -> StrResult<Color> { + let mut colors = colors.into_iter(); + if space.hue_index().is_some() && colors.len() > 2 { + bail!("cannot mix more than two colors in a hue-based space"); + } + + let m = if space.hue_index().is_some() && colors.len() == 2 { + let mut m = [0.0; 4]; + + let WeightedColor { color: c0, weight: w0 } = colors.next().unwrap(); + let WeightedColor { color: c1, weight: w1 } = colors.next().unwrap(); + + let c0 = c0.to_space(space).to_vec4(); + let c1 = c1.to_space(space).to_vec4(); + let w0 = w0 as f32; + let w1 = w1 as f32; + + if w0 + w1 <= 0.0 { + bail!("sum of weights must be positive"); + } + + for i in 0..4 { + m[i] = (w0 * c0[i] + w1 * c1[i]) / (w0 + w1); + } + + // Ensure that the hue circle is traversed in the short direction. + if let Some(index) = space.hue_index() { + if (c0[index] - c1[index]).abs() > 180.0 { + let (h0, h1) = if c0[index] < c1[index] { + (c0[index] + 360.0, c1[index]) + } else { + (c0[index], c1[index] + 360.0) + }; + m[index] = (w0 * h0 + w1 * h1) / (w0 + w1); + } + } + + m + } else { + let mut total = 0.0; + let mut acc = [0.0; 4]; + + for WeightedColor { color, weight } in colors { + let weight = weight as f32; + let v = color.to_space(space).to_vec4(); + acc[0] += weight * v[0]; + acc[1] += weight * v[1]; + acc[2] += weight * v[2]; + acc[3] += weight * v[3]; + total += weight; + } + + if total <= 0.0 { + bail!("sum of weights must be positive"); + } + + acc.map(|v| v / total) + }; + + Ok(match space { + ColorSpace::Oklab => Color::Oklab(Oklab::new(m[0], m[1], m[2], m[3])), + ColorSpace::Oklch => Color::Oklch(Oklch::new(m[0], m[1], m[2], m[3])), + ColorSpace::Srgb => Color::Rgb(Rgb::new(m[0], m[1], m[2], m[3])), + ColorSpace::LinearRgb => { + Color::LinearRgb(LinearRgb::new(m[0], m[1], m[2], m[3])) + } + ColorSpace::Hsl => { + Color::Hsl(Hsl::new(RgbHue::from_degrees(m[0]), m[1], m[2], m[3])) + } + ColorSpace::Hsv => { + Color::Hsv(Hsv::new(RgbHue::from_degrees(m[0]), m[1], m[2], m[3])) + } + ColorSpace::Cmyk => Color::Cmyk(Cmyk::new(m[0], m[1], m[2], m[3])), + ColorSpace::D65Gray => Color::Luma(Luma::new(m[0], m[3])), + }) + } + + /// Construct a new RGBA color from 8-bit values. + pub fn from_u8(r: u8, g: u8, b: u8, a: u8) -> Self { + Self::Rgb(Rgb::new( + f32::from(r) / 255.0, + f32::from(g) / 255.0, + f32::from(b) / 255.0, + f32::from(a) / 255.0, + )) + } + + /// Converts a 32-bit integer to an RGBA color. + pub fn from_u32(color: u32) -> Self { + Self::from_u8( + ((color >> 24) & 0xFF) as u8, + ((color >> 16) & 0xFF) as u8, + ((color >> 8) & 0xFF) as u8, + (color & 0xFF) as u8, + ) + } + + /// Returns the alpha channel of the color, if it has one. + pub fn alpha(&self) -> Option<f32> { + match self { + Color::Cmyk(_) => None, + Color::Luma(c) => Some(c.alpha), + Color::Oklab(c) => Some(c.alpha), + Color::Oklch(c) => Some(c.alpha), + Color::Rgb(c) => Some(c.alpha), + Color::LinearRgb(c) => Some(c.alpha), + Color::Hsl(c) => Some(c.alpha), + Color::Hsv(c) => Some(c.alpha), + } + } + + /// Sets the alpha channel of the color, if it has one. + pub fn with_alpha(mut self, alpha: f32) -> Self { + match &mut self { + Color::Cmyk(_) => {} + Color::Luma(c) => c.alpha = alpha, + Color::Oklab(c) => c.alpha = alpha, + Color::Oklch(c) => c.alpha = alpha, + Color::Rgb(c) => c.alpha = alpha, + Color::LinearRgb(c) => c.alpha = alpha, + Color::Hsl(c) => c.alpha = alpha, + Color::Hsv(c) => c.alpha = alpha, + } + + self + } + + /// Scales the alpha value of a color by a given amount. + /// + /// For positive scales, computes `alpha + scale - alpha * scale`. + /// For non-positive scales, computes `alpha + alpha * scale`. + fn scale_alpha(self, scale: Ratio) -> StrResult<Color> { + #[inline] + fn transform<C>(mut color: Alpha<C, f32>, scale: Ratio) -> Alpha<C, f32> { + let scale = scale.get() as f32; + let factor = if scale > 0.0 { 1.0 - color.alpha } else { color.alpha }; + color.alpha = (color.alpha + scale * factor).clamp(0.0, 1.0); + color + } + + Ok(match self { + Color::Luma(c) => Color::Luma(transform(c, scale)), + Color::Oklab(c) => Color::Oklab(transform(c, scale)), + Color::Oklch(c) => Color::Oklch(transform(c, scale)), + Color::Rgb(c) => Color::Rgb(transform(c, scale)), + Color::LinearRgb(c) => Color::LinearRgb(transform(c, scale)), + Color::Cmyk(_) => bail!("CMYK does not have an alpha component"), + Color::Hsl(c) => Color::Hsl(transform(c, scale)), + Color::Hsv(c) => Color::Hsv(transform(c, scale)), + }) + } + + /// Converts the color to a vec of four floats. + pub fn to_vec4(&self) -> [f32; 4] { + match self { + Color::Luma(c) => [c.luma, c.luma, c.luma, c.alpha], + Color::Oklab(c) => [c.l, c.a, c.b, c.alpha], + Color::Oklch(c) => { + [c.l, c.chroma, c.hue.into_degrees().rem_euclid(360.0), c.alpha] + } + Color::Rgb(c) => [c.red, c.green, c.blue, c.alpha], + Color::LinearRgb(c) => [c.red, c.green, c.blue, c.alpha], + Color::Cmyk(c) => [c.c, c.m, c.y, c.k], + Color::Hsl(c) => [ + c.hue.into_degrees().rem_euclid(360.0), + c.saturation, + c.lightness, + c.alpha, + ], + Color::Hsv(c) => { + [c.hue.into_degrees().rem_euclid(360.0), c.saturation, c.value, c.alpha] + } + } + } + + /// Converts the color to a vec of four [`u8`]s. + pub fn to_vec4_u8(&self) -> [u8; 4] { + self.to_vec4().map(|x| (x * 255.0).round() as u8) + } + + pub fn to_space(self, space: ColorSpace) -> Self { + match space { + ColorSpace::Oklab => self.to_oklab(), + ColorSpace::Oklch => self.to_oklch(), + ColorSpace::Srgb => self.to_rgb(), + ColorSpace::LinearRgb => self.to_linear_rgb(), + ColorSpace::Hsl => self.to_hsl(), + ColorSpace::Hsv => self.to_hsv(), + ColorSpace::Cmyk => self.to_cmyk(), + ColorSpace::D65Gray => self.to_luma(), + } + } + + pub fn to_luma(self) -> Self { + Self::Luma(match self { + Self::Luma(c) => c, + Self::Oklab(c) => Luma::from_color(c), + Self::Oklch(c) => Luma::from_color(c), + Self::Rgb(c) => Luma::from_color(c), + Self::LinearRgb(c) => Luma::from_color(c), + Self::Cmyk(c) => Luma::from_color(c.to_rgba()), + Self::Hsl(c) => Luma::from_color(c), + Self::Hsv(c) => Luma::from_color(c), + }) + } + + pub fn to_oklab(self) -> Self { + Self::Oklab(match self { + Self::Luma(c) => Oklab::from_color(c), + Self::Oklab(c) => c, + Self::Oklch(c) => Oklab::from_color(c), + Self::Rgb(c) => Oklab::from_color(c), + Self::LinearRgb(c) => Oklab::from_color(c), + Self::Cmyk(c) => Oklab::from_color(c.to_rgba()), + Self::Hsl(c) => Oklab::from_color(c), + Self::Hsv(c) => Oklab::from_color(c), + }) + } + + pub fn to_oklch(self) -> Self { + Self::Oklch(match self { + Self::Luma(c) => Oklch::from_color(c), + Self::Oklab(c) => Oklch::from_color(c), + Self::Oklch(c) => c, + Self::Rgb(c) => Oklch::from_color(c), + Self::LinearRgb(c) => Oklch::from_color(c), + Self::Cmyk(c) => Oklch::from_color(c.to_rgba()), + Self::Hsl(c) => Oklch::from_color(c), + Self::Hsv(c) => Oklch::from_color(c), + }) + } + + pub fn to_rgb(self) -> Self { + Self::Rgb(match self { + Self::Luma(c) => Rgb::from_color(c), + Self::Oklab(c) => Rgb::from_color(c), + Self::Oklch(c) => Rgb::from_color(c), + Self::Rgb(c) => c, + Self::LinearRgb(c) => Rgb::from_linear(c), + Self::Cmyk(c) => Rgb::from_color(c.to_rgba()), + Self::Hsl(c) => Rgb::from_color(c), + Self::Hsv(c) => Rgb::from_color(c), + }) + } + + pub fn to_linear_rgb(self) -> Self { + Self::LinearRgb(match self { + Self::Luma(c) => LinearRgb::from_color(c), + Self::Oklab(c) => LinearRgb::from_color(c), + Self::Oklch(c) => LinearRgb::from_color(c), + Self::Rgb(c) => LinearRgb::from_color(c), + Self::LinearRgb(c) => c, + Self::Cmyk(c) => LinearRgb::from_color(c.to_rgba()), + Self::Hsl(c) => Rgb::from_color(c).into_linear(), + Self::Hsv(c) => Rgb::from_color(c).into_linear(), + }) + } + + pub fn to_cmyk(self) -> Self { + Self::Cmyk(match self { + Self::Luma(c) => Cmyk::from_luma(c), + Self::Oklab(c) => Cmyk::from_rgba(Rgb::from_color(c)), + Self::Oklch(c) => Cmyk::from_rgba(Rgb::from_color(c)), + Self::Rgb(c) => Cmyk::from_rgba(c), + Self::LinearRgb(c) => Cmyk::from_rgba(Rgb::from_linear(c)), + Self::Cmyk(c) => c, + Self::Hsl(c) => Cmyk::from_rgba(Rgb::from_color(c)), + Self::Hsv(c) => Cmyk::from_rgba(Rgb::from_color(c)), + }) + } + + pub fn to_hsl(self) -> Self { + Self::Hsl(match self { + Self::Luma(c) => Hsl::from_color(c), + Self::Oklab(c) => Hsl::from_color(c), + Self::Oklch(c) => Hsl::from_color(c), + Self::Rgb(c) => Hsl::from_color(c), + Self::LinearRgb(c) => Hsl::from_color(Rgb::from_linear(c)), + Self::Cmyk(c) => Hsl::from_color(c.to_rgba()), + Self::Hsl(c) => c, + Self::Hsv(c) => Hsl::from_color(c), + }) + } + + pub fn to_hsv(self) -> Self { + Self::Hsv(match self { + Self::Luma(c) => Hsv::from_color(c), + Self::Oklab(c) => Hsv::from_color(c), + Self::Oklch(c) => Hsv::from_color(c), + Self::Rgb(c) => Hsv::from_color(c), + Self::LinearRgb(c) => Hsv::from_color(Rgb::from_linear(c)), + Self::Cmyk(c) => Hsv::from_color(c.to_rgba()), + Self::Hsl(c) => Hsv::from_color(c), + Self::Hsv(c) => c, + }) + } +} + +impl Debug for Color { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Luma(v) => write!(f, "Luma({}, {})", v.luma, v.alpha), + Self::Oklab(v) => write!(f, "Oklab({}, {}, {}, {})", v.l, v.a, v.b, v.alpha), + Self::Oklch(v) => { + write!( + f, + "Oklch({}, {}, {:?}, {})", + v.l, + v.chroma, + hue_angle(v.hue.into_degrees()), + v.alpha + ) + } + Self::Rgb(v) => { + write!(f, "Rgb({}, {}, {}, {})", v.red, v.green, v.blue, v.alpha) + } + Self::LinearRgb(v) => { + write!(f, "LinearRgb({}, {}, {}, {})", v.red, v.green, v.blue, v.alpha) + } + Self::Cmyk(v) => write!(f, "Cmyk({}, {}, {}, {})", v.c, v.m, v.y, v.k), + Self::Hsl(v) => write!( + f, + "Hsl({:?}, {}, {}, {})", + hue_angle(v.hue.into_degrees()), + v.saturation, + v.lightness, + v.alpha + ), + Self::Hsv(v) => write!( + f, + "Hsv({:?}, {}, {}, {})", + hue_angle(v.hue.into_degrees()), + v.saturation, + v.value, + v.alpha + ), + } + } +} + +impl Repr for Color { + fn repr(&self) -> EcoString { + match self { + Self::Luma(c) => { + if c.alpha == 1.0 { + eco_format!("luma({})", Ratio::new(c.luma.into()).repr()) + } else { + eco_format!( + "luma({}, {})", + Ratio::new(c.luma.into()).repr(), + Ratio::new(c.alpha.into()).repr(), + ) + } + } + Self::Rgb(_) => eco_format!("rgb({})", self.to_hex().repr()), + Self::LinearRgb(c) => { + if c.alpha == 1.0 { + eco_format!( + "color.linear-rgb({}, {}, {})", + Ratio::new(c.red.into()).repr(), + Ratio::new(c.green.into()).repr(), + Ratio::new(c.blue.into()).repr(), + ) + } else { + eco_format!( + "color.linear-rgb({}, {}, {}, {})", + Ratio::new(c.red.into()).repr(), + Ratio::new(c.green.into()).repr(), + Ratio::new(c.blue.into()).repr(), + Ratio::new(c.alpha.into()).repr(), + ) + } + } + Self::Cmyk(c) => { + eco_format!( + "cmyk({}, {}, {}, {})", + Ratio::new(c.c.into()).repr(), + Ratio::new(c.m.into()).repr(), + Ratio::new(c.y.into()).repr(), + Ratio::new(c.k.into()).repr(), + ) + } + Self::Oklab(c) => { + if c.alpha == 1.0 { + eco_format!( + "oklab({}, {}, {})", + Ratio::new(c.l.into()).repr(), + repr::format_float_component(c.a.into()), + repr::format_float_component(c.b.into()), + ) + } else { + eco_format!( + "oklab({}, {}, {}, {})", + Ratio::new(c.l.into()).repr(), + repr::format_float_component(c.a.into()), + repr::format_float_component(c.b.into()), + Ratio::new(c.alpha.into()).repr(), + ) + } + } + Self::Oklch(c) => { + if c.alpha == 1.0 { + eco_format!( + "oklch({}, {}, {})", + Ratio::new(c.l.into()).repr(), + repr::format_float_component(c.chroma.into()), + hue_angle(c.hue.into_degrees()).repr(), + ) + } else { + eco_format!( + "oklch({}, {}, {}, {})", + Ratio::new(c.l.into()).repr(), + repr::format_float_component(c.chroma.into()), + hue_angle(c.hue.into_degrees()).repr(), + Ratio::new(c.alpha.into()).repr(), + ) + } + } + Self::Hsl(c) => { + if c.alpha == 1.0 { + eco_format!( + "color.hsl({}, {}, {})", + hue_angle(c.hue.into_degrees()).repr(), + Ratio::new(c.saturation.into()).repr(), + Ratio::new(c.lightness.into()).repr(), + ) + } else { + eco_format!( + "color.hsl({}, {}, {}, {})", + hue_angle(c.hue.into_degrees()).repr(), + Ratio::new(c.saturation.into()).repr(), + Ratio::new(c.lightness.into()).repr(), + Ratio::new(c.alpha.into()).repr(), + ) + } + } + Self::Hsv(c) => { + if c.alpha == 1.0 { + eco_format!( + "color.hsv({}, {}, {})", + hue_angle(c.hue.into_degrees()).repr(), + Ratio::new(c.saturation.into()).repr(), + Ratio::new(c.value.into()).repr(), + ) + } else { + eco_format!( + "color.hsv({}, {}, {}, {})", + hue_angle(c.hue.into_degrees()).repr(), + Ratio::new(c.saturation.into()).repr(), + Ratio::new(c.value.into()).repr(), + Ratio::new(c.alpha.into()).repr(), + ) + } + } + } + } +} + +fn hue_angle(degrees: f32) -> Angle { + Angle::deg(f64::from(degrees).rem_euclid(360.0)) +} + +impl PartialEq for Color { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + // Lower precision for comparison to avoid rounding errors. + // Keeps backward compatibility with previous versions of Typst. + (Self::Rgb(_), Self::Rgb(_)) => self.to_vec4_u8() == other.to_vec4_u8(), + (Self::Luma(a), Self::Luma(b)) => { + (a.luma * 255.0).round() as u8 == (b.luma * 255.0).round() as u8 + } + (Self::Oklab(a), Self::Oklab(b)) => a == b, + (Self::Oklch(a), Self::Oklch(b)) => a == b, + (Self::LinearRgb(a), Self::LinearRgb(b)) => a == b, + (Self::Cmyk(a), Self::Cmyk(b)) => a == b, + (Self::Hsl(a), Self::Hsl(b)) => a == b, + (Self::Hsv(a), Self::Hsv(b)) => a == b, + _ => false, + } + } +} + +impl Eq for Color {} + +impl Hash for Color { + fn hash<H: Hasher>(&self, state: &mut H) { + core::mem::discriminant(self).hash(state); + let [x, y, z, w] = self.to_vec4(); + x.to_bits().hash(state); + y.to_bits().hash(state); + z.to_bits().hash(state); + w.to_bits().hash(state); + } +} + +impl FromStr for Color { + type Err = &'static str; + + /// Constructs a new color from hex strings like the following: + /// - `#aef` (shorthand, with leading hash), + /// - `7a03c2` (without alpha), + /// - `abcdefff` (with alpha). + /// + /// The hash 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::from_u8(values[0], values[1], values[2], values[3])) + } +} + +impl From<Luma> for Color { + fn from(c: Luma) -> Self { + Self::Luma(c) + } +} + +impl From<Oklab> for Color { + fn from(c: Oklab) -> Self { + Self::Oklab(c) + } +} + +impl From<Oklch> for Color { + fn from(c: Oklch) -> Self { + Self::Oklch(c) + } +} + +impl From<Rgb> for Color { + fn from(c: Rgb) -> Self { + Self::Rgb(c) + } +} + +impl From<LinearRgb> for Color { + fn from(c: LinearRgb) -> Self { + Self::LinearRgb(c) + } +} + +impl From<Cmyk> for Color { + fn from(c: Cmyk) -> Self { + Self::Cmyk(c) + } +} + +impl From<Hsl> for Color { + fn from(c: Hsl) -> Self { + Self::Hsl(c) + } +} + +impl From<Hsv> for Color { + fn from(c: Hsv) -> Self { + Self::Hsv(c) + } +} + +/// An 8-bit CMYK color. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Cmyk { + /// The cyan component. + pub c: f32, + /// The magenta component. + pub m: f32, + /// The yellow component. + pub y: f32, + /// The key (black) component. + pub k: f32, +} + +impl Cmyk { + fn new(c: f32, m: f32, y: f32, k: f32) -> Self { + Self { c, m, y, k } + } + + fn from_luma(luma: Luma) -> Self { + let l = 1.0 - luma.luma; + Cmyk::new(l * 0.75, l * 0.68, l * 0.67, l * 0.90) + } + + // This still uses naive conversion, because qcms does not support + // converting to CMYK yet. + fn from_rgba(rgba: Rgb) -> Self { + let r = rgba.red; + let g = rgba.green; + let b = rgba.blue; + + let k = 1.0 - r.max(g).max(b); + if k == 1.0 { + return Cmyk::new(0.0, 0.0, 0.0, 1.0); + } + + let c = (1.0 - r - k) / (1.0 - k); + let m = (1.0 - g - k) / (1.0 - k); + let y = (1.0 - b - k) / (1.0 - k); + + Cmyk::new(c, m, y, k) + } + + fn to_rgba(self) -> Rgb { + let mut dest: [u8; 3] = [0; 3]; + TO_SRGB.convert( + &[ + (self.c * 255.0).round() as u8, + (self.m * 255.0).round() as u8, + (self.y * 255.0).round() as u8, + (self.k * 255.0).round() as u8, + ], + &mut dest, + ); + + Rgb::new( + f32::from(dest[0]) / 255.0, + f32::from(dest[1]) / 255.0, + f32::from(dest[2]) / 255.0, + 1.0, + ) + } + + fn lighten(self, factor: f32) -> Self { + let lighten = |u: f32| (u - u * factor).clamp(0.0, 1.0); + Self::new(lighten(self.c), lighten(self.m), lighten(self.y), lighten(self.k)) + } + + fn darken(self, factor: f32) -> Self { + let darken = |u: f32| (u + (1.0 - u) * factor).clamp(0.0, 1.0); + Self::new(darken(self.c), darken(self.m), darken(self.y), darken(self.k)) + } +} + +/// A color with a weight. +pub struct WeightedColor { + color: Color, + weight: f64, +} + +impl WeightedColor { + /// Create a new weighted color. + pub const fn new(color: Color, weight: f64) -> Self { + Self { color, weight } + } +} + +cast! { + WeightedColor, + self => array![self.color, Value::Float(self.weight)].into_value(), + color: Color => Self { color, weight: 1.0 }, + v: Array => { + let mut iter = v.into_iter(); + match (iter.next(), iter.next(), iter.next()) { + (Some(c), Some(w), None) => Self { + color: c.cast()?, + weight: w.cast::<Weight>()?.0, + }, + _ => bail!("expected a color or color-weight pair"), + } + } +} + +/// A weight for color mixing. +struct Weight(f64); + +cast! { + Weight, + v: f64 => Self(v), + v: Ratio => Self(v.get()), +} + +/// A color space for color manipulation. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum ColorSpace { + /// The perceptual Oklab color space. + Oklab, + /// The perceptual Oklch color space. + Oklch, + /// The standard RGB color space. + Srgb, + /// The D65-gray color space. + D65Gray, + /// The linear RGB color space. + LinearRgb, + /// The HSL color space. + Hsl, + /// The HSV color space. + Hsv, + /// The CMYK color space. + Cmyk, +} + +impl ColorSpace { + /// Returns the index of the hue component in this color space, if it has + /// one. + pub fn hue_index(&self) -> Option<usize> { + match self { + Self::Hsl | Self::Hsv => Some(0), + Self::Oklch => Some(2), + _ => None, + } + } +} + +cast! { + ColorSpace, + self => match self { + Self::Oklab => Color::oklab_data(), + Self::Oklch => Color::oklch_data(), + Self::Srgb => Color::rgb_data(), + Self::D65Gray => Color::luma_data(), + Self::LinearRgb => Color::linear_rgb_data(), + Self::Hsl => Color::hsl_data(), + Self::Hsv => Color::hsv_data(), + Self::Cmyk => Color::cmyk_data(), + }.into_value(), + v: Value => { + let expected = "expected `rgb`, `luma`, `cmyk`, `oklab`, `oklch`, `color.linear-rgb`, `color.hsl`, or `color.hsv`"; + let Value::Func(func) = v else { + bail!("{expected}, found {}", v.ty()); + }; + + // Here comparing the function pointer since it's `Eq` + // whereas the `NativeFuncData` is not. + if func == Color::oklab_data() { + Self::Oklab + } else if func == Color::oklch_data() { + Self::Oklch + } else if func == Color::rgb_data() { + Self::Srgb + } else if func == Color::luma_data() { + Self::D65Gray + } else if func == Color::linear_rgb_data() { + Self::LinearRgb + } else if func == Color::hsl_data() { + Self::Hsl + } else if func == Color::hsv_data() { + Self::Hsv + } else if func == Color::cmyk_data() { + Self::Cmyk + } else { + bail!("{expected}"); + } + }, +} + +/// A component that must be a ratio. +pub struct RatioComponent(Ratio); + +cast! { + RatioComponent, + self => self.0.into_value(), + v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) { + Self(v) + } else { + bail!("ratio must be between 0% and 100%"); + }, +} + +/// A chroma color component. +/// +/// Must either be: +/// - a ratio, in which case it is relative to 0.4. +/// - a float, in which case it is taken literally. +pub struct ChromaComponent(f32); + +cast! { + ChromaComponent, + v: f64 => Self(v as f32), + v: Ratio => Self((v.get() * 0.4) as f32), +} + +/// An integer or ratio component. +pub struct Component(Ratio); + +cast! { + Component, + self => self.0.into_value(), + v: i64 => match v { + 0 ..= 255 => Self(Ratio::new(v as f64 / 255.0)), + _ => bail!("number must be between 0 and 255"), + }, + v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) { + Self(v) + } else { + bail!("ratio must be between 0% and 100%"); + }, +} + +/// A module with all preset color maps. +fn map() -> Module { + let mut scope = Scope::new(); + scope.define("turbo", turbo()); + scope.define("cividis", cividis()); + scope.define("rainbow", rainbow()); + scope.define("spectral", spectral()); + scope.define("viridis", viridis()); + scope.define("inferno", inferno()); + scope.define("magma", magma()); + scope.define("plasma", plasma()); + scope.define("rocket", rocket()); + scope.define("mako", mako()); + scope.define("vlag", vlag()); + scope.define("icefire", icefire()); + scope.define("flare", flare()); + scope.define("crest", crest()); + Module::new("map", scope) +} + +/// Defines a gradient preset as a series of colors expressed as u32s. +macro_rules! preset { + ($name:ident; $($colors:literal),* $(,)*) => { + fn $name() -> Array { + Array::from( + [$(Color::from_u32($colors)),*] + .iter() + .map(|c| c.into_value()) + .collect::<EcoVec<_>>() + ) + } + }; +} + +preset!(turbo; 0x23171bff, 0x271a28ff, 0x2b1c33ff, 0x2f1e3fff, 0x32204aff, 0x362354ff, 0x39255fff, 0x3b2768ff, 0x3e2a72ff, 0x402c7bff, 0x422f83ff, 0x44318bff, 0x453493ff, 0x46369bff, 0x4839a2ff, 0x493ca8ff, 0x493eafff, 0x4a41b5ff, 0x4a44bbff, 0x4b46c0ff, 0x4b49c5ff, 0x4b4ccaff, 0x4b4ecfff, 0x4b51d3ff, 0x4a54d7ff, 0x4a56dbff, 0x4959deff, 0x495ce2ff, 0x485fe5ff, 0x4761e7ff, 0x4664eaff, 0x4567ecff, 0x446aeeff, 0x446df0ff, 0x426ff2ff, 0x4172f3ff, 0x4075f5ff, 0x3f78f6ff, 0x3e7af7ff, 0x3d7df7ff, 0x3c80f8ff, 0x3a83f9ff, 0x3985f9ff, 0x3888f9ff, 0x378bf9ff, 0x368df9ff, 0x3590f8ff, 0x3393f8ff, 0x3295f7ff, 0x3198f7ff, 0x309bf6ff, 0x2f9df5ff, 0x2ea0f4ff, 0x2da2f3ff, 0x2ca5f1ff, 0x2ba7f0ff, 0x2aaaefff, 0x2aacedff, 0x29afecff, 0x28b1eaff, 0x28b4e8ff, 0x27b6e6ff, 0x27b8e5ff, 0x26bbe3ff, 0x26bde1ff, 0x26bfdfff, 0x25c1dcff, 0x25c3daff, 0x25c6d8ff, 0x25c8d6ff, 0x25cad3ff, 0x25ccd1ff, 0x25cecfff, 0x26d0ccff, 0x26d2caff, 0x26d4c8ff, 0x27d6c5ff, 0x27d8c3ff, 0x28d9c0ff, 0x29dbbeff, 0x29ddbbff, 0x2adfb8ff, 0x2be0b6ff, 0x2ce2b3ff, 0x2de3b1ff, 0x2ee5aeff, 0x30e6acff, 0x31e8a9ff, 0x32e9a6ff, 0x34eba4ff, 0x35eca1ff, 0x37ed9fff, 0x39ef9cff, 0x3af09aff, 0x3cf197ff, 0x3ef295ff, 0x40f392ff, 0x42f490ff, 0x44f58dff, 0x46f68bff, 0x48f788ff, 0x4af786ff, 0x4df884ff, 0x4ff981ff, 0x51fa7fff, 0x54fa7dff, 0x56fb7aff, 0x59fb78ff, 0x5cfc76ff, 0x5efc74ff, 0x61fd71ff, 0x64fd6fff, 0x66fd6dff, 0x69fd6bff, 0x6cfd69ff, 0x6ffe67ff, 0x72fe65ff, 0x75fe63ff, 0x78fe61ff, 0x7bfe5fff, 0x7efd5dff, 0x81fd5cff, 0x84fd5aff, 0x87fd58ff, 0x8afc56ff, 0x8dfc55ff, 0x90fb53ff, 0x93fb51ff, 0x96fa50ff, 0x99fa4eff, 0x9cf94dff, 0x9ff84bff, 0xa2f84aff, 0xa6f748ff, 0xa9f647ff, 0xacf546ff, 0xaff444ff, 0xb2f343ff, 0xb5f242ff, 0xb8f141ff, 0xbbf03fff, 0xbeef3eff, 0xc1ed3dff, 0xc3ec3cff, 0xc6eb3bff, 0xc9e93aff, 0xcce839ff, 0xcfe738ff, 0xd1e537ff, 0xd4e336ff, 0xd7e235ff, 0xd9e034ff, 0xdcdf33ff, 0xdedd32ff, 0xe0db32ff, 0xe3d931ff, 0xe5d730ff, 0xe7d52fff, 0xe9d42fff, 0xecd22eff, 0xeed02dff, 0xf0ce2cff, 0xf1cb2cff, 0xf3c92bff, 0xf5c72bff, 0xf7c52aff, 0xf8c329ff, 0xfac029ff, 0xfbbe28ff, 0xfdbc28ff, 0xfeb927ff, 0xffb727ff, 0xffb526ff, 0xffb226ff, 0xffb025ff, 0xffad25ff, 0xffab24ff, 0xffa824ff, 0xffa623ff, 0xffa323ff, 0xffa022ff, 0xff9e22ff, 0xff9b21ff, 0xff9921ff, 0xff9621ff, 0xff9320ff, 0xff9020ff, 0xff8e1fff, 0xff8b1fff, 0xff881eff, 0xff851eff, 0xff831dff, 0xff801dff, 0xff7d1dff, 0xff7a1cff, 0xff781cff, 0xff751bff, 0xff721bff, 0xff6f1aff, 0xfd6c1aff, 0xfc6a19ff, 0xfa6719ff, 0xf96418ff, 0xf76118ff, 0xf65f18ff, 0xf45c17ff, 0xf25916ff, 0xf05716ff, 0xee5415ff, 0xec5115ff, 0xea4f14ff, 0xe84c14ff, 0xe64913ff, 0xe44713ff, 0xe24412ff, 0xdf4212ff, 0xdd3f11ff, 0xda3d10ff, 0xd83a10ff, 0xd5380fff, 0xd3360fff, 0xd0330eff, 0xce310dff, 0xcb2f0dff, 0xc92d0cff, 0xc62a0bff, 0xc3280bff, 0xc1260aff, 0xbe2409ff, 0xbb2309ff, 0xb92108ff, 0xb61f07ff, 0xb41d07ff, 0xb11b06ff, 0xaf1a05ff, 0xac1805ff, 0xaa1704ff, 0xa81604ff, 0xa51403ff, 0xa31302ff, 0xa11202ff, 0x9f1101ff, 0x9d1000ff, 0x9b0f00ff, 0x9a0e00ff, 0x980e00ff, 0x960d00ff, 0x950c00ff, 0x940c00ff, 0x930c00ff, 0x920c00ff, 0x910b00ff, 0x910c00ff, 0x900c00ff, 0x900c00ff, 0x900c00ff); +preset!(cividis; 0x002051ff, 0x002153ff, 0x002255ff, 0x002356ff, 0x002358ff, 0x002459ff, 0x00255aff, 0x00255cff, 0x00265dff, 0x00275eff, 0x00275fff, 0x002860ff, 0x002961ff, 0x002962ff, 0x002a63ff, 0x002b64ff, 0x012b65ff, 0x022c65ff, 0x032d66ff, 0x042d67ff, 0x052e67ff, 0x052f68ff, 0x063069ff, 0x073069ff, 0x08316aff, 0x09326aff, 0x0b326aff, 0x0c336bff, 0x0d346bff, 0x0e346bff, 0x0f356cff, 0x10366cff, 0x12376cff, 0x13376dff, 0x14386dff, 0x15396dff, 0x17396dff, 0x183a6dff, 0x193b6dff, 0x1a3b6dff, 0x1c3c6eff, 0x1d3d6eff, 0x1e3e6eff, 0x203e6eff, 0x213f6eff, 0x23406eff, 0x24406eff, 0x25416eff, 0x27426eff, 0x28436eff, 0x29436eff, 0x2b446eff, 0x2c456eff, 0x2e456eff, 0x2f466eff, 0x30476eff, 0x32486eff, 0x33486eff, 0x34496eff, 0x364a6eff, 0x374a6eff, 0x394b6eff, 0x3a4c6eff, 0x3b4d6eff, 0x3d4d6eff, 0x3e4e6eff, 0x3f4f6eff, 0x414f6eff, 0x42506eff, 0x43516dff, 0x44526dff, 0x46526dff, 0x47536dff, 0x48546dff, 0x4a546dff, 0x4b556dff, 0x4c566dff, 0x4d576dff, 0x4e576eff, 0x50586eff, 0x51596eff, 0x52596eff, 0x535a6eff, 0x545b6eff, 0x565c6eff, 0x575c6eff, 0x585d6eff, 0x595e6eff, 0x5a5e6eff, 0x5b5f6eff, 0x5c606eff, 0x5d616eff, 0x5e616eff, 0x60626eff, 0x61636fff, 0x62646fff, 0x63646fff, 0x64656fff, 0x65666fff, 0x66666fff, 0x67676fff, 0x686870ff, 0x696970ff, 0x6a6970ff, 0x6b6a70ff, 0x6c6b70ff, 0x6d6c70ff, 0x6d6c71ff, 0x6e6d71ff, 0x6f6e71ff, 0x706f71ff, 0x716f71ff, 0x727071ff, 0x737172ff, 0x747172ff, 0x757272ff, 0x767372ff, 0x767472ff, 0x777473ff, 0x787573ff, 0x797673ff, 0x7a7773ff, 0x7b7774ff, 0x7b7874ff, 0x7c7974ff, 0x7d7a74ff, 0x7e7a74ff, 0x7f7b75ff, 0x807c75ff, 0x807d75ff, 0x817d75ff, 0x827e75ff, 0x837f76ff, 0x848076ff, 0x858076ff, 0x858176ff, 0x868276ff, 0x878376ff, 0x888477ff, 0x898477ff, 0x898577ff, 0x8a8677ff, 0x8b8777ff, 0x8c8777ff, 0x8d8877ff, 0x8e8978ff, 0x8e8a78ff, 0x8f8a78ff, 0x908b78ff, 0x918c78ff, 0x928d78ff, 0x938e78ff, 0x938e78ff, 0x948f78ff, 0x959078ff, 0x969178ff, 0x979278ff, 0x989278ff, 0x999378ff, 0x9a9478ff, 0x9b9578ff, 0x9b9678ff, 0x9c9678ff, 0x9d9778ff, 0x9e9878ff, 0x9f9978ff, 0xa09a78ff, 0xa19a78ff, 0xa29b78ff, 0xa39c78ff, 0xa49d78ff, 0xa59e77ff, 0xa69e77ff, 0xa79f77ff, 0xa8a077ff, 0xa9a177ff, 0xaaa276ff, 0xaba376ff, 0xaca376ff, 0xada476ff, 0xaea575ff, 0xafa675ff, 0xb0a775ff, 0xb2a874ff, 0xb3a874ff, 0xb4a974ff, 0xb5aa73ff, 0xb6ab73ff, 0xb7ac72ff, 0xb8ad72ff, 0xbaae72ff, 0xbbae71ff, 0xbcaf71ff, 0xbdb070ff, 0xbeb170ff, 0xbfb26fff, 0xc1b36fff, 0xc2b46eff, 0xc3b56dff, 0xc4b56dff, 0xc5b66cff, 0xc7b76cff, 0xc8b86bff, 0xc9b96aff, 0xcaba6aff, 0xccbb69ff, 0xcdbc68ff, 0xcebc68ff, 0xcfbd67ff, 0xd1be66ff, 0xd2bf66ff, 0xd3c065ff, 0xd4c164ff, 0xd6c263ff, 0xd7c363ff, 0xd8c462ff, 0xd9c561ff, 0xdbc660ff, 0xdcc660ff, 0xddc75fff, 0xdec85eff, 0xe0c95dff, 0xe1ca5cff, 0xe2cb5cff, 0xe3cc5bff, 0xe4cd5aff, 0xe6ce59ff, 0xe7cf58ff, 0xe8d058ff, 0xe9d157ff, 0xead256ff, 0xebd355ff, 0xecd454ff, 0xedd453ff, 0xeed553ff, 0xf0d652ff, 0xf1d751ff, 0xf1d850ff, 0xf2d950ff, 0xf3da4fff, 0xf4db4eff, 0xf5dc4dff, 0xf6dd4dff, 0xf7de4cff, 0xf8df4bff, 0xf8e04bff, 0xf9e14aff, 0xfae249ff, 0xfae349ff, 0xfbe448ff, 0xfbe548ff, 0xfce647ff, 0xfce746ff, 0xfde846ff, 0xfde946ff, 0xfdea45ff); +preset!(rainbow; 0x7c4bbbff, 0x7f4bbcff, 0x824bbdff, 0x854abeff, 0x884abeff, 0x8b4abfff, 0x8e49bfff, 0x9149c0ff, 0x9449c0ff, 0x9748c0ff, 0x9a48c1ff, 0x9e48c1ff, 0xa148c1ff, 0xa447c1ff, 0xa747c1ff, 0xaa47c0ff, 0xad47c0ff, 0xb046c0ff, 0xb446bfff, 0xb746bfff, 0xba46beff, 0xbd46beff, 0xc046bdff, 0xc346bcff, 0xc646bbff, 0xc946baff, 0xcc46b9ff, 0xcf46b8ff, 0xd246b7ff, 0xd446b5ff, 0xd747b4ff, 0xda47b3ff, 0xdd47b1ff, 0xdf47b0ff, 0xe248aeff, 0xe448acff, 0xe748abff, 0xe949a9ff, 0xec49a7ff, 0xee4aa5ff, 0xf04ba3ff, 0xf34ba1ff, 0xf54c9fff, 0xf74c9dff, 0xf94d9bff, 0xfb4e98ff, 0xfd4f96ff, 0xfe5094ff, 0xff5191ff, 0xff528fff, 0xff538dff, 0xff548aff, 0xff5588ff, 0xff5685ff, 0xff5783ff, 0xff5880ff, 0xff5a7eff, 0xff5b7bff, 0xff5c79ff, 0xff5e76ff, 0xff5f74ff, 0xff6171ff, 0xff626fff, 0xff646cff, 0xff666aff, 0xff6767ff, 0xff6965ff, 0xff6b63ff, 0xff6d60ff, 0xff6e5eff, 0xff705cff, 0xff7259ff, 0xff7457ff, 0xff7655ff, 0xff7853ff, 0xff7a51ff, 0xff7c4fff, 0xff7f4dff, 0xff814bff, 0xff8349ff, 0xff8547ff, 0xff8745ff, 0xff8a44ff, 0xff8c42ff, 0xff8e40ff, 0xff913fff, 0xff933eff, 0xff953cff, 0xff983bff, 0xfd9a3aff, 0xfb9c39ff, 0xfa9f38ff, 0xf8a137ff, 0xf6a436ff, 0xf4a636ff, 0xf2a935ff, 0xf0ab35ff, 0xeeae34ff, 0xecb034ff, 0xeab234ff, 0xe8b534ff, 0xe6b734ff, 0xe4ba34ff, 0xe1bc34ff, 0xdfbf35ff, 0xddc135ff, 0xdbc336ff, 0xd9c636ff, 0xd6c837ff, 0xd4ca38ff, 0xd2cd39ff, 0xd0cf3aff, 0xcdd13bff, 0xcbd33dff, 0xc9d63eff, 0xc7d840ff, 0xc5da41ff, 0xc3dc43ff, 0xc1de45ff, 0xbfe047ff, 0xbde249ff, 0xbbe44bff, 0xb9e64dff, 0xb7e84fff, 0xb5ea52ff, 0xb3ec54ff, 0xb2ed57ff, 0xb0ef59ff, 0xadf05aff, 0xaaf15aff, 0xa6f159ff, 0xa2f259ff, 0x9ff259ff, 0x9bf358ff, 0x97f358ff, 0x94f459ff, 0x90f459ff, 0x8df559ff, 0x89f559ff, 0x85f65aff, 0x82f65bff, 0x7ff65bff, 0x7ef75cff, 0x7cf75dff, 0x7bf75eff, 0x7af75fff, 0x79f760ff, 0x78f762ff, 0x77f763ff, 0x76f764ff, 0x75f766ff, 0x74f768ff, 0x73f769ff, 0x72f76bff, 0x71f76dff, 0x70f76fff, 0x6ff671ff, 0x6ef673ff, 0x6df675ff, 0x6df577ff, 0x6cf579ff, 0x6bf47cff, 0x6af37eff, 0x69f380ff, 0x68f283ff, 0x67f185ff, 0x66f188ff, 0x66f08aff, 0x65ef8dff, 0x64ee8fff, 0x63ed92ff, 0x62ec94ff, 0x62eb97ff, 0x61ea9aff, 0x60e89cff, 0x5fe79fff, 0x5fe6a1ff, 0x5ee4a4ff, 0x5de3a7ff, 0x5ce2a9ff, 0x5ce0acff, 0x5bdfafff, 0x5addb1ff, 0x5adbb4ff, 0x59dab6ff, 0x58d8b9ff, 0x58d6bbff, 0x57d5beff, 0x56d3c0ff, 0x56d1c2ff, 0x55cfc5ff, 0x54cdc7ff, 0x54cbc9ff, 0x53c9cbff, 0x52c7cdff, 0x52c5cfff, 0x51c3d1ff, 0x51c1d3ff, 0x50bfd5ff, 0x50bdd7ff, 0x4fbbd9ff, 0x4eb9daff, 0x4eb6dcff, 0x4db4ddff, 0x4db2dfff, 0x4cb0e0ff, 0x4caee2ff, 0x4babe3ff, 0x4ba9e4ff, 0x4aa7e5ff, 0x4aa4e6ff, 0x49a2e7ff, 0x49a0e8ff, 0x489ee8ff, 0x489be9ff, 0x4799e9ff, 0x4797eaff, 0x4694eaff, 0x4692eaff, 0x4690ebff, 0x458eebff, 0x478bebff, 0x4889ebff, 0x4a87eaff, 0x4c85eaff, 0x4e82eaff, 0x5080e9ff, 0x527ee9ff, 0x537ce8ff, 0x557ae7ff, 0x5778e7ff, 0x5975e6ff, 0x5b73e5ff, 0x5c71e4ff, 0x5e6fe3ff, 0x606de1ff, 0x626be0ff, 0x6369dfff, 0x6567ddff, 0x6765dcff, 0x6864daff, 0x6a62d9ff, 0x6b60d7ff, 0x6d5ed5ff, 0x6e5cd3ff, 0x705bd1ff, 0x7159cfff, 0x7357cdff, 0x7456cbff, 0x7554c9ff, 0x7652c7ff, 0x7751c5ff, 0x794fc2ff, 0x7a4ec0ff, 0x7b4dbeff, 0x7c4bbbff); +preset!(spectral; 0x9e0142ff, 0xd53e4fff, 0xf46d43ff, 0xfdae61ff, 0xfee08bff, 0xffffbfff, 0xe6f598ff, 0xabdda4ff, 0x66c2a5ff, 0x3288bdff, 0x5e4fa2ff); +preset!(viridis; 0x440154ff, 0x482777ff, 0x3f4a8aff, 0x31678eff, 0x26838fff, 0x1f9d8aff, 0x6cce5aff, 0xb6de2bff, 0xfee825ff); +preset!(inferno; 0x000004ff, 0x170b3aff, 0x420a68ff, 0x6b176eff, 0x932667ff, 0xbb3654ff, 0xdd513aff, 0xf3771aff, 0xfca50aff, 0xf6d644ff, 0xfcffa4ff); +preset!(magma; 0x000004ff, 0x140e37ff, 0x3b0f70ff, 0x641a80ff, 0x8c2981ff, 0xb63679ff, 0xde4968ff, 0xf66f5cff, 0xfe9f6dff, 0xfece91ff, 0xfcfdbfff); +preset!(plasma; 0x0d0887ff, 0x42039dff, 0x6a00a8ff, 0x900da3ff, 0xb12a90ff, 0xcb4678ff, 0xe16462ff, 0xf1834bff, 0xfca636ff, 0xfccd25ff, 0xf0f921ff); +preset!(rocket; 0x3051aff, 0x4051aff, 0x5061bff, 0x6071cff, 0x7071dff, 0x8081eff, 0xa091fff, 0xb0920ff, 0xd0a21ff, 0xe0b22ff, 0x100b23ff, 0x110c24ff, 0x130d25ff, 0x140e26ff, 0x160e27ff, 0x170f28ff, 0x180f29ff, 0x1a102aff, 0x1b112bff, 0x1d112cff, 0x1e122dff, 0x20122eff, 0x211330ff, 0x221331ff, 0x241432ff, 0x251433ff, 0x271534ff, 0x281535ff, 0x2a1636ff, 0x2b1637ff, 0x2d1738ff, 0x2e1739ff, 0x30173aff, 0x31183bff, 0x33183cff, 0x34193dff, 0x35193eff, 0x37193fff, 0x381a40ff, 0x3a1a41ff, 0x3c1a42ff, 0x3d1a42ff, 0x3f1b43ff, 0x401b44ff, 0x421b45ff, 0x431c46ff, 0x451c47ff, 0x461c48ff, 0x481c48ff, 0x491d49ff, 0x4b1d4aff, 0x4c1d4bff, 0x4e1d4bff, 0x501d4cff, 0x511e4dff, 0x531e4dff, 0x541e4eff, 0x561e4fff, 0x581e4fff, 0x591e50ff, 0x5b1e51ff, 0x5c1e51ff, 0x5e1f52ff, 0x601f52ff, 0x611f53ff, 0x631f53ff, 0x641f54ff, 0x661f54ff, 0x681f55ff, 0x691f55ff, 0x6b1f56ff, 0x6d1f56ff, 0x6e1f57ff, 0x701f57ff, 0x711f57ff, 0x731f58ff, 0x751f58ff, 0x761f58ff, 0x781f59ff, 0x7a1f59ff, 0x7b1f59ff, 0x7d1f5aff, 0x7f1e5aff, 0x811e5aff, 0x821e5aff, 0x841e5aff, 0x861e5bff, 0x871e5bff, 0x891e5bff, 0x8b1d5bff, 0x8c1d5bff, 0x8e1d5bff, 0x901d5bff, 0x921c5bff, 0x931c5bff, 0x951c5bff, 0x971c5bff, 0x981b5bff, 0x9a1b5bff, 0x9c1b5bff, 0x9e1a5bff, 0x9f1a5bff, 0xa11a5bff, 0xa3195bff, 0xa4195bff, 0xa6195aff, 0xa8185aff, 0xaa185aff, 0xab185aff, 0xad1759ff, 0xaf1759ff, 0xb01759ff, 0xb21758ff, 0xb41658ff, 0xb51657ff, 0xb71657ff, 0xb91657ff, 0xba1656ff, 0xbc1656ff, 0xbd1655ff, 0xbf1654ff, 0xc11754ff, 0xc21753ff, 0xc41753ff, 0xc51852ff, 0xc71951ff, 0xc81951ff, 0xca1a50ff, 0xcb1b4fff, 0xcd1c4eff, 0xce1d4eff, 0xcf1e4dff, 0xd11f4cff, 0xd2204cff, 0xd3214bff, 0xd5224aff, 0xd62449ff, 0xd72549ff, 0xd82748ff, 0xd92847ff, 0xdb2946ff, 0xdc2b46ff, 0xdd2c45ff, 0xde2e44ff, 0xdf2f44ff, 0xe03143ff, 0xe13342ff, 0xe23442ff, 0xe33641ff, 0xe43841ff, 0xe53940ff, 0xe63b40ff, 0xe73d3fff, 0xe83f3fff, 0xe8403eff, 0xe9423eff, 0xea443eff, 0xeb463eff, 0xeb483eff, 0xec4a3eff, 0xec4c3eff, 0xed4e3eff, 0xed503eff, 0xee523fff, 0xee543fff, 0xef5640ff, 0xef5840ff, 0xef5a41ff, 0xf05c42ff, 0xf05e42ff, 0xf06043ff, 0xf16244ff, 0xf16445ff, 0xf16646ff, 0xf26747ff, 0xf26948ff, 0xf26b49ff, 0xf26d4bff, 0xf26f4cff, 0xf3714dff, 0xf3734eff, 0xf37450ff, 0xf37651ff, 0xf37852ff, 0xf47a54ff, 0xf47c55ff, 0xf47d57ff, 0xf47f58ff, 0xf4815aff, 0xf4835bff, 0xf4845dff, 0xf4865eff, 0xf58860ff, 0xf58a61ff, 0xf58b63ff, 0xf58d64ff, 0xf58f66ff, 0xf59067ff, 0xf59269ff, 0xf5946bff, 0xf5966cff, 0xf5976eff, 0xf59970ff, 0xf69b71ff, 0xf69c73ff, 0xf69e75ff, 0xf6a077ff, 0xf6a178ff, 0xf6a37aff, 0xf6a47cff, 0xf6a67eff, 0xf6a880ff, 0xf6a981ff, 0xf6ab83ff, 0xf6ad85ff, 0xf6ae87ff, 0xf6b089ff, 0xf6b18bff, 0xf6b38dff, 0xf6b48fff, 0xf6b691ff, 0xf6b893ff, 0xf6b995ff, 0xf6bb97ff, 0xf6bc99ff, 0xf6be9bff, 0xf6bf9dff, 0xf6c19fff, 0xf7c2a2ff, 0xf7c4a4ff, 0xf7c6a6ff, 0xf7c7a8ff, 0xf7c9aaff, 0xf7caacff, 0xf7ccafff, 0xf7cdb1ff, 0xf7cfb3ff, 0xf7d0b5ff, 0xf8d1b8ff, 0xf8d3baff, 0xf8d4bcff, 0xf8d6beff, 0xf8d7c0ff, 0xf8d9c3ff, 0xf8dac5ff, 0xf8dcc7ff, 0xf9ddc9ff, 0xf9dfcbff, 0xf9e0cdff, 0xf9e2d0ff, 0xf9e3d2ff, 0xf9e5d4ff, 0xfae6d6ff, 0xfae8d8ff, 0xfae9daff, 0xfaebddff); +preset!(mako; 0xb0405ff, 0xd0406ff, 0xe0508ff, 0xf0609ff, 0x10060aff, 0x11070cff, 0x12080dff, 0x13090fff, 0x140910ff, 0x150a12ff, 0x160b13ff, 0x170c15ff, 0x180d16ff, 0x190e18ff, 0x1a0e19ff, 0x1b0f1aff, 0x1c101cff, 0x1d111dff, 0x1e111fff, 0x1f1220ff, 0x201322ff, 0x211423ff, 0x221425ff, 0x231526ff, 0x241628ff, 0x251729ff, 0x26172bff, 0x27182dff, 0x28192eff, 0x291930ff, 0x291a31ff, 0x2a1b33ff, 0x2b1c35ff, 0x2c1c36ff, 0x2d1d38ff, 0x2e1e39ff, 0x2e1e3bff, 0x2f1f3dff, 0x30203eff, 0x312140ff, 0x312142ff, 0x322243ff, 0x332345ff, 0x342447ff, 0x342548ff, 0x35254aff, 0x35264cff, 0x36274dff, 0x37284fff, 0x372851ff, 0x382953ff, 0x382a54ff, 0x392b56ff, 0x3a2c58ff, 0x3a2c59ff, 0x3b2d5bff, 0x3b2e5dff, 0x3b2f5fff, 0x3c3060ff, 0x3c3162ff, 0x3d3164ff, 0x3d3266ff, 0x3e3367ff, 0x3e3469ff, 0x3e356bff, 0x3f366dff, 0x3f366fff, 0x3f3770ff, 0x403872ff, 0x403974ff, 0x403a76ff, 0x403b78ff, 0x403c79ff, 0x413d7bff, 0x413e7dff, 0x413e7fff, 0x413f80ff, 0x414082ff, 0x414184ff, 0x414285ff, 0x414387ff, 0x414488ff, 0x40468aff, 0x40478bff, 0x40488dff, 0x40498eff, 0x3f4a8fff, 0x3f4b90ff, 0x3f4c92ff, 0x3e4d93ff, 0x3e4f94ff, 0x3e5095ff, 0x3d5195ff, 0x3d5296ff, 0x3c5397ff, 0x3c5598ff, 0x3b5698ff, 0x3b5799ff, 0x3b589aff, 0x3a599aff, 0x3a5b9bff, 0x3a5c9bff, 0x395d9cff, 0x395e9cff, 0x385f9cff, 0x38619dff, 0x38629dff, 0x38639dff, 0x37649eff, 0x37659eff, 0x37669eff, 0x37689fff, 0x36699fff, 0x366a9fff, 0x366b9fff, 0x366ca0ff, 0x366da0ff, 0x366fa0ff, 0x3670a0ff, 0x3671a0ff, 0x3572a1ff, 0x3573a1ff, 0x3574a1ff, 0x3575a1ff, 0x3576a2ff, 0x3578a2ff, 0x3579a2ff, 0x357aa2ff, 0x357ba3ff, 0x357ca3ff, 0x357da3ff, 0x357ea4ff, 0x347fa4ff, 0x3480a4ff, 0x3482a4ff, 0x3483a5ff, 0x3484a5ff, 0x3485a5ff, 0x3486a5ff, 0x3487a6ff, 0x3488a6ff, 0x3489a6ff, 0x348ba6ff, 0x348ca7ff, 0x348da7ff, 0x348ea7ff, 0x348fa7ff, 0x3490a8ff, 0x3491a8ff, 0x3492a8ff, 0x3493a8ff, 0x3495a9ff, 0x3496a9ff, 0x3497a9ff, 0x3498a9ff, 0x3499aaff, 0x349aaaff, 0x359baaff, 0x359caaff, 0x359eaaff, 0x359fabff, 0x35a0abff, 0x35a1abff, 0x36a2abff, 0x36a3abff, 0x36a4abff, 0x37a5acff, 0x37a6acff, 0x37a8acff, 0x38a9acff, 0x38aaacff, 0x39abacff, 0x39acacff, 0x3aadacff, 0x3aaeadff, 0x3bafadff, 0x3cb1adff, 0x3cb2adff, 0x3db3adff, 0x3eb4adff, 0x3fb5adff, 0x3fb6adff, 0x40b7adff, 0x41b8adff, 0x42b9adff, 0x43baadff, 0x44bcadff, 0x45bdadff, 0x46beadff, 0x47bfadff, 0x48c0adff, 0x49c1adff, 0x4bc2adff, 0x4cc3adff, 0x4dc4adff, 0x4fc5adff, 0x50c6adff, 0x52c7adff, 0x53c9adff, 0x55caadff, 0x57cbadff, 0x59ccadff, 0x5bcdadff, 0x5ecdadff, 0x60ceacff, 0x62cfacff, 0x65d0adff, 0x68d1adff, 0x6ad2adff, 0x6dd3adff, 0x70d4adff, 0x73d4adff, 0x76d5aeff, 0x79d6aeff, 0x7cd6afff, 0x7fd7afff, 0x82d8b0ff, 0x85d9b1ff, 0x88d9b1ff, 0x8bdab2ff, 0x8edbb3ff, 0x91dbb4ff, 0x94dcb5ff, 0x96ddb5ff, 0x99ddb6ff, 0x9cdeb7ff, 0x9edfb8ff, 0xa1dfb9ff, 0xa4e0bbff, 0xa6e1bcff, 0xa9e1bdff, 0xabe2beff, 0xaee3c0ff, 0xb0e4c1ff, 0xb2e4c2ff, 0xb5e5c4ff, 0xb7e6c5ff, 0xb9e6c7ff, 0xbbe7c8ff, 0xbee8caff, 0xc0e9ccff, 0xc2e9cdff, 0xc4eacfff, 0xc6ebd1ff, 0xc8ecd2ff, 0xcaedd4ff, 0xccedd6ff, 0xceeed7ff, 0xd0efd9ff, 0xd2f0dbff, 0xd4f1dcff, 0xd6f1deff, 0xd8f2e0ff, 0xdaf3e1ff, 0xdcf4e3ff, 0xdef5e5ff); +preset!(vlag; 0x2369bdff, 0x266abdff, 0x296cbcff, 0x2c6dbcff, 0x2f6ebcff, 0x316fbcff, 0x3470bcff, 0x3671bcff, 0x3972bcff, 0x3b73bcff, 0x3d74bcff, 0x3f75bcff, 0x4276bcff, 0x4477bcff, 0x4678bcff, 0x4879bcff, 0x4a7bbcff, 0x4c7cbcff, 0x4e7dbcff, 0x507ebcff, 0x517fbcff, 0x5380bcff, 0x5581bcff, 0x5782bcff, 0x5983bdff, 0x5b84bdff, 0x5c85bdff, 0x5e86bdff, 0x6087bdff, 0x6288bdff, 0x6489beff, 0x658abeff, 0x678bbeff, 0x698cbeff, 0x6a8dbfff, 0x6c8ebfff, 0x6e90bfff, 0x6f91bfff, 0x7192c0ff, 0x7393c0ff, 0x7594c0ff, 0x7695c1ff, 0x7896c1ff, 0x7997c1ff, 0x7b98c2ff, 0x7d99c2ff, 0x7e9ac2ff, 0x809bc3ff, 0x829cc3ff, 0x839dc4ff, 0x859ec4ff, 0x87a0c4ff, 0x88a1c5ff, 0x8aa2c5ff, 0x8ba3c6ff, 0x8da4c6ff, 0x8fa5c7ff, 0x90a6c7ff, 0x92a7c8ff, 0x93a8c8ff, 0x95a9c8ff, 0x97abc9ff, 0x98acc9ff, 0x9aadcaff, 0x9baecbff, 0x9dafcbff, 0x9fb0ccff, 0xa0b1ccff, 0xa2b2cdff, 0xa3b4cdff, 0xa5b5ceff, 0xa7b6ceff, 0xa8b7cfff, 0xaab8d0ff, 0xabb9d0ff, 0xadbbd1ff, 0xafbcd1ff, 0xb0bdd2ff, 0xb2bed3ff, 0xb3bfd3ff, 0xb5c0d4ff, 0xb7c2d5ff, 0xb8c3d5ff, 0xbac4d6ff, 0xbbc5d7ff, 0xbdc6d7ff, 0xbfc8d8ff, 0xc0c9d9ff, 0xc2cadaff, 0xc3cbdaff, 0xc5cddbff, 0xc7cedcff, 0xc8cfddff, 0xcad0ddff, 0xcbd1deff, 0xcdd3dfff, 0xcfd4e0ff, 0xd0d5e0ff, 0xd2d7e1ff, 0xd4d8e2ff, 0xd5d9e3ff, 0xd7dae4ff, 0xd9dce5ff, 0xdadde5ff, 0xdcdee6ff, 0xdde0e7ff, 0xdfe1e8ff, 0xe1e2e9ff, 0xe2e3eaff, 0xe4e5ebff, 0xe6e6ecff, 0xe7e7ecff, 0xe9e9edff, 0xebeaeeff, 0xecebefff, 0xeeedf0ff, 0xefeef1ff, 0xf1eff2ff, 0xf2f0f2ff, 0xf3f1f3ff, 0xf5f2f4ff, 0xf6f3f4ff, 0xf7f4f4ff, 0xf8f4f5ff, 0xf9f5f5ff, 0xf9f5f5ff, 0xfaf5f5ff, 0xfaf5f5ff, 0xfaf5f4ff, 0xfaf5f4ff, 0xfaf4f3ff, 0xfaf3f3ff, 0xfaf3f2ff, 0xfaf2f1ff, 0xfaf0efff, 0xf9efeeff, 0xf9eeedff, 0xf8edebff, 0xf7ebeaff, 0xf7eae8ff, 0xf6e8e7ff, 0xf5e7e5ff, 0xf5e5e4ff, 0xf4e3e2ff, 0xf3e2e0ff, 0xf2e0dfff, 0xf2dfddff, 0xf1dddbff, 0xf0dbdaff, 0xefdad8ff, 0xefd8d6ff, 0xeed7d5ff, 0xedd5d3ff, 0xecd3d2ff, 0xecd2d0ff, 0xebd0ceff, 0xeacfcdff, 0xeacdcbff, 0xe9cbc9ff, 0xe8cac8ff, 0xe7c8c6ff, 0xe7c7c5ff, 0xe6c5c3ff, 0xe5c3c1ff, 0xe5c2c0ff, 0xe4c0beff, 0xe3bfbdff, 0xe3bdbbff, 0xe2bcb9ff, 0xe1bab8ff, 0xe1b9b6ff, 0xe0b7b5ff, 0xdfb5b3ff, 0xdfb4b2ff, 0xdeb2b0ff, 0xdeb1aeff, 0xddafadff, 0xdcaeabff, 0xdcacaaff, 0xdbaba8ff, 0xdaa9a7ff, 0xdaa8a5ff, 0xd9a6a4ff, 0xd9a5a2ff, 0xd8a3a0ff, 0xd7a29fff, 0xd7a09dff, 0xd69f9cff, 0xd59d9aff, 0xd59c99ff, 0xd49a97ff, 0xd49896ff, 0xd39794ff, 0xd29593ff, 0xd29491ff, 0xd19290ff, 0xd1918eff, 0xd08f8dff, 0xcf8e8bff, 0xcf8c8aff, 0xce8b88ff, 0xcd8987ff, 0xcd8885ff, 0xcc8784ff, 0xcc8582ff, 0xcb8481ff, 0xca827fff, 0xca817eff, 0xc97f7dff, 0xc87e7bff, 0xc87c7aff, 0xc77b78ff, 0xc77977ff, 0xc67875ff, 0xc57674ff, 0xc57572ff, 0xc47371ff, 0xc3726fff, 0xc3706eff, 0xc26f6dff, 0xc16d6bff, 0xc16c6aff, 0xc06a68ff, 0xc06967ff, 0xbf6765ff, 0xbe6664ff, 0xbe6463ff, 0xbd6361ff, 0xbc6160ff, 0xbc605eff, 0xbb5e5dff, 0xba5d5cff, 0xb95b5aff, 0xb95a59ff, 0xb85857ff, 0xb75756ff, 0xb75555ff, 0xb65453ff, 0xb55252ff, 0xb55151ff, 0xb44f4fff, 0xb34d4eff, 0xb24c4cff, 0xb24a4bff, 0xb1494aff, 0xb04748ff, 0xaf4647ff, 0xaf4446ff, 0xae4244ff, 0xad4143ff, 0xac3f42ff, 0xac3e40ff, 0xab3c3fff, 0xaa3a3eff, 0xa9393cff, 0xa9373bff); +preset!(icefire; 0xbde7dbff, 0xbae5daff, 0xb7e3d9ff, 0xb4e1d9ff, 0xb2dfd8ff, 0xafddd7ff, 0xacdbd7ff, 0xa9d9d6ff, 0xa7d7d5ff, 0xa4d5d5ff, 0xa1d3d4ff, 0x9ed1d3ff, 0x9bcfd3ff, 0x98cdd2ff, 0x95cbd2ff, 0x93cad1ff, 0x90c8d1ff, 0x8dc6d0ff, 0x8ac4d0ff, 0x87c2cfff, 0x84c1cfff, 0x81bfcfff, 0x7ebdceff, 0x7bbbceff, 0x78b9ceff, 0x75b8ceff, 0x72b6ceff, 0x6eb4cdff, 0x6bb2cdff, 0x68b0cdff, 0x65afcdff, 0x63adcdff, 0x60abcdff, 0x5da9cdff, 0x5aa7cdff, 0x58a5cdff, 0x55a3cdff, 0x53a2cdff, 0x50a0cdff, 0x4e9ecdff, 0x4c9ccdff, 0x499aceff, 0x4798ceff, 0x4596ceff, 0x4394ceff, 0x4192ceff, 0x3f90ceff, 0x3e8ecfff, 0x3c8ccfff, 0x3a89cfff, 0x3987cfff, 0x3885d0ff, 0x3783d0ff, 0x3781d0ff, 0x377fd0ff, 0x377cd0ff, 0x377ad0ff, 0x3878cfff, 0x3975cfff, 0x3a73ceff, 0x3b71cdff, 0x3d6eccff, 0x3e6ccbff, 0x3f69c9ff, 0x4167c7ff, 0x4265c5ff, 0x4363c3ff, 0x4560c1ff, 0x465ebeff, 0x475cbcff, 0x475ab9ff, 0x4858b6ff, 0x4956b3ff, 0x4954b0ff, 0x4952adff, 0x4a50a9ff, 0x4a4fa5ff, 0x494da1ff, 0x494c9eff, 0x494a9aff, 0x484996ff, 0x474792ff, 0x47468eff, 0x46458aff, 0x454386ff, 0x444282ff, 0x43417fff, 0x42407bff, 0x413e77ff, 0x3f3d74ff, 0x3e3c70ff, 0x3d3b6dff, 0x3c3a69ff, 0x3b3866ff, 0x393763ff, 0x38365fff, 0x37355cff, 0x363459ff, 0x343356ff, 0x333153ff, 0x323050ff, 0x312f4dff, 0x302e4aff, 0x2e2d48ff, 0x2d2c45ff, 0x2c2b42ff, 0x2b2a40ff, 0x2a293dff, 0x29283bff, 0x282739ff, 0x272636ff, 0x262534ff, 0x252532ff, 0x242430ff, 0x24232eff, 0x23222dff, 0x22222bff, 0x222129ff, 0x212028ff, 0x212026ff, 0x202025ff, 0x201f24ff, 0x1f1f23ff, 0x1f1f21ff, 0x1f1e21ff, 0x1f1e20ff, 0x1f1e1fff, 0x1f1e1eff, 0x1f1e1eff, 0x201e1eff, 0x211e1eff, 0x221e1eff, 0x231e1eff, 0x251e1fff, 0x261e1fff, 0x271e1fff, 0x291e20ff, 0x2a1e20ff, 0x2c1e21ff, 0x2d1f21ff, 0x2f1f22ff, 0x311f23ff, 0x332023ff, 0x352024ff, 0x372025ff, 0x392126ff, 0x3b2127ff, 0x3d2228ff, 0x3f2228ff, 0x412329ff, 0x43232aff, 0x46242bff, 0x48242cff, 0x4a252eff, 0x4d252fff, 0x4f2630ff, 0x522731ff, 0x542732ff, 0x572833ff, 0x5a2834ff, 0x5c2935ff, 0x5f2936ff, 0x622937ff, 0x642a38ff, 0x672a39ff, 0x6a2b3aff, 0x6d2b3bff, 0x702b3cff, 0x722c3dff, 0x752c3eff, 0x782c3fff, 0x7b2d40ff, 0x7e2d40ff, 0x812d41ff, 0x842d42ff, 0x872d42ff, 0x8a2e43ff, 0x8d2e43ff, 0x902e44ff, 0x932e44ff, 0x962e44ff, 0x992e44ff, 0x9c2f45ff, 0x9f2f44ff, 0xa22f44ff, 0xa52f44ff, 0xa83044ff, 0xab3043ff, 0xae3143ff, 0xb13242ff, 0xb33341ff, 0xb63441ff, 0xb93540ff, 0xbb363fff, 0xbe373eff, 0xc0393dff, 0xc33a3cff, 0xc53c3cff, 0xc73d3bff, 0xc93f3aff, 0xcc4139ff, 0xce4338ff, 0xd04537ff, 0xd24737ff, 0xd34936ff, 0xd54b35ff, 0xd74e35ff, 0xd95034ff, 0xda5334ff, 0xdc5534ff, 0xde5733ff, 0xdf5a33ff, 0xe15c33ff, 0xe25f33ff, 0xe36233ff, 0xe56433ff, 0xe66734ff, 0xe76a34ff, 0xe86d35ff, 0xe96f36ff, 0xea7238ff, 0xeb753aff, 0xec783bff, 0xed7b3eff, 0xed7e40ff, 0xee8142ff, 0xef8445ff, 0xef8748ff, 0xf0894bff, 0xf18c4eff, 0xf18f51ff, 0xf29255ff, 0xf29558ff, 0xf3985bff, 0xf39a5fff, 0xf49d63ff, 0xf5a066ff, 0xf5a36aff, 0xf6a56dff, 0xf6a871ff, 0xf7ab75ff, 0xf7ae79ff, 0xf8b07cff, 0xf8b380ff, 0xf9b684ff, 0xfab887ff, 0xfabb8bff, 0xfbbe8fff, 0xfbc192ff, 0xfcc396ff, 0xfcc69aff, 0xfdc99eff, 0xfdcca1ff, 0xfecea5ff, 0xfed1a9ff, 0xffd4acff); +preset!(flare; 0xedb081ff, 0xedaf80ff, 0xedae7fff, 0xedad7fff, 0xedac7eff, 0xedab7eff, 0xecaa7dff, 0xeca97cff, 0xeca87cff, 0xeca77bff, 0xeca67bff, 0xeca57aff, 0xeca479ff, 0xeca379ff, 0xeca278ff, 0xeca178ff, 0xeca077ff, 0xec9f76ff, 0xeb9e76ff, 0xeb9d75ff, 0xeb9c75ff, 0xeb9b74ff, 0xeb9a73ff, 0xeb9973ff, 0xeb9972ff, 0xeb9872ff, 0xeb9771ff, 0xea9671ff, 0xea9570ff, 0xea946fff, 0xea936fff, 0xea926eff, 0xea916eff, 0xea906dff, 0xea8f6cff, 0xea8e6cff, 0xe98d6bff, 0xe98c6bff, 0xe98b6aff, 0xe98a6aff, 0xe98969ff, 0xe98868ff, 0xe98768ff, 0xe98667ff, 0xe88567ff, 0xe88466ff, 0xe88366ff, 0xe88265ff, 0xe88165ff, 0xe88064ff, 0xe87f64ff, 0xe77e63ff, 0xe77d63ff, 0xe77c63ff, 0xe77b62ff, 0xe77a62ff, 0xe67961ff, 0xe67861ff, 0xe67760ff, 0xe67660ff, 0xe67560ff, 0xe5745fff, 0xe5735fff, 0xe5725fff, 0xe5715eff, 0xe5705eff, 0xe46f5eff, 0xe46e5eff, 0xe46d5dff, 0xe46c5dff, 0xe36b5dff, 0xe36a5dff, 0xe3695dff, 0xe3685cff, 0xe2675cff, 0xe2665cff, 0xe2655cff, 0xe1645cff, 0xe1635cff, 0xe1625cff, 0xe0615cff, 0xe0605cff, 0xe05f5cff, 0xdf5f5cff, 0xdf5e5cff, 0xde5d5cff, 0xde5c5cff, 0xde5b5cff, 0xdd5a5cff, 0xdd595cff, 0xdc585cff, 0xdc575cff, 0xdb565dff, 0xdb565dff, 0xda555dff, 0xda545dff, 0xd9535dff, 0xd9525eff, 0xd8525eff, 0xd7515eff, 0xd7505eff, 0xd64f5fff, 0xd64f5fff, 0xd54e5fff, 0xd44d60ff, 0xd44c60ff, 0xd34c60ff, 0xd24b60ff, 0xd24a61ff, 0xd14a61ff, 0xd04962ff, 0xd04962ff, 0xcf4862ff, 0xce4763ff, 0xcd4763ff, 0xcc4663ff, 0xcc4664ff, 0xcb4564ff, 0xca4564ff, 0xc94465ff, 0xc84465ff, 0xc84365ff, 0xc74366ff, 0xc64366ff, 0xc54266ff, 0xc44267ff, 0xc34167ff, 0xc24167ff, 0xc14168ff, 0xc14068ff, 0xc04068ff, 0xbf4069ff, 0xbe3f69ff, 0xbd3f69ff, 0xbc3f69ff, 0xbb3f6aff, 0xba3e6aff, 0xb93e6aff, 0xb83e6bff, 0xb73d6bff, 0xb63d6bff, 0xb53d6bff, 0xb43d6bff, 0xb33c6cff, 0xb23c6cff, 0xb13c6cff, 0xb13c6cff, 0xb03b6dff, 0xaf3b6dff, 0xae3b6dff, 0xad3b6dff, 0xac3a6dff, 0xab3a6dff, 0xaa3a6eff, 0xa93a6eff, 0xa8396eff, 0xa7396eff, 0xa6396eff, 0xa5396eff, 0xa4386fff, 0xa3386fff, 0xa2386fff, 0xa1386fff, 0xa1376fff, 0xa0376fff, 0x9f376fff, 0x9e3770ff, 0x9d3670ff, 0x9c3670ff, 0x9b3670ff, 0x9a3670ff, 0x993570ff, 0x983570ff, 0x973570ff, 0x963570ff, 0x953470ff, 0x943470ff, 0x943471ff, 0x933471ff, 0x923371ff, 0x913371ff, 0x903371ff, 0x8f3371ff, 0x8e3271ff, 0x8d3271ff, 0x8c3271ff, 0x8b3271ff, 0x8a3171ff, 0x893171ff, 0x883171ff, 0x873171ff, 0x873171ff, 0x863071ff, 0x853071ff, 0x843071ff, 0x833070ff, 0x822f70ff, 0x812f70ff, 0x802f70ff, 0x7f2f70ff, 0x7e2f70ff, 0x7d2e70ff, 0x7c2e70ff, 0x7b2e70ff, 0x7a2e70ff, 0x792e6fff, 0x782e6fff, 0x772d6fff, 0x762d6fff, 0x752d6fff, 0x752d6fff, 0x742d6eff, 0x732c6eff, 0x722c6eff, 0x712c6eff, 0x702c6eff, 0x6f2c6dff, 0x6e2c6dff, 0x6d2b6dff, 0x6c2b6dff, 0x6b2b6cff, 0x6a2b6cff, 0x692b6cff, 0x682a6cff, 0x672a6bff, 0x662a6bff, 0x652a6bff, 0x642a6aff, 0x642a6aff, 0x63296aff, 0x62296aff, 0x612969ff, 0x602969ff, 0x5f2969ff, 0x5e2868ff, 0x5d2868ff, 0x5c2868ff, 0x5b2867ff, 0x5a2767ff, 0x592767ff, 0x582766ff, 0x582766ff, 0x572766ff, 0x562666ff, 0x552665ff, 0x542665ff, 0x532665ff, 0x522564ff, 0x512564ff, 0x502564ff, 0x4f2463ff, 0x4f2463ff, 0x4e2463ff, 0x4d2463ff, 0x4c2362ff, 0x4b2362ff); +preset!(crest; 0xa5cd90ff, 0xa4cc90ff, 0xa3cc91ff, 0xa2cb91ff, 0xa0cb91ff, 0x9fca91ff, 0x9eca91ff, 0x9dc991ff, 0x9cc891ff, 0x9bc891ff, 0x9ac791ff, 0x99c791ff, 0x98c691ff, 0x96c691ff, 0x95c591ff, 0x94c591ff, 0x93c491ff, 0x92c491ff, 0x91c391ff, 0x90c391ff, 0x8fc291ff, 0x8ec291ff, 0x8dc191ff, 0x8bc191ff, 0x8ac091ff, 0x89bf91ff, 0x88bf91ff, 0x87be91ff, 0x86be91ff, 0x85bd91ff, 0x84bd91ff, 0x82bc91ff, 0x81bc91ff, 0x80bb91ff, 0x7fbb91ff, 0x7eba91ff, 0x7dba91ff, 0x7cb991ff, 0x7bb991ff, 0x79b891ff, 0x78b891ff, 0x77b791ff, 0x76b791ff, 0x75b690ff, 0x74b690ff, 0x73b590ff, 0x72b490ff, 0x71b490ff, 0x70b390ff, 0x6fb390ff, 0x6eb290ff, 0x6db290ff, 0x6cb190ff, 0x6bb190ff, 0x6ab090ff, 0x69b090ff, 0x68af90ff, 0x67ae90ff, 0x66ae90ff, 0x65ad90ff, 0x64ad90ff, 0x63ac90ff, 0x62ac90ff, 0x62ab90ff, 0x61aa90ff, 0x60aa90ff, 0x5fa990ff, 0x5ea990ff, 0x5da890ff, 0x5ca890ff, 0x5ba790ff, 0x5ba690ff, 0x5aa690ff, 0x59a590ff, 0x58a590ff, 0x57a490ff, 0x57a490ff, 0x56a390ff, 0x55a290ff, 0x54a290ff, 0x53a190ff, 0x53a190ff, 0x52a090ff, 0x519f90ff, 0x509f90ff, 0x509e90ff, 0x4f9e90ff, 0x4e9d90ff, 0x4e9d90ff, 0x4d9c90ff, 0x4c9b90ff, 0x4b9b90ff, 0x4b9a8fff, 0x4a9a8fff, 0x49998fff, 0x49988fff, 0x48988fff, 0x47978fff, 0x47978fff, 0x46968fff, 0x45958fff, 0x45958fff, 0x44948fff, 0x43948fff, 0x43938fff, 0x42928fff, 0x41928fff, 0x41918fff, 0x40918fff, 0x40908eff, 0x3f8f8eff, 0x3e8f8eff, 0x3e8e8eff, 0x3d8e8eff, 0x3c8d8eff, 0x3c8c8eff, 0x3b8c8eff, 0x3a8b8eff, 0x3a8b8eff, 0x398a8eff, 0x388a8eff, 0x38898eff, 0x37888eff, 0x37888dff, 0x36878dff, 0x35878dff, 0x35868dff, 0x34858dff, 0x33858dff, 0x33848dff, 0x32848dff, 0x31838dff, 0x31828dff, 0x30828dff, 0x2f818dff, 0x2f818dff, 0x2e808dff, 0x2d808cff, 0x2d7f8cff, 0x2c7e8cff, 0x2c7e8cff, 0x2b7d8cff, 0x2a7d8cff, 0x2a7c8cff, 0x297b8cff, 0x287b8cff, 0x287a8cff, 0x277a8cff, 0x27798cff, 0x26788cff, 0x25788cff, 0x25778cff, 0x24778bff, 0x24768bff, 0x23758bff, 0x23758bff, 0x22748bff, 0x22748bff, 0x21738bff, 0x21728bff, 0x20728bff, 0x20718bff, 0x20718bff, 0x1f708bff, 0x1f6f8aff, 0x1e6f8aff, 0x1e6e8aff, 0x1e6d8aff, 0x1e6d8aff, 0x1d6c8aff, 0x1d6c8aff, 0x1d6b8aff, 0x1d6a8aff, 0x1d6a8aff, 0x1c6989ff, 0x1c6889ff, 0x1c6889ff, 0x1c6789ff, 0x1c6689ff, 0x1c6689ff, 0x1c6589ff, 0x1c6488ff, 0x1c6488ff, 0x1c6388ff, 0x1d6388ff, 0x1d6288ff, 0x1d6188ff, 0x1d6187ff, 0x1d6087ff, 0x1d5f87ff, 0x1d5f87ff, 0x1e5e87ff, 0x1e5d86ff, 0x1e5d86ff, 0x1e5c86ff, 0x1e5b86ff, 0x1f5b86ff, 0x1f5a85ff, 0x1f5985ff, 0x1f5985ff, 0x205885ff, 0x205784ff, 0x205784ff, 0x205684ff, 0x215584ff, 0x215583ff, 0x215483ff, 0x225383ff, 0x225283ff, 0x225282ff, 0x225182ff, 0x235082ff, 0x235081ff, 0x234f81ff, 0x244e81ff, 0x244e80ff, 0x244d80ff, 0x254c80ff, 0x254c7fff, 0x254b7fff, 0x254a7fff, 0x26497eff, 0x26497eff, 0x26487eff, 0x27477dff, 0x27477dff, 0x27467cff, 0x27457cff, 0x28457cff, 0x28447bff, 0x28437bff, 0x28427aff, 0x29427aff, 0x29417aff, 0x294079ff, 0x294079ff, 0x2a3f78ff, 0x2a3e78ff, 0x2a3d78ff, 0x2a3d77ff, 0x2a3c77ff, 0x2a3b76ff, 0x2b3b76ff, 0x2b3a76ff, 0x2b3975ff, 0x2b3875ff, 0x2b3875ff, 0x2b3774ff, 0x2b3674ff, 0x2c3574ff, 0x2c3573ff, 0x2c3473ff, 0x2c3373ff, 0x2c3272ff, 0x2c3172ff, 0x2c3172ff); + +#[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!(Color::from_str(hex), Ok(Color::from_u8(r, g, b, a))); + } + + test("f61243ff", 0xf6, 0x12, 0x43, 255); + test("b3d8b3", 0xb3, 0xd8, 0xb3, 255); + test("fCd2a9AD", 0xfc, 0xd2, 0xa9, 0xad); + test("233", 0x22, 0x33, 0x33, 255); + test("111b", 0x11, 0x11, 0x11, 0xbb); + } + + #[test] + fn test_parse_invalid_colors() { + #[track_caller] + fn test(hex: &str, message: &str) { + assert_eq!(Color::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/crates/typst-library/src/visualize/gradient.rs b/crates/typst-library/src/visualize/gradient.rs new file mode 100644 index 00000000..2be7e370 --- /dev/null +++ b/crates/typst-library/src/visualize/gradient.rs @@ -0,0 +1,1260 @@ +use std::f64::consts::{FRAC_PI_2, PI, TAU}; +use std::fmt::{self, Debug, Formatter}; +use std::hash::Hash; +use std::sync::Arc; + +use ecow::EcoString; +use kurbo::Vec2; +use typst_syntax::{Span, Spanned}; + +use crate::diag::{bail, SourceResult}; +use crate::foundations::{ + array, cast, func, scope, ty, Args, Array, Cast, Func, IntoValue, Repr, Smart, +}; +use crate::layout::{Angle, Axes, Dir, Quadrant, Ratio}; +use crate::visualize::{Color, ColorSpace, WeightedColor}; + +/// A color gradient. +/// +/// Typst supports linear gradients through the +/// [`gradient.linear` function]($gradient.linear), radial gradients through +/// the [`gradient.radial` function]($gradient.radial), and conic gradients +/// through the [`gradient.conic` function]($gradient.conic). +/// +/// A gradient can be used for the following purposes: +/// - As a fill to paint the interior of a shape: +/// `{rect(fill: gradient.linear(..))}` +/// - As a stroke to paint the outline of a shape: +/// `{rect(stroke: 1pt + gradient.linear(..))}` +/// - As the fill of text: +/// `{set text(fill: gradient.linear(..))}` +/// - As a color map you can [sample]($gradient.sample) from: +/// `{gradient.linear(..).sample(50%)}` +/// +/// # Examples +/// ```example +/// >>> #set square(size: 50pt) +/// #stack( +/// dir: ltr, +/// spacing: 1fr, +/// square(fill: gradient.linear(..color.map.rainbow)), +/// square(fill: gradient.radial(..color.map.rainbow)), +/// square(fill: gradient.conic(..color.map.rainbow)), +/// ) +/// ``` +/// +/// Gradients are also supported on text, but only when setting the +/// [relativeness]($gradient.relative) to either `{auto}` (the default value) or +/// `{"parent"}`. To create word-by-word or glyph-by-glyph gradients, you can +/// wrap the words or characters of your text in [boxes]($box) manually or +/// through a [show rule]($styling/#show-rules). +/// +/// ```example +/// >>> #set page(width: auto, height: auto, margin: 12pt) +/// >>> #set text(size: 12pt) +/// #set text(fill: gradient.linear(red, blue)) +/// #let rainbow(content) = { +/// set text(fill: gradient.linear(..color.map.rainbow)) +/// box(content) +/// } +/// +/// This is a gradient on text, but with a #rainbow[twist]! +/// ``` +/// +/// # Stops +/// A gradient is composed of a series of stops. Each of these stops has a color +/// and an offset. The offset is a [ratio]($ratio) between `{0%}` and `{100%}` or +/// an angle between `{0deg}` and `{360deg}`. The offset is a relative position +/// that determines how far along the gradient the stop is located. The stop's +/// color is the color of the gradient at that position. You can choose to omit +/// the offsets when defining a gradient. In this case, Typst will space all +/// stops evenly. +/// +/// # Relativeness +/// The location of the `{0%}` and `{100%}` stops depends on the dimensions +/// of a container. This container can either be the shape that it is being +/// painted on, or the closest surrounding container. This is controlled by the +/// `relative` argument of a gradient constructor. By default, gradients are +/// relative to the shape they are being painted on, unless the gradient is +/// applied on text, in which case they are relative to the closest ancestor +/// container. +/// +/// Typst determines the ancestor container as follows: +/// - For shapes that are placed at the root/top level of the document, the +/// closest ancestor is the page itself. +/// - For other shapes, the ancestor is the innermost [`block`] or [`box`] that +/// contains the shape. This includes the boxes and blocks that are implicitly +/// created by show rules and elements. For example, a [`rotate`] will not +/// affect the parent of a gradient, but a [`grid`] will. +/// +/// # Color spaces and interpolation +/// Gradients can be interpolated in any color space. By default, gradients are +/// interpolated in the [Oklab]($color.oklab) color space, which is a +/// [perceptually uniform](https://programmingdesignsystems.com/color/perceptually-uniform-color-spaces/index.html) +/// color space. This means that the gradient will be perceived as having a +/// smooth progression of colors. This is particularly useful for data +/// visualization. +/// +/// However, you can choose to interpolate the gradient in any supported color +/// space you want, but beware that some color spaces are not suitable for +/// perceptually interpolating between colors. Consult the table below when +/// choosing an interpolation space. +/// +/// | Color space | Perceptually uniform? | +/// | ------------------------------- |-----------------------| +/// | [Oklab]($color.oklab) | *Yes* | +/// | [Oklch]($color.oklch) | *Yes* | +/// | [sRGB]($color.rgb) | *No* | +/// | [linear-RGB]($color.linear-rgb) | *Yes* | +/// | [CMYK]($color.cmyk) | *No* | +/// | [Grayscale]($color.luma) | *Yes* | +/// | [HSL]($color.hsl) | *No* | +/// | [HSV]($color.hsv) | *No* | +/// +/// ```preview +/// >>> #set text(fill: white, font: "IBM Plex Sans", 8pt) +/// >>> #set block(spacing: 0pt) +/// #let spaces = ( +/// ("Oklab", color.oklab), +/// ("Oklch", color.oklch), +/// ("linear-RGB", color.linear-rgb), +/// ("sRGB", color.rgb), +/// ("CMYK", color.cmyk), +/// ("HSL", color.hsl), +/// ("HSV", color.hsv), +/// ("Grayscale", color.luma), +/// ) +/// +/// #for (name, space) in spaces { +/// block( +/// width: 100%, +/// inset: 4pt, +/// fill: gradient.linear( +/// red, +/// blue, +/// space: space, +/// ), +/// strong(upper(name)), +/// ) +/// } +/// ``` +/// +/// # Direction +/// Some gradients are sensitive to direction. For example, a linear gradient +/// has an angle that determines its direction. Typst uses a clockwise angle, +/// with 0° being from left to right, 90° from top to bottom, 180° from right to +/// left, and 270° from bottom to top. +/// +/// ```example +/// >>> #set square(size: 50pt) +/// #stack( +/// dir: ltr, +/// spacing: 1fr, +/// square(fill: gradient.linear(red, blue, angle: 0deg)), +/// square(fill: gradient.linear(red, blue, angle: 90deg)), +/// square(fill: gradient.linear(red, blue, angle: 180deg)), +/// square(fill: gradient.linear(red, blue, angle: 270deg)), +/// ) +/// ``` +/// +/// # Presets +/// Typst predefines color maps that you can use with your gradients. See the +/// [`color`]($color/#predefined-color-maps) documentation for more details. +/// +/// # Note on file sizes +/// +/// Gradients can be quite large, especially if they have many stops. This is +/// because gradients are stored as a list of colors and offsets, which can +/// take up a lot of space. If you are concerned about file sizes, you should +/// consider the following: +/// - SVG gradients are currently inefficiently encoded. This will be improved +/// in the future. +/// - PDF gradients in the [`color.oklab`]($color.oklab), [`color.hsv`]($color.hsv), +/// [`color.hsl`]($color.hsl), and [`color.oklch`]($color.oklch) color spaces +/// are stored as a list of [`color.rgb`]($color.rgb) colors with extra stops +/// in between. This avoids needing to encode these color spaces in your PDF +/// file, but it does add extra stops to your gradient, which can increase +/// the file size. +#[ty(scope, cast)] +#[derive(Clone, PartialEq, Eq, Hash)] +pub enum Gradient { + Linear(Arc<LinearGradient>), + Radial(Arc<RadialGradient>), + Conic(Arc<ConicGradient>), +} + +#[scope] +#[allow(clippy::too_many_arguments)] +impl Gradient { + /// Creates a new linear gradient, in which colors transition along a + /// straight line. + /// + /// ```example + /// #rect( + /// width: 100%, + /// height: 20pt, + /// fill: gradient.linear( + /// ..color.map.viridis, + /// ), + /// ) + /// ``` + #[func(title = "Linear Gradient")] + pub fn linear( + /// The args of this function. + args: &mut Args, + /// The call site of this function. + span: Span, + /// The color [stops](#stops) of the gradient. + #[variadic] + stops: Vec<Spanned<GradientStop>>, + /// The color space in which to interpolate the gradient. + /// + /// Defaults to a perceptually uniform color space called + /// [Oklab]($color.oklab). + #[named] + #[default(ColorSpace::Oklab)] + space: ColorSpace, + /// The [relative placement](#relativeness) of the gradient. + /// + /// For an element placed at the root/top level of the document, the + /// parent is the page itself. For other elements, the parent is the + /// innermost block, box, column, grid, or stack that contains the + /// element. + #[named] + #[default(Smart::Auto)] + relative: Smart<RelativeTo>, + /// The direction of the gradient. + #[external] + #[default(Dir::LTR)] + dir: Dir, + /// The angle of the gradient. + #[external] + angle: Angle, + ) -> SourceResult<Gradient> { + let angle = if let Some(angle) = args.named::<Angle>("angle")? { + angle + } else if let Some(dir) = args.named::<Dir>("dir")? { + match dir { + Dir::LTR => Angle::rad(0.0), + Dir::RTL => Angle::rad(PI), + Dir::TTB => Angle::rad(FRAC_PI_2), + Dir::BTT => Angle::rad(3.0 * FRAC_PI_2), + } + } else { + Angle::rad(0.0) + }; + + if stops.len() < 2 { + bail!( + span, "a gradient must have at least two stops"; + hint: "try filling the shape with a single color instead" + ); + } + + Ok(Self::Linear(Arc::new(LinearGradient { + stops: process_stops(&stops)?, + angle, + space, + relative, + anti_alias: true, + }))) + } + + /// Creates a new radial gradient, in which colors radiate away from an + /// origin. + /// + /// The gradient is defined by two circles: the focal circle and the end + /// circle. The focal circle is a circle with center `focal-center` and + /// radius `focal-radius`, that defines the points at which the gradient + /// starts and has the color of the first stop. The end circle is a circle + /// with center `center` and radius `radius`, that defines the points at + /// which the gradient ends and has the color of the last stop. The gradient + /// is then interpolated between these two circles. + /// + /// Using these four values, also called the focal point for the starting + /// circle and the center and radius for the end circle, we can define a + /// gradient with more interesting properties than a basic radial gradient. + /// + /// ```example + /// >>> #set circle(radius: 30pt) + /// #stack( + /// dir: ltr, + /// spacing: 1fr, + /// circle(fill: gradient.radial( + /// ..color.map.viridis, + /// )), + /// circle(fill: gradient.radial( + /// ..color.map.viridis, + /// focal-center: (10%, 40%), + /// focal-radius: 5%, + /// )), + /// ) + /// ``` + #[func] + fn radial( + /// The call site of this function. + span: Span, + /// The color [stops](#stops) of the gradient. + #[variadic] + stops: Vec<Spanned<GradientStop>>, + /// The color space in which to interpolate the gradient. + /// + /// Defaults to a perceptually uniform color space called + /// [Oklab]($color.oklab). + #[named] + #[default(ColorSpace::Oklab)] + space: ColorSpace, + /// The [relative placement](#relativeness) of the gradient. + /// + /// For an element placed at the root/top level of the document, the parent + /// is the page itself. For other elements, the parent is the innermost block, + /// box, column, grid, or stack that contains the element. + #[named] + #[default(Smart::Auto)] + relative: Smart<RelativeTo>, + /// The center of the end circle of the gradient. + /// + /// A value of `{(50%, 50%)}` means that the end circle is + /// centered inside of its container. + #[named] + #[default(Axes::splat(Ratio::new(0.5)))] + center: Axes<Ratio>, + /// The radius of the end circle of the gradient. + /// + /// By default, it is set to `{50%}`. The ending radius must be bigger + /// than the focal radius. + #[named] + #[default(Spanned::new(Ratio::new(0.5), Span::detached()))] + radius: Spanned<Ratio>, + /// The center of the focal circle of the gradient. + /// + /// The focal center must be inside of the end circle. + /// + /// A value of `{(50%, 50%)}` means that the focal circle is + /// centered inside of its container. + /// + /// By default it is set to the same as the center of the last circle. + #[named] + #[default(Smart::Auto)] + focal_center: Smart<Axes<Ratio>>, + /// The radius of the focal circle of the gradient. + /// + /// The focal center must be inside of the end circle. + /// + /// By default, it is set to `{0%}`. The focal radius must be smaller + /// than the ending radius`. + #[named] + #[default(Spanned::new(Ratio::new(0.0), Span::detached()))] + focal_radius: Spanned<Ratio>, + ) -> SourceResult<Gradient> { + if stops.len() < 2 { + bail!( + span, "a gradient must have at least two stops"; + hint: "try filling the shape with a single color instead" + ); + } + + if focal_radius.v > radius.v { + bail!( + focal_radius.span, + "the focal radius must be smaller than the end radius"; + hint: "try using a focal radius of `0%` instead" + ); + } + + let focal_center = focal_center.unwrap_or(center); + let d_center_sqr = (focal_center.x - center.x).get().powi(2) + + (focal_center.y - center.y).get().powi(2); + if d_center_sqr.sqrt() >= (radius.v - focal_radius.v).get() { + bail!( + span, + "the focal circle must be inside of the end circle"; + hint: "try using a focal center of `auto` instead" + ); + } + + Ok(Gradient::Radial(Arc::new(RadialGradient { + stops: process_stops(&stops)?, + center: center.map(From::from), + radius: radius.v, + focal_center, + focal_radius: focal_radius.v, + space, + relative, + anti_alias: true, + }))) + } + + /// Creates a new conic gradient, in which colors change radially around a + /// center point. + /// + /// You can control the center point of the gradient by using the `center` + /// argument. By default, the center point is the center of the shape. + /// + /// ```example + /// >>> #set circle(radius: 30pt) + /// #stack( + /// dir: ltr, + /// spacing: 1fr, + /// circle(fill: gradient.conic( + /// ..color.map.viridis, + /// )), + /// circle(fill: gradient.conic( + /// ..color.map.viridis, + /// center: (20%, 30%), + /// )), + /// ) + /// ``` + #[func] + pub fn conic( + /// The call site of this function. + span: Span, + /// The color [stops](#stops) of the gradient. + #[variadic] + stops: Vec<Spanned<GradientStop>>, + /// The angle of the gradient. + #[named] + #[default(Angle::zero())] + angle: Angle, + /// The color space in which to interpolate the gradient. + /// + /// Defaults to a perceptually uniform color space called + /// [Oklab]($color.oklab). + #[named] + #[default(ColorSpace::Oklab)] + space: ColorSpace, + /// The [relative placement](#relativeness) of the gradient. + /// + /// For an element placed at the root/top level of the document, the parent + /// is the page itself. For other elements, the parent is the innermost block, + /// box, column, grid, or stack that contains the element. + #[named] + #[default(Smart::Auto)] + relative: Smart<RelativeTo>, + /// The center of the last circle of the gradient. + /// + /// A value of `{(50%, 50%)}` means that the end circle is + /// centered inside of its container. + #[named] + #[default(Axes::splat(Ratio::new(0.5)))] + center: Axes<Ratio>, + ) -> SourceResult<Gradient> { + if stops.len() < 2 { + bail!( + span, "a gradient must have at least two stops"; + hint: "try filling the shape with a single color instead" + ); + } + + Ok(Gradient::Conic(Arc::new(ConicGradient { + stops: process_stops(&stops)?, + angle, + center: center.map(From::from), + space, + relative, + anti_alias: true, + }))) + } + + /// Creates a sharp version of this gradient. + /// + /// Sharp gradients have discrete jumps between colors, instead of a + /// smooth transition. They are particularly useful for creating color + /// lists for a preset gradient. + /// + /// ```example + /// #set rect(width: 100%, height: 20pt) + /// #let grad = gradient.linear(..color.map.rainbow) + /// #rect(fill: grad) + /// #rect(fill: grad.sharp(5)) + /// #rect(fill: grad.sharp(5, smoothness: 20%)) + /// ``` + #[func] + pub fn sharp( + &self, + /// The number of stops in the gradient. + steps: Spanned<usize>, + /// How much to smooth the gradient. + #[named] + #[default(Spanned::new(Ratio::zero(), Span::detached()))] + smoothness: Spanned<Ratio>, + ) -> SourceResult<Gradient> { + if steps.v < 2 { + bail!(steps.span, "sharp gradients must have at least two stops"); + } + + if smoothness.v.get() < 0.0 || smoothness.v.get() > 1.0 { + bail!(smoothness.span, "smoothness must be between 0 and 1"); + } + + let n = steps.v; + let smoothness = smoothness.v.get(); + let colors = (0..n) + .flat_map(|i| { + let c = self + .sample(RatioOrAngle::Ratio(Ratio::new(i as f64 / (n - 1) as f64))); + + [c, c] + }) + .collect::<Vec<_>>(); + + let mut positions = Vec::with_capacity(n * 2); + let index_to_progress = |i| i as f64 * 1.0 / n as f64; + + let progress = smoothness * 1.0 / (4.0 * n as f64); + for i in 0..n { + let mut j = 2 * i; + positions.push(index_to_progress(i)); + if j > 0 { + positions[j] += progress; + } + + j += 1; + positions.push(index_to_progress(i + 1)); + if j < colors.len() - 1 { + positions[j] -= progress; + } + } + + let mut stops = colors + .into_iter() + .zip(positions) + .map(|(c, p)| (c, Ratio::new(p))) + .collect::<Vec<_>>(); + + stops.dedup(); + + Ok(match self { + Self::Linear(linear) => Self::Linear(Arc::new(LinearGradient { + stops, + angle: linear.angle, + space: linear.space, + relative: linear.relative, + anti_alias: false, + })), + Self::Radial(radial) => Self::Radial(Arc::new(RadialGradient { + stops, + center: radial.center, + radius: radial.radius, + focal_center: radial.focal_center, + focal_radius: radial.focal_radius, + space: radial.space, + relative: radial.relative, + anti_alias: false, + })), + Self::Conic(conic) => Self::Conic(Arc::new(ConicGradient { + stops, + angle: conic.angle, + center: conic.center, + space: conic.space, + relative: conic.relative, + anti_alias: false, + })), + }) + } + + /// Repeats this gradient a given number of times, optionally mirroring it + /// at each repetition. + /// + /// ```example + /// #circle( + /// radius: 40pt, + /// fill: gradient + /// .radial(aqua, white) + /// .repeat(4), + /// ) + /// ``` + #[func] + pub fn repeat( + &self, + /// The number of times to repeat the gradient. + repetitions: Spanned<usize>, + /// Whether to mirror the gradient at each repetition. + #[named] + #[default(false)] + mirror: bool, + ) -> SourceResult<Gradient> { + if repetitions.v == 0 { + bail!(repetitions.span, "must repeat at least once"); + } + + let n = repetitions.v; + let mut stops = std::iter::repeat(self.stops_ref()) + .take(n) + .enumerate() + .flat_map(|(i, stops)| { + let mut stops = stops + .iter() + .map(move |&(color, offset)| { + let t = i as f64 / n as f64; + let r = offset.get(); + if i % 2 == 1 && mirror { + (color, Ratio::new(t + (1.0 - r) / n as f64)) + } else { + (color, Ratio::new(t + r / n as f64)) + } + }) + .collect::<Vec<_>>(); + + if i % 2 == 1 && mirror { + stops.reverse(); + } + + stops + }) + .collect::<Vec<_>>(); + + stops.dedup(); + + Ok(match self { + Self::Linear(linear) => Self::Linear(Arc::new(LinearGradient { + stops, + angle: linear.angle, + space: linear.space, + relative: linear.relative, + anti_alias: linear.anti_alias, + })), + Self::Radial(radial) => Self::Radial(Arc::new(RadialGradient { + stops, + center: radial.center, + radius: radial.radius, + focal_center: radial.focal_center, + focal_radius: radial.focal_radius, + space: radial.space, + relative: radial.relative, + anti_alias: radial.anti_alias, + })), + Self::Conic(conic) => Self::Conic(Arc::new(ConicGradient { + stops, + angle: conic.angle, + center: conic.center, + space: conic.space, + relative: conic.relative, + anti_alias: conic.anti_alias, + })), + }) + } + + /// Returns the kind of this gradient. + #[func] + pub fn kind(&self) -> Func { + match self { + Self::Linear(_) => Self::linear_data().into(), + Self::Radial(_) => Self::radial_data().into(), + Self::Conic(_) => Self::conic_data().into(), + } + } + + /// Returns the stops of this gradient. + #[func] + pub fn stops(&self) -> Vec<GradientStop> { + match self { + Self::Linear(linear) => linear + .stops + .iter() + .map(|(color, offset)| GradientStop { + color: *color, + offset: Some(*offset), + }) + .collect(), + Self::Radial(radial) => radial + .stops + .iter() + .map(|(color, offset)| GradientStop { + color: *color, + offset: Some(*offset), + }) + .collect(), + Self::Conic(conic) => conic + .stops + .iter() + .map(|(color, offset)| GradientStop { + color: *color, + offset: Some(*offset), + }) + .collect(), + } + } + + /// Returns the mixing space of this gradient. + #[func] + pub fn space(&self) -> ColorSpace { + match self { + Self::Linear(linear) => linear.space, + Self::Radial(radial) => radial.space, + Self::Conic(conic) => conic.space, + } + } + + /// Returns the relative placement of this gradient. + #[func] + pub fn relative(&self) -> Smart<RelativeTo> { + match self { + Self::Linear(linear) => linear.relative, + Self::Radial(radial) => radial.relative, + Self::Conic(conic) => conic.relative, + } + } + + /// Returns the angle of this gradient. + #[func] + pub fn angle(&self) -> Option<Angle> { + match self { + Self::Linear(linear) => Some(linear.angle), + Self::Radial(_) => None, + Self::Conic(conic) => Some(conic.angle), + } + } + + /// Sample the gradient at a given position. + /// + /// The position is either a position along the gradient (a [ratio] between + /// `{0%}` and `{100%}`) or an [angle]. Any value outside of this range will + /// be clamped. + #[func] + pub fn sample( + &self, + /// The position at which to sample the gradient. + t: RatioOrAngle, + ) -> Color { + let value: f64 = t.to_ratio().get(); + + match self { + Self::Linear(linear) => sample_stops(&linear.stops, linear.space, value), + Self::Radial(radial) => sample_stops(&radial.stops, radial.space, value), + Self::Conic(conic) => sample_stops(&conic.stops, conic.space, value), + } + } + + /// Samples the gradient at multiple positions at once and returns the + /// results as an array. + #[func] + pub fn samples( + &self, + /// The positions at which to sample the gradient. + #[variadic] + ts: Vec<RatioOrAngle>, + ) -> Array { + ts.into_iter().map(|t| self.sample(t).into_value()).collect() + } +} + +impl Gradient { + /// Clones this gradient, but with a different relative placement. + pub fn with_relative(mut self, relative: RelativeTo) -> Self { + match &mut self { + Self::Linear(linear) => { + Arc::make_mut(linear).relative = Smart::Custom(relative); + } + Self::Radial(radial) => { + Arc::make_mut(radial).relative = Smart::Custom(relative); + } + Self::Conic(conic) => { + Arc::make_mut(conic).relative = Smart::Custom(relative); + } + } + + self + } + /// Returns a reference to the stops of this gradient. + pub fn stops_ref(&self) -> &[(Color, Ratio)] { + match self { + Gradient::Linear(linear) => &linear.stops, + Gradient::Radial(radial) => &radial.stops, + Gradient::Conic(conic) => &conic.stops, + } + } + + /// Samples the gradient at a given position, in the given container. + /// Handles the aspect ratio and angle directly. + pub fn sample_at(&self, (x, y): (f32, f32), (width, height): (f32, f32)) -> Color { + // Normalize the coordinates. + let (mut x, mut y) = (x / width, y / height); + let t = match self { + Self::Linear(linear) => { + // Aspect ratio correction. + let angle = Gradient::correct_aspect_ratio( + linear.angle, + Ratio::new((width / height) as f64), + ) + .to_rad(); + let (sin, cos) = angle.sin_cos(); + + let length = sin.abs() + cos.abs(); + if angle > FRAC_PI_2 && angle < 3.0 * FRAC_PI_2 { + x = 1.0 - x; + } + + if angle > PI { + y = 1.0 - y; + } + + (x as f64 * cos.abs() + y as f64 * sin.abs()) / length + } + Self::Radial(radial) => { + // Source: @Enivex - https://typst.app/project/pYLeS0QyCCe8mf0pdnwoAI + let cr = radial.radius.get(); + let fr = radial.focal_radius.get(); + let z = Vec2::new(x as f64, y as f64); + let p = Vec2::new(radial.center.x.get(), radial.center.y.get()); + let q = + Vec2::new(radial.focal_center.x.get(), radial.focal_center.y.get()); + + if (z - q).hypot() < fr { + 0.0 + } else if (z - p).hypot() > cr { + 1.0 + } else { + let uz = (z - q).normalize(); + let az = (q - p).dot(uz); + let rho = cr.powi(2) - (q - p).hypot().powi(2); + let bz = (az.powi(2) + rho).sqrt() - az; + + ((z - q).hypot() - fr) / (bz - fr) + } + } + Self::Conic(conic) => { + let (x, y) = + (x as f64 - conic.center.x.get(), y as f64 - conic.center.y.get()); + let angle = Gradient::correct_aspect_ratio( + conic.angle, + Ratio::new((width / height) as f64), + ); + ((-y.atan2(x) + PI + angle.to_rad()) % TAU) / TAU + } + }; + + self.sample(RatioOrAngle::Ratio(Ratio::new(t.clamp(0.0, 1.0)))) + } + + /// Does this gradient need to be anti-aliased? + pub fn anti_alias(&self) -> bool { + match self { + Self::Linear(linear) => linear.anti_alias, + Self::Radial(radial) => radial.anti_alias, + Self::Conic(conic) => conic.anti_alias, + } + } + + /// Returns the relative placement of this gradient, handling + /// the special case of `auto`. + pub fn unwrap_relative(&self, on_text: bool) -> RelativeTo { + self.relative().unwrap_or_else(|| { + if on_text { + RelativeTo::Parent + } else { + RelativeTo::Self_ + } + }) + } + + /// Corrects this angle for the aspect ratio of a gradient. + /// + /// This is used specifically for gradients. + pub fn correct_aspect_ratio(angle: Angle, aspect_ratio: Ratio) -> Angle { + let rad = (angle.to_rad().rem_euclid(TAU).tan() / aspect_ratio.get()).atan(); + let rad = match angle.quadrant() { + Quadrant::First => rad, + Quadrant::Second => rad + PI, + Quadrant::Third => rad + PI, + Quadrant::Fourth => rad + TAU, + }; + Angle::rad(rad.rem_euclid(TAU)) + } +} + +impl Debug for Gradient { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Linear(v) => v.fmt(f), + Self::Radial(v) => v.fmt(f), + Self::Conic(v) => v.fmt(f), + } + } +} + +impl Repr for Gradient { + fn repr(&self) -> EcoString { + match self { + Self::Radial(radial) => radial.repr(), + Self::Linear(linear) => linear.repr(), + Self::Conic(conic) => conic.repr(), + } + } +} + +/// A gradient that interpolates between two colors along an axis. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct LinearGradient { + /// The color stops of this gradient. + pub stops: Vec<(Color, Ratio)>, + /// The direction of this gradient. + pub angle: Angle, + /// The color space in which to interpolate the gradient. + pub space: ColorSpace, + /// The relative placement of the gradient. + pub relative: Smart<RelativeTo>, + /// Whether to anti-alias the gradient (used for sharp gradients). + pub anti_alias: bool, +} + +impl Repr for LinearGradient { + fn repr(&self) -> EcoString { + let mut r = EcoString::from("gradient.linear("); + + let angle = self.angle.to_rad().rem_euclid(TAU); + if angle.abs() < f64::EPSILON { + // Default value, do nothing + } else if (angle - FRAC_PI_2).abs() < f64::EPSILON { + r.push_str("dir: rtl, "); + } else if (angle - PI).abs() < f64::EPSILON { + r.push_str("dir: ttb, "); + } else if (angle - 3.0 * FRAC_PI_2).abs() < f64::EPSILON { + r.push_str("dir: btt, "); + } else { + r.push_str("angle: "); + r.push_str(&self.angle.repr()); + r.push_str(", "); + } + + if self.space != ColorSpace::Oklab { + r.push_str("space: "); + r.push_str(&self.space.into_value().repr()); + r.push_str(", "); + } + + if self.relative.is_custom() { + r.push_str("relative: "); + r.push_str(&self.relative.into_value().repr()); + r.push_str(", "); + } + + for (i, (color, offset)) in self.stops.iter().enumerate() { + r.push('('); + r.push_str(&color.repr()); + r.push_str(", "); + r.push_str(&offset.repr()); + r.push(')'); + if i != self.stops.len() - 1 { + r.push_str(", "); + } + } + + r.push(')'); + r + } +} + +/// A gradient that interpolates between two colors along a circle. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct RadialGradient { + /// The color stops of this gradient. + pub stops: Vec<(Color, Ratio)>, + /// The center of last circle of this gradient. + pub center: Axes<Ratio>, + /// The radius of last circle of this gradient. + pub radius: Ratio, + /// The center of first circle of this gradient. + pub focal_center: Axes<Ratio>, + /// The radius of first circle of this gradient. + pub focal_radius: Ratio, + /// The color space in which to interpolate the gradient. + pub space: ColorSpace, + /// The relative placement of the gradient. + pub relative: Smart<RelativeTo>, + /// Whether to anti-alias the gradient (used for sharp gradients). + pub anti_alias: bool, +} + +impl Repr for RadialGradient { + fn repr(&self) -> EcoString { + let mut r = EcoString::from("gradient.radial("); + + if self.center.x != Ratio::new(0.5) || self.center.y != Ratio::new(0.5) { + r.push_str("center: ("); + r.push_str(&self.center.x.repr()); + r.push_str(", "); + r.push_str(&self.center.y.repr()); + r.push_str("), "); + } + + if self.radius != Ratio::new(0.5) { + r.push_str("radius: "); + r.push_str(&self.radius.repr()); + r.push_str(", "); + } + + if self.focal_center != self.center { + r.push_str("focal-center: ("); + r.push_str(&self.focal_center.x.repr()); + r.push_str(", "); + r.push_str(&self.focal_center.y.repr()); + r.push_str("), "); + } + + if self.focal_radius != Ratio::zero() { + r.push_str("focal-radius: "); + r.push_str(&self.focal_radius.repr()); + r.push_str(", "); + } + + if self.space != ColorSpace::Oklab { + r.push_str("space: "); + r.push_str(&self.space.into_value().repr()); + r.push_str(", "); + } + + if self.relative.is_custom() { + r.push_str("relative: "); + r.push_str(&self.relative.into_value().repr()); + r.push_str(", "); + } + + for (i, (color, offset)) in self.stops.iter().enumerate() { + r.push('('); + r.push_str(&color.repr()); + r.push_str(", "); + r.push_str(&offset.repr()); + r.push(')'); + if i != self.stops.len() - 1 { + r.push_str(", "); + } + } + + r.push(')'); + r + } +} + +/// A gradient that interpolates between two colors radially +/// around a center point. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct ConicGradient { + /// The color stops of this gradient. + pub stops: Vec<(Color, Ratio)>, + /// The direction of this gradient. + pub angle: Angle, + /// The center of last circle of this gradient. + pub center: Axes<Ratio>, + /// The color space in which to interpolate the gradient. + pub space: ColorSpace, + /// The relative placement of the gradient. + pub relative: Smart<RelativeTo>, + /// Whether to anti-alias the gradient (used for sharp gradients). + pub anti_alias: bool, +} + +impl Repr for ConicGradient { + fn repr(&self) -> EcoString { + let mut r = EcoString::from("gradient.conic("); + + let angle = self.angle.to_rad().rem_euclid(TAU); + if angle.abs() > f64::EPSILON { + r.push_str("angle: "); + r.push_str(&self.angle.repr()); + r.push_str(", "); + } + + if self.center.x != Ratio::new(0.5) || self.center.y != Ratio::new(0.5) { + r.push_str("center: ("); + r.push_str(&self.center.x.repr()); + r.push_str(", "); + r.push_str(&self.center.y.repr()); + r.push_str("), "); + } + + if self.space != ColorSpace::Oklab { + r.push_str("space: "); + r.push_str(&self.space.into_value().repr()); + r.push_str(", "); + } + + if self.relative.is_custom() { + r.push_str("relative: "); + r.push_str(&self.relative.into_value().repr()); + r.push_str(", "); + } + + for (i, (color, offset)) in self.stops.iter().enumerate() { + r.push('('); + r.push_str(&color.repr()); + r.push_str(", "); + r.push_str(&Angle::deg(offset.get() * 360.0).repr()); + r.push(')'); + if i != self.stops.len() - 1 { + r.push_str(", "); + } + } + + r.push(')'); + r + } +} + +/// What is the gradient relative to. +#[derive(Cast, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum RelativeTo { + /// The gradient is relative to itself (its own bounding box). + Self_, + /// The gradient is relative to its parent (the parent's bounding box). + Parent, +} + +/// A color stop. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct GradientStop { + /// The color for this stop. + pub color: Color, + /// The offset of the stop along the gradient. + pub offset: Option<Ratio>, +} + +impl GradientStop { + /// Create a new stop from a `color` and an `offset`. + pub fn new(color: Color, offset: Ratio) -> Self { + Self { color, offset: Some(offset) } + } +} + +cast! { + GradientStop, + self => if let Some(offset) = self.offset { + array![self.color.into_value(), offset].into_value() + } else { + self.color.into_value() + }, + color: Color => Self { color, offset: None }, + array: Array => { + let mut iter = array.into_iter(); + match (iter.next(), iter.next(), iter.next()) { + (Some(a), Some(b), None) => Self { + color: a.cast()?, + offset: Some(b.cast()?) + }, + _ => Err("a color stop must contain exactly two entries")?, + } + } +} + +/// A ratio or an angle. +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub enum RatioOrAngle { + Ratio(Ratio), + Angle(Angle), +} + +impl RatioOrAngle { + pub fn to_ratio(self) -> Ratio { + match self { + Self::Ratio(ratio) => ratio, + Self::Angle(angle) => Ratio::new(angle.to_rad().rem_euclid(TAU) / TAU), + } + .clamp(Ratio::zero(), Ratio::one()) + } +} + +cast! { + RatioOrAngle, + self => match self { + Self::Ratio(ratio) => ratio.into_value(), + Self::Angle(angle) => angle.into_value(), + }, + ratio: Ratio => Self::Ratio(ratio), + angle: Angle => Self::Angle(angle), +} + +/// Pre-processes the stops, checking that they are valid and computing the +/// offsets if necessary. +/// +/// Returns an error if the stops are invalid. +/// +/// This is split into its own function because it is used by all of the +/// different gradient types. +#[comemo::memoize] +fn process_stops(stops: &[Spanned<GradientStop>]) -> SourceResult<Vec<(Color, Ratio)>> { + let has_offset = stops.iter().any(|stop| stop.v.offset.is_some()); + if has_offset { + let mut last_stop = f64::NEG_INFINITY; + for Spanned { v: stop, span } in stops.iter() { + let Some(stop) = stop.offset else { + bail!( + *span, "either all stops must have an offset or none of them can"; + hint: "try adding an offset to all stops" + ); + }; + + if stop.get() < last_stop { + bail!(*span, "offsets must be in strictly monotonic order"); + } + + last_stop = stop.get(); + } + + let out = stops + .iter() + .map(|Spanned { v: GradientStop { color, offset }, span }| { + if offset.unwrap().get() > 1.0 || offset.unwrap().get() < 0.0 { + bail!(*span, "offset must be between 0 and 1"); + } + Ok((*color, offset.unwrap())) + }) + .collect::<SourceResult<Vec<_>>>()?; + + if out[0].1 != Ratio::zero() { + bail!( + stops[0].span, + "first stop must have an offset of 0"; + hint: "try setting this stop to `0%`" + ); + } + + if out[out.len() - 1].1 != Ratio::one() { + bail!( + stops[out.len() - 1].span, + "last stop must have an offset of 100%"; + hint: "try setting this stop to `100%`" + ); + } + + return Ok(out); + } + + Ok(stops + .iter() + .enumerate() + .map(|(i, stop)| { + let offset = i as f64 / (stops.len() - 1) as f64; + (stop.v.color, Ratio::new(offset)) + }) + .collect()) +} + +/// Sample the stops at a given position. +fn sample_stops(stops: &[(Color, Ratio)], mixing_space: ColorSpace, t: f64) -> Color { + let t = t.clamp(0.0, 1.0); + let mut low = 0; + let mut high = stops.len(); + + while low < high { + let mid = (low + high) / 2; + if stops[mid].1.get() < t { + low = mid + 1; + } else { + high = mid; + } + } + + if low == 0 { + low = 1; + } + + let (col_0, pos_0) = stops[low - 1]; + let (col_1, pos_1) = stops[low]; + let t = (t - pos_0.get()) / (pos_1.get() - pos_0.get()); + + Color::mix_iter( + [WeightedColor::new(col_0, 1.0 - t), WeightedColor::new(col_1, t)], + mixing_space, + ) + .unwrap() +} diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs new file mode 100644 index 00000000..359db252 --- /dev/null +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -0,0 +1,360 @@ +//! Image handling. + +mod raster; +mod svg; + +pub use self::raster::{RasterFormat, RasterImage}; +pub use self::svg::SvgImage; + +use std::fmt::{self, Debug, Formatter}; +use std::sync::Arc; + +use comemo::Tracked; +use ecow::EcoString; +use typst_syntax::{Span, Spanned}; +use typst_utils::LazyHash; + +use crate::diag::{At, SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, func, scope, Bytes, Cast, Content, NativeElement, Packed, Show, Smart, + StyleChain, +}; +use crate::layout::{BlockElem, Length, Rel, Sizing}; +use crate::loading::Readable; +use crate::model::Figurable; +use crate::text::LocalName; +use crate::World; + +/// A raster or vector graphic. +/// +/// You can wrap the image in a [`figure`] to give it a number and caption. +/// +/// Like most elements, images are _block-level_ by default and thus do not +/// integrate themselves into adjacent paragraphs. To force an image to become +/// inline, put it into a [`box`]. +/// +/// # Example +/// ```example +/// #figure( +/// image("molecular.jpg", width: 80%), +/// caption: [ +/// A step in the molecular testing +/// pipeline of our lab. +/// ], +/// ) +/// ``` +#[elem(scope, Show, LocalName, Figurable)] +pub struct ImageElem { + /// Path to an image file + /// + /// For more details, see the [Paths section]($syntax/#paths). + #[required] + #[parse( + let Spanned { v: path, span } = + args.expect::<Spanned<EcoString>>("path to image file")?; + let id = span.resolve_path(&path).at(span)?; + let data = engine.world.file(id).at(span)?; + path + )] + #[borrowed] + pub path: EcoString, + + /// The raw file data. + #[internal] + #[required] + #[parse(Readable::Bytes(data))] + pub data: Readable, + + /// The image's format. Detected automatically by default. + /// + /// Supported formats are PNG, JPEG, GIF, and SVG. Using a PDF as an image + /// is [not currently supported](https://github.com/typst/typst/issues/145). + pub format: Smart<ImageFormat>, + + /// The width of the image. + pub width: Smart<Rel<Length>>, + + /// The height of the image. + pub height: Sizing, + + /// A text describing the image. + pub alt: Option<EcoString>, + + /// How the image should adjust itself to a given area (the area is defined + /// by the `width` and `height` fields). Note that `fit` doesn't visually + /// change anything if the area's aspect ratio is the same as the image's + /// one. + /// + /// ```example + /// #set page(width: 300pt, height: 50pt, margin: 10pt) + /// #image("tiger.jpg", width: 100%, fit: "cover") + /// #image("tiger.jpg", width: 100%, fit: "contain") + /// #image("tiger.jpg", width: 100%, fit: "stretch") + /// ``` + #[default(ImageFit::Cover)] + pub fit: ImageFit, +} + +#[scope] +impl ImageElem { + /// Decode a raster or vector graphic from bytes or a string. + /// + /// ```example + /// #let original = read("diagram.svg") + /// #let changed = original.replace( + /// "#2B80FF", // blue + /// green.to-hex(), + /// ) + /// + /// #image.decode(original) + /// #image.decode(changed) + /// ``` + #[func(title = "Decode Image")] + pub fn decode( + /// The call span of this function. + span: Span, + /// The data to decode as an image. Can be a string for SVGs. + data: Readable, + /// The image's format. Detected automatically by default. + #[named] + format: Option<Smart<ImageFormat>>, + /// The width of the image. + #[named] + width: Option<Smart<Rel<Length>>>, + /// The height of the image. + #[named] + height: Option<Sizing>, + /// A text describing the image. + #[named] + alt: Option<Option<EcoString>>, + /// How the image should adjust itself to a given area. + #[named] + fit: Option<ImageFit>, + ) -> StrResult<Content> { + let mut elem = ImageElem::new(EcoString::new(), data); + if let Some(format) = format { + elem.push_format(format); + } + if let Some(width) = width { + elem.push_width(width); + } + if let Some(height) = height { + elem.push_height(height); + } + if let Some(alt) = alt { + elem.push_alt(alt); + } + if let Some(fit) = fit { + elem.push_fit(fit); + } + Ok(elem.pack().spanned(span)) + } +} + +impl Show for Packed<ImageElem> { + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_image) + .with_width(self.width(styles)) + .with_height(self.height(styles)) + .pack() + .spanned(self.span())) + } +} + +impl LocalName for Packed<ImageElem> { + const KEY: &'static str = "figure"; +} + +impl Figurable for Packed<ImageElem> {} + +/// How an image should adjust itself to a given area, +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum ImageFit { + /// The image should completely cover the area (preserves aspect ratio by + /// cropping the image only horizontally or vertically). This is the + /// default. + Cover, + /// The image should be fully contained in the area (preserves aspect + /// ratio; doesn't crop the image; one dimension can be narrower than + /// specified). + Contain, + /// The image should be stretched so that it exactly fills the area, even if + /// this means that the image will be distorted (doesn't preserve aspect + /// ratio and doesn't crop the image). + Stretch, +} + +/// A loaded raster or vector image. +/// +/// Values of this type are cheap to clone and hash. +#[derive(Clone, Hash, Eq, PartialEq)] +pub struct Image(Arc<LazyHash<Repr>>); + +/// The internal representation. +#[derive(Hash)] +struct Repr { + /// The raw, undecoded image data. + kind: ImageKind, + /// A text describing the image. + alt: Option<EcoString>, +} + +/// A kind of image. +#[derive(Hash)] +pub enum ImageKind { + /// A raster image. + Raster(RasterImage), + /// An SVG image. + Svg(SvgImage), +} + +impl Image { + /// When scaling an image to it's natural size, we default to this DPI + /// if the image doesn't contain DPI metadata. + pub const DEFAULT_DPI: f64 = 72.0; + + /// Should always be the same as the default DPI used by usvg. + pub const USVG_DEFAULT_DPI: f64 = 96.0; + + /// Create an image from a buffer and a format. + #[comemo::memoize] + #[typst_macros::time(name = "load image")] + pub fn new( + data: Bytes, + format: ImageFormat, + alt: Option<EcoString>, + ) -> StrResult<Image> { + let kind = match format { + ImageFormat::Raster(format) => { + ImageKind::Raster(RasterImage::new(data, format)?) + } + ImageFormat::Vector(VectorFormat::Svg) => { + ImageKind::Svg(SvgImage::new(data)?) + } + }; + + Ok(Self(Arc::new(LazyHash::new(Repr { kind, alt })))) + } + + /// Create a possibly font-dependent image from a buffer and a format. + #[comemo::memoize] + #[typst_macros::time(name = "load image")] + pub fn with_fonts( + data: Bytes, + format: ImageFormat, + alt: Option<EcoString>, + world: Tracked<dyn World + '_>, + families: &[&str], + ) -> StrResult<Image> { + let kind = match format { + ImageFormat::Raster(format) => { + ImageKind::Raster(RasterImage::new(data, format)?) + } + ImageFormat::Vector(VectorFormat::Svg) => { + ImageKind::Svg(SvgImage::with_fonts(data, world, families)?) + } + }; + + Ok(Self(Arc::new(LazyHash::new(Repr { kind, alt })))) + } + + /// The raw image data. + pub fn data(&self) -> &Bytes { + match &self.0.kind { + ImageKind::Raster(raster) => raster.data(), + ImageKind::Svg(svg) => svg.data(), + } + } + + /// The format of the image. + pub fn format(&self) -> ImageFormat { + match &self.0.kind { + ImageKind::Raster(raster) => raster.format().into(), + ImageKind::Svg(_) => VectorFormat::Svg.into(), + } + } + + /// The width of the image in pixels. + pub fn width(&self) -> f64 { + match &self.0.kind { + ImageKind::Raster(raster) => raster.width() as f64, + ImageKind::Svg(svg) => svg.width(), + } + } + + /// The height of the image in pixels. + pub fn height(&self) -> f64 { + match &self.0.kind { + ImageKind::Raster(raster) => raster.height() as f64, + ImageKind::Svg(svg) => svg.height(), + } + } + + /// The image's pixel density in pixels per inch, if known. + pub fn dpi(&self) -> Option<f64> { + match &self.0.kind { + ImageKind::Raster(raster) => raster.dpi(), + ImageKind::Svg(_) => Some(Image::USVG_DEFAULT_DPI), + } + } + + /// A text describing the image. + pub fn alt(&self) -> Option<&str> { + self.0.alt.as_deref() + } + + /// The decoded image. + pub fn kind(&self) -> &ImageKind { + &self.0.kind + } +} + +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 vector graphics format. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum VectorFormat { + /// The vector graphics format of the web. + Svg, +} + +impl From<RasterFormat> for ImageFormat { + fn from(format: RasterFormat) -> Self { + Self::Raster(format) + } +} + +impl From<VectorFormat> for ImageFormat { + fn from(format: VectorFormat) -> Self { + Self::Vector(format) + } +} + +cast! { + ImageFormat, + self => match self { + Self::Raster(v) => v.into_value(), + Self::Vector(v) => v.into_value() + }, + v: RasterFormat => Self::Raster(v), + v: VectorFormat => Self::Vector(v), +} diff --git a/crates/typst-library/src/visualize/image/raster.rs b/crates/typst-library/src/visualize/image/raster.rs new file mode 100644 index 00000000..829826c7 --- /dev/null +++ b/crates/typst-library/src/visualize/image/raster.rs @@ -0,0 +1,286 @@ +use std::cmp::Ordering; +use std::hash::{Hash, Hasher}; +use std::io; +use std::sync::Arc; + +use ecow::{eco_format, EcoString}; +use image::codecs::gif::GifDecoder; +use image::codecs::jpeg::JpegDecoder; +use image::codecs::png::PngDecoder; +use image::{guess_format, DynamicImage, ImageDecoder, ImageResult, Limits}; + +use crate::diag::{bail, StrResult}; +use crate::foundations::{Bytes, Cast}; + +/// A decoded raster image. +#[derive(Clone, Hash)] +pub struct RasterImage(Arc<Repr>); + +/// The internal representation. +struct Repr { + data: Bytes, + format: RasterFormat, + dynamic: image::DynamicImage, + icc: Option<Vec<u8>>, + dpi: Option<f64>, +} + +impl RasterImage { + /// Decode a raster image. + #[comemo::memoize] + pub fn new(data: Bytes, format: RasterFormat) -> StrResult<RasterImage> { + fn decode_with<T: ImageDecoder>( + decoder: ImageResult<T>, + ) -> ImageResult<(image::DynamicImage, Option<Vec<u8>>)> { + let mut decoder = decoder?; + let icc = decoder.icc_profile().ok().flatten().filter(|icc| !icc.is_empty()); + decoder.set_limits(Limits::default())?; + let dynamic = image::DynamicImage::from_decoder(decoder)?; + Ok((dynamic, icc)) + } + + let cursor = io::Cursor::new(&data); + let (mut 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)?; + + let exif = exif::Reader::new() + .read_from_container(&mut std::io::Cursor::new(&data)) + .ok(); + + // Apply rotation from EXIF metadata. + if let Some(rotation) = exif.as_ref().and_then(exif_rotation) { + apply_rotation(&mut dynamic, rotation); + } + + // Extract pixel density. + let dpi = determine_dpi(&data, exif.as_ref()); + + Ok(Self(Arc::new(Repr { data, format, dynamic, icc, dpi }))) + } + + /// The raw image data. + pub fn data(&self) -> &Bytes { + &self.0.data + } + + /// The image's format. + pub fn format(&self) -> RasterFormat { + self.0.format + } + + /// The image's pixel width. + pub fn width(&self) -> u32 { + self.dynamic().width() + } + + /// The image's pixel height. + pub fn height(&self) -> u32 { + self.dynamic().height() + } + + /// The image's pixel density in pixels per inch, if known. + pub fn dpi(&self) -> Option<f64> { + self.0.dpi + } + + /// Access the underlying dynamic image. + pub fn dynamic(&self) -> &image::DynamicImage { + &self.0.dynamic + } + + /// Access the ICC profile, if any. + pub fn icc(&self) -> Option<&[u8]> { + self.0.icc.as_deref() + } +} + +impl Hash for Repr { + fn hash<H: Hasher>(&self, state: &mut H) { + // The image is fully defined by data and format. + self.data.hash(state); + self.format.hash(state); + } +} + +/// A raster graphics format. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +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, +} + +impl RasterFormat { + /// Try to detect the format of data in a buffer. + pub fn detect(data: &[u8]) -> Option<Self> { + guess_format(data).ok().and_then(|format| format.try_into().ok()) + } +} + +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 TryFrom<image::ImageFormat> for RasterFormat { + type Error = EcoString; + + fn try_from(format: image::ImageFormat) -> StrResult<Self> { + Ok(match format { + image::ImageFormat::Png => RasterFormat::Png, + image::ImageFormat::Jpeg => RasterFormat::Jpg, + image::ImageFormat::Gif => RasterFormat::Gif, + _ => bail!("Format not yet supported."), + }) + } +} + +/// Try to get the rotation from the EXIF metadata. +fn exif_rotation(exif: &exif::Exif) -> Option<u32> { + exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY)? + .value + .get_uint(0) +} + +/// Apply an EXIF rotation to a dynamic image. +fn apply_rotation(image: &mut DynamicImage, rotation: u32) { + use image::imageops as ops; + match rotation { + 2 => ops::flip_horizontal_in_place(image), + 3 => ops::rotate180_in_place(image), + 4 => ops::flip_vertical_in_place(image), + 5 => { + ops::flip_horizontal_in_place(image); + *image = image.rotate270(); + } + 6 => *image = image.rotate90(), + 7 => { + ops::flip_horizontal_in_place(image); + *image = image.rotate90(); + } + 8 => *image = image.rotate270(), + _ => {} + } +} + +/// Try to determine the DPI (dots per inch) of the image. +fn determine_dpi(data: &[u8], exif: Option<&exif::Exif>) -> Option<f64> { + // Try to extract the DPI from the EXIF metadata. If that doesn't yield + // anything, fall back to specialized procedures for extracting JPEG or PNG + // DPI metadata. GIF does not have any. + exif.and_then(exif_dpi) + .or_else(|| jpeg_dpi(data)) + .or_else(|| png_dpi(data)) +} + +/// Try to get the DPI from the EXIF metadata. +fn exif_dpi(exif: &exif::Exif) -> Option<f64> { + let axis = |tag| { + let dpi = exif.get_field(tag, exif::In::PRIMARY)?; + let exif::Value::Rational(rational) = &dpi.value else { return None }; + Some(rational.first()?.to_f64()) + }; + + [axis(exif::Tag::XResolution), axis(exif::Tag::YResolution)] + .into_iter() + .flatten() + .max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)) +} + +/// Tries to extract the DPI from raw JPEG data (by inspecting the JFIF APP0 +/// section). +fn jpeg_dpi(data: &[u8]) -> Option<f64> { + let validate_at = |index: usize, expect: &[u8]| -> Option<()> { + data.get(index..)?.starts_with(expect).then_some(()) + }; + let u16_at = |index: usize| -> Option<u16> { + data.get(index..index + 2)?.try_into().ok().map(u16::from_be_bytes) + }; + + validate_at(0, b"\xFF\xD8\xFF\xE0\0")?; + validate_at(6, b"JFIF\0")?; + validate_at(11, b"\x01")?; + + let len = u16_at(4)?; + if len < 16 { + return None; + } + + let units = *data.get(13)?; + let x = u16_at(14)?; + let y = u16_at(16)?; + let dpu = x.max(y) as f64; + + Some(match units { + 1 => dpu, // already inches + 2 => dpu * 2.54, // cm -> inches + _ => return None, + }) +} + +/// Tries to extract the DPI from raw PNG data. +fn png_dpi(mut data: &[u8]) -> Option<f64> { + let mut decoder = png::StreamingDecoder::new(); + let dims = loop { + let (consumed, event) = decoder.update(data, &mut Vec::new()).ok()?; + match event { + png::Decoded::PixelDimensions(dims) => break dims, + // Bail as soon as there is anything data-like. + png::Decoded::ChunkBegin(_, png::chunk::IDAT) + | png::Decoded::ImageData + | png::Decoded::ImageEnd => return None, + _ => {} + } + data = data.get(consumed..)?; + if consumed == 0 { + return None; + } + }; + + let dpu = dims.xppu.max(dims.yppu) as f64; + match dims.unit { + png::Unit::Meter => Some(dpu * 0.0254), // meter -> inches + png::Unit::Unspecified => None, + } +} + +/// 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(), + err => eco_format!("failed to decode image ({err})"), + } +} + +#[cfg(test)] +mod tests { + use super::{RasterFormat, RasterImage}; + use crate::foundations::Bytes; + + #[test] + fn test_image_dpi() { + #[track_caller] + fn test(path: &str, format: RasterFormat, dpi: f64) { + let data = typst_dev_assets::get(path).unwrap(); + let bytes = Bytes::from_static(data); + let image = RasterImage::new(bytes, format).unwrap(); + assert_eq!(image.dpi().map(f64::round), Some(dpi)); + } + + test("images/f2t.jpg", RasterFormat::Jpg, 220.0); + test("images/tiger.jpg", RasterFormat::Jpg, 72.0); + test("images/graph.png", RasterFormat::Png, 144.0); + } +} diff --git a/crates/typst-library/src/visualize/image/svg.rs b/crates/typst-library/src/visualize/image/svg.rs new file mode 100644 index 00000000..f7a498a8 --- /dev/null +++ b/crates/typst-library/src/visualize/image/svg.rs @@ -0,0 +1,289 @@ +use std::collections::HashMap; +use std::hash::{Hash, Hasher}; +use std::sync::{Arc, Mutex}; + +use comemo::Tracked; +use ecow::EcoString; +use siphasher::sip128::{Hasher128, SipHasher13}; + +use crate::diag::{format_xml_like_error, StrResult}; +use crate::foundations::Bytes; +use crate::layout::Axes; +use crate::text::{ + Font, FontBook, FontFlags, FontStretch, FontStyle, FontVariant, FontWeight, +}; +use crate::World; + +/// A decoded SVG. +#[derive(Clone, Hash)] +pub struct SvgImage(Arc<Repr>); + +/// The internal representation. +struct Repr { + data: Bytes, + size: Axes<f64>, + font_hash: u128, + tree: usvg::Tree, +} + +impl SvgImage { + /// Decode an SVG image without fonts. + #[comemo::memoize] + pub fn new(data: Bytes) -> StrResult<SvgImage> { + let tree = + usvg::Tree::from_data(&data, &base_options()).map_err(format_usvg_error)?; + Ok(Self(Arc::new(Repr { data, size: tree_size(&tree), font_hash: 0, tree }))) + } + + /// Decode an SVG image with access to fonts. + #[comemo::memoize] + pub fn with_fonts( + data: Bytes, + world: Tracked<dyn World + '_>, + families: &[&str], + ) -> StrResult<SvgImage> { + let book = world.book(); + let resolver = Mutex::new(FontResolver::new(world, book, families)); + let tree = usvg::Tree::from_data( + &data, + &usvg::Options { + font_resolver: usvg::FontResolver { + select_font: Box::new(|font, db| { + resolver.lock().unwrap().select_font(font, db) + }), + select_fallback: Box::new(|c, exclude_fonts, db| { + resolver.lock().unwrap().select_fallback(c, exclude_fonts, db) + }), + }, + ..base_options() + }, + ) + .map_err(format_usvg_error)?; + let font_hash = resolver.into_inner().unwrap().finish(); + Ok(Self(Arc::new(Repr { data, size: tree_size(&tree), font_hash, tree }))) + } + + /// The raw image data. + pub fn data(&self) -> &Bytes { + &self.0.data + } + + /// The SVG's width in pixels. + pub fn width(&self) -> f64 { + self.0.size.x + } + + /// The SVG's height in pixels. + pub fn height(&self) -> f64 { + self.0.size.y + } + + /// Accesses the usvg tree. + pub fn tree(&self) -> &usvg::Tree { + &self.0.tree + } +} + +impl Hash for Repr { + fn hash<H: Hasher>(&self, state: &mut H) { + // An SVG might contain fonts, which must be incorporated into the hash. + // We can't hash a usvg tree directly, but the raw SVG data + a hash of + // all used fonts gives us something similar. + self.data.hash(state); + self.font_hash.hash(state); + } +} + +/// The base conversion options, to be extended with font-related options +/// because those can change across the document. +fn base_options() -> usvg::Options<'static> { + usvg::Options { + // Disable usvg's default to "Times New Roman". + font_family: String::new(), + + // We don't override the DPI here, because we already + // force the image into the corresponding DPI by setting + // the width and height. Changing the DPI only trips up + // the logic in `resvg`. + + // Override usvg's resource loading defaults. + resources_dir: None, + image_href_resolver: usvg::ImageHrefResolver { + resolve_data: usvg::ImageHrefResolver::default_data_resolver(), + resolve_string: Box::new(|_, _| None), + }, + + ..Default::default() + } +} + +/// The pixel size of an SVG. +fn tree_size(tree: &usvg::Tree) -> Axes<f64> { + Axes::new(tree.size().width() as f64, tree.size().height() as f64) +} + +/// 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), + } +} + +/// Provides Typst's fonts to usvg. +struct FontResolver<'a> { + /// Typst's font book. + book: &'a FontBook, + /// The world we use to load fonts. + world: Tracked<'a, dyn World + 'a>, + /// The active list of font families at the location of the SVG. + families: &'a [&'a str], + /// A mapping from Typst font indices to fontdb IDs. + to_id: HashMap<usize, Option<fontdb::ID>>, + /// The reverse mapping. + from_id: HashMap<fontdb::ID, Font>, + /// Accumulates a hash of all used fonts. + hasher: SipHasher13, +} + +impl<'a> FontResolver<'a> { + /// Create a new font provider. + fn new( + world: Tracked<'a, dyn World + 'a>, + book: &'a FontBook, + families: &'a [&'a str], + ) -> Self { + Self { + book, + world, + families, + to_id: HashMap::new(), + from_id: HashMap::new(), + hasher: SipHasher13::new(), + } + } + + /// Returns a hash of all used fonts. + fn finish(self) -> u128 { + self.hasher.finish128().as_u128() + } +} + +impl FontResolver<'_> { + /// Select a font. + fn select_font( + &mut self, + font: &usvg::Font, + db: &mut Arc<fontdb::Database>, + ) -> Option<fontdb::ID> { + let variant = FontVariant { + style: font.style().into(), + weight: FontWeight::from_number(font.weight()), + stretch: font.stretch().into(), + }; + + // Find a family that is available. + font.families() + .iter() + .filter_map(|family| match family { + usvg::FontFamily::Named(named) => Some(named.as_str()), + // We don't support generic families at the moment. + _ => None, + }) + .chain(self.families.iter().copied()) + .filter_map(|named| self.book.select(&named.to_lowercase(), variant)) + .find_map(|index| self.get_or_load(index, db)) + } + + /// Select a fallback font. + fn select_fallback( + &mut self, + c: char, + exclude_fonts: &[fontdb::ID], + db: &mut Arc<fontdb::Database>, + ) -> Option<fontdb::ID> { + // Get the font info of the originally selected font. + let like = exclude_fonts + .first() + .and_then(|first| self.from_id.get(first)) + .map(|font| font.info()); + + // usvg doesn't provide a variant in the fallback handler, but + // `exclude_fonts` is actually never empty in practice. Still, we + // prefer to fall back to the default variant rather than panicking + // in case that changes in the future. + let variant = like.map(|info| info.variant).unwrap_or_default(); + + // Select the font. + let index = + self.book.select_fallback(like, variant, c.encode_utf8(&mut [0; 4]))?; + + self.get_or_load(index, db) + } + + /// Tries to retrieve the ID for the index or loads the font, allocating + /// a new ID. + fn get_or_load( + &mut self, + index: usize, + db: &mut Arc<fontdb::Database>, + ) -> Option<fontdb::ID> { + self.to_id + .get(&index) + .copied() + .unwrap_or_else(|| self.load(index, db)) + } + + /// Tries to load the font with the given index in the font book into the + /// database and returns its ID. + fn load( + &mut self, + index: usize, + db: &mut Arc<fontdb::Database>, + ) -> Option<fontdb::ID> { + let font = self.world.font(index)?; + let info = font.info(); + let variant = info.variant; + let id = Arc::make_mut(db).push_face_info(fontdb::FaceInfo { + id: fontdb::ID::dummy(), + source: fontdb::Source::Binary(Arc::new(font.data().clone())), + index: font.index(), + families: vec![( + info.family.clone(), + ttf_parser::Language::English_UnitedStates, + )], + post_script_name: String::new(), + style: match variant.style { + FontStyle::Normal => fontdb::Style::Normal, + FontStyle::Italic => fontdb::Style::Italic, + FontStyle::Oblique => fontdb::Style::Oblique, + }, + weight: fontdb::Weight(variant.weight.to_number()), + stretch: match variant.stretch.round() { + FontStretch::ULTRA_CONDENSED => ttf_parser::Width::UltraCondensed, + FontStretch::EXTRA_CONDENSED => ttf_parser::Width::ExtraCondensed, + FontStretch::CONDENSED => ttf_parser::Width::Condensed, + FontStretch::SEMI_CONDENSED => ttf_parser::Width::SemiCondensed, + FontStretch::NORMAL => ttf_parser::Width::Normal, + FontStretch::SEMI_EXPANDED => ttf_parser::Width::SemiExpanded, + FontStretch::EXPANDED => ttf_parser::Width::Expanded, + FontStretch::EXTRA_EXPANDED => ttf_parser::Width::ExtraExpanded, + FontStretch::ULTRA_EXPANDED => ttf_parser::Width::UltraExpanded, + _ => unreachable!(), + }, + monospaced: info.flags.contains(FontFlags::MONOSPACE), + }); + + font.hash(&mut self.hasher); + + self.to_id.insert(index, Some(id)); + self.from_id.insert(id, font); + + Some(id) + } +} diff --git a/crates/typst-library/src/visualize/line.rs b/crates/typst-library/src/visualize/line.rs new file mode 100644 index 00000000..d9ddab84 --- /dev/null +++ b/crates/typst-library/src/visualize/line.rs @@ -0,0 +1,64 @@ +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain}; +use crate::layout::{Abs, Angle, Axes, BlockElem, Length, Rel}; +use crate::visualize::Stroke; + +/// A line from one point to another. +/// +/// # Example +/// ```example +/// #set page(height: 100pt) +/// +/// #line(length: 100%) +/// #line(end: (50%, 50%)) +/// #line( +/// length: 4cm, +/// stroke: 2pt + maroon, +/// ) +/// ``` +#[elem(Show)] +pub struct LineElem { + /// The start point of the line. + /// + /// Must be an array of exactly two relative lengths. + #[resolve] + pub start: Axes<Rel<Length>>, + + /// The offset from `start` where the line ends. + #[resolve] + pub end: Option<Axes<Rel<Length>>>, + + /// The line's length. This is only respected if `end` is `{none}`. + #[resolve] + #[default(Abs::pt(30.0).into())] + pub length: Rel<Length>, + + /// The angle at which the line points away from the origin. This is only + /// respected if `end` is `{none}`. + pub angle: Angle, + + /// How to [stroke] the line. + /// + /// ```example + /// #set line(length: 100%) + /// #stack( + /// spacing: 1em, + /// line(stroke: 2pt + red), + /// line(stroke: (paint: blue, thickness: 4pt, cap: "round")), + /// line(stroke: (paint: blue, thickness: 1pt, dash: "dashed")), + /// line(stroke: (paint: blue, thickness: 1pt, dash: ("dot", 2pt, 4pt, 2pt))), + /// ) + /// ``` + #[resolve] + #[fold] + pub stroke: Stroke, +} + +impl Show for Packed<LineElem> { + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_line) + .pack() + .spanned(self.span())) + } +} diff --git a/crates/typst-library/src/visualize/mod.rs b/crates/typst-library/src/visualize/mod.rs new file mode 100644 index 00000000..5c8bf646 --- /dev/null +++ b/crates/typst-library/src/visualize/mod.rs @@ -0,0 +1,50 @@ +//! Drawing and visualization. + +mod color; +mod gradient; +mod image; +mod line; +mod paint; +mod path; +mod pattern; +mod polygon; +mod shape; +mod stroke; + +pub use self::color::*; +pub use self::gradient::*; +pub use self::image::*; +pub use self::line::*; +pub use self::paint::*; +pub use self::path::*; +pub use self::pattern::*; +pub use self::polygon::*; +pub use self::shape::*; +pub use self::stroke::*; + +use crate::foundations::{category, Category, Scope}; + +/// Drawing and data visualization. +/// +/// If you want to create more advanced drawings or plots, also have a look at +/// the [CetZ](https://github.com/johannes-wolf/cetz) package as well as more +/// specialized [packages]($universe) for your use case. +#[category] +pub static VISUALIZE: Category; + +/// Hook up all visualize definitions. +pub(super) fn define(global: &mut Scope) { + global.category(VISUALIZE); + global.define_type::<Color>(); + global.define_type::<Gradient>(); + global.define_type::<Pattern>(); + global.define_type::<Stroke>(); + global.define_elem::<ImageElem>(); + global.define_elem::<LineElem>(); + global.define_elem::<RectElem>(); + global.define_elem::<SquareElem>(); + global.define_elem::<EllipseElem>(); + global.define_elem::<CircleElem>(); + global.define_elem::<PolygonElem>(); + global.define_elem::<PathElem>(); +} diff --git a/crates/typst-library/src/visualize/paint.rs b/crates/typst-library/src/visualize/paint.rs new file mode 100644 index 00000000..cd1006aa --- /dev/null +++ b/crates/typst-library/src/visualize/paint.rs @@ -0,0 +1,102 @@ +use std::fmt::{self, Debug, Formatter}; + +use ecow::EcoString; + +use crate::foundations::{cast, Repr, Smart}; +use crate::visualize::{Color, Gradient, Pattern, RelativeTo}; + +/// How a fill or stroke should be painted. +#[derive(Clone, Eq, PartialEq, Hash)] +pub enum Paint { + /// A solid color. + Solid(Color), + /// A gradient. + Gradient(Gradient), + /// A pattern. + Pattern(Pattern), +} + +impl Paint { + /// Unwraps a solid color used for text rendering. + pub fn unwrap_solid(&self) -> Color { + match self { + Self::Solid(color) => *color, + Self::Gradient(_) | Self::Pattern(_) => panic!("expected solid color"), + } + } + + /// Gets the relative coordinate system for this paint. + pub fn relative(&self) -> Smart<RelativeTo> { + match self { + Self::Solid(_) => Smart::Auto, + Self::Gradient(gradient) => gradient.relative(), + Self::Pattern(pattern) => pattern.relative(), + } + } + + /// Turns this paint into a paint for a text decoration. + /// + /// If this paint is a gradient, it will be converted to a gradient with + /// relative set to [`RelativeTo::Parent`]. + pub fn as_decoration(&self) -> Self { + match self { + Self::Solid(color) => Self::Solid(*color), + Self::Gradient(gradient) => { + Self::Gradient(gradient.clone().with_relative(RelativeTo::Parent)) + } + Self::Pattern(pattern) => { + Self::Pattern(pattern.clone().with_relative(RelativeTo::Parent)) + } + } + } +} + +impl Debug for Paint { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Solid(v) => v.fmt(f), + Self::Gradient(v) => v.fmt(f), + Self::Pattern(v) => v.fmt(f), + } + } +} + +impl From<Pattern> for Paint { + fn from(pattern: Pattern) -> Self { + Self::Pattern(pattern) + } +} + +impl Repr for Paint { + fn repr(&self) -> EcoString { + match self { + Self::Solid(color) => color.repr(), + Self::Gradient(gradient) => gradient.repr(), + Self::Pattern(pattern) => pattern.repr(), + } + } +} + +impl<T: Into<Color>> From<T> for Paint { + fn from(t: T) -> Self { + Self::Solid(t.into()) + } +} + +impl From<Gradient> for Paint { + fn from(gradient: Gradient) -> Self { + Self::Gradient(gradient) + } +} + +cast! { + Paint, + self => match self { + Self::Solid(color) => color.into_value(), + Self::Gradient(gradient) => gradient.into_value(), + Self::Pattern(pattern) => pattern.into_value(), + }, + color: Color => Self::Solid(color), + gradient: Gradient => Self::Gradient(gradient), + pattern: Pattern => Self::Pattern(pattern), +} diff --git a/crates/typst-library/src/visualize/path.rs b/crates/typst-library/src/visualize/path.rs new file mode 100644 index 00000000..76fd0df0 --- /dev/null +++ b/crates/typst-library/src/visualize/path.rs @@ -0,0 +1,276 @@ +use kurbo::ParamCurveExtrema; + +use self::PathVertex::{AllControlPoints, MirroredControlPoint, Vertex}; +use crate::diag::{bail, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + array, cast, elem, Array, Content, NativeElement, Packed, Reflect, Show, Smart, + StyleChain, +}; +use crate::layout::{Abs, Axes, BlockElem, Length, Point, Rel, Size}; +use crate::visualize::{FillRule, Paint, Stroke}; + +/// A path through a list of points, connected by Bezier curves. +/// +/// # Example +/// ```example +/// #path( +/// fill: blue.lighten(80%), +/// stroke: blue, +/// closed: true, +/// (0pt, 50pt), +/// (100%, 50pt), +/// ((50%, 0pt), (40pt, 0pt)), +/// ) +/// ``` +#[elem(Show)] +pub struct PathElem { + /// How to fill the path. + /// + /// When setting a fill, the default stroke disappears. To create a + /// rectangle with both fill and stroke, you have to configure both. + pub fill: Option<Paint>, + + /// The drawing rule used to fill the path. + /// + /// ```example + /// // We use `.with` to get a new + /// // function that has the common + /// // arguments pre-applied. + /// #let star = path.with( + /// fill: red, + /// closed: true, + /// (25pt, 0pt), + /// (10pt, 50pt), + /// (50pt, 20pt), + /// (0pt, 20pt), + /// (40pt, 50pt), + /// ) + /// + /// #star(fill-rule: "non-zero") + /// #star(fill-rule: "even-odd") + /// ``` + #[default] + pub fill_rule: FillRule, + + /// How to [stroke] the path. This can be: + /// + /// Can be set to `{none}` to disable the stroke or to `{auto}` for a + /// stroke of `{1pt}` black if and if only if no fill is given. + #[resolve] + #[fold] + pub stroke: Smart<Option<Stroke>>, + + /// Whether to close this path with one last bezier curve. This curve will + /// takes into account the adjacent control points. If you want to close + /// with a straight line, simply add one last point that's the same as the + /// start point. + #[default(false)] + pub closed: bool, + + /// The vertices of the path. + /// + /// Each vertex can be defined in 3 ways: + /// + /// - A regular point, as given to the [`line`] or [`polygon`] function. + /// - An array of two points, the first being the vertex and the second + /// being the control point. The control point is expressed relative to + /// the vertex and is mirrored to get the second control point. The given + /// control point is the one that affects the curve coming _into_ this + /// vertex (even for the first point). The mirrored control point affects + /// the curve going out of this vertex. + /// - An array of three points, the first being the vertex and the next + /// being the control points (control point for curves coming in and out, + /// respectively). + #[variadic] + pub vertices: Vec<PathVertex>, +} + +impl Show for Packed<PathElem> { + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_path) + .pack() + .spanned(self.span())) + } +} + +/// A component used for path creation. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum PathVertex { + Vertex(Axes<Rel<Length>>), + MirroredControlPoint(Axes<Rel<Length>>, Axes<Rel<Length>>), + AllControlPoints(Axes<Rel<Length>>, Axes<Rel<Length>>, Axes<Rel<Length>>), +} + +impl PathVertex { + pub fn vertex(&self) -> Axes<Rel<Length>> { + match self { + Vertex(x) => *x, + MirroredControlPoint(x, _) => *x, + AllControlPoints(x, _, _) => *x, + } + } + + pub fn control_point_from(&self) -> Axes<Rel<Length>> { + match self { + Vertex(_) => Axes::new(Rel::zero(), Rel::zero()), + MirroredControlPoint(_, a) => a.map(|x| -x), + AllControlPoints(_, _, b) => *b, + } + } + + pub fn control_point_to(&self) -> Axes<Rel<Length>> { + match self { + Vertex(_) => Axes::new(Rel::zero(), Rel::zero()), + MirroredControlPoint(_, a) => *a, + AllControlPoints(_, a, _) => *a, + } + } +} + +cast! { + PathVertex, + self => match self { + Vertex(x) => x.into_value(), + MirroredControlPoint(x, c) => array![x, c].into_value(), + AllControlPoints(x, c1, c2) => array![x, c1, c2].into_value(), + }, + array: Array => { + let mut iter = array.into_iter(); + match (iter.next(), iter.next(), iter.next(), iter.next()) { + (Some(a), None, None, None) => { + Vertex(a.cast()?) + }, + (Some(a), Some(b), None, None) => { + if Axes::<Rel<Length>>::castable(&a) { + MirroredControlPoint(a.cast()?, b.cast()?) + } else { + Vertex(Axes::new(a.cast()?, b.cast()?)) + } + }, + (Some(a), Some(b), Some(c), None) => { + AllControlPoints(a.cast()?, b.cast()?, c.cast()?) + }, + _ => bail!("path vertex must have 1, 2, or 3 points"), + } + }, +} + +/// 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 + } + + /// Create a path that describes an axis-aligned ellipse. + pub fn ellipse(size: Size) -> Self { + // 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)); + 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); + } + + /// Computes the size of bounding box of this path. + pub fn bbox_size(&self) -> 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(); + + let mut cursor = Point::zero(); + for item in self.0.iter() { + match item { + PathItem::MoveTo(to) => { + min_x = min_x.min(cursor.x); + min_y = min_y.min(cursor.y); + max_x = max_x.max(cursor.x); + max_y = max_y.max(cursor.y); + cursor = *to; + } + PathItem::LineTo(to) => { + min_x = min_x.min(cursor.x); + min_y = min_y.min(cursor.y); + max_x = max_x.max(cursor.x); + max_y = max_y.max(cursor.y); + cursor = *to; + } + PathItem::CubicTo(c0, c1, end) => { + let cubic = kurbo::CubicBez::new( + kurbo::Point::new(cursor.x.to_pt(), cursor.y.to_pt()), + kurbo::Point::new(c0.x.to_pt(), c0.y.to_pt()), + kurbo::Point::new(c1.x.to_pt(), c1.y.to_pt()), + kurbo::Point::new(end.x.to_pt(), end.y.to_pt()), + ); + + let bbox = cubic.bounding_box(); + min_x = min_x.min(Abs::pt(bbox.x0)).min(Abs::pt(bbox.x1)); + min_y = min_y.min(Abs::pt(bbox.y0)).min(Abs::pt(bbox.y1)); + max_x = max_x.max(Abs::pt(bbox.x0)).max(Abs::pt(bbox.x1)); + max_y = max_y.max(Abs::pt(bbox.y0)).max(Abs::pt(bbox.y1)); + cursor = *end; + } + PathItem::ClosePath => (), + } + } + + Size::new(max_x - min_x, max_y - min_y) + } +} diff --git a/crates/typst-library/src/visualize/pattern.rs b/crates/typst-library/src/visualize/pattern.rs new file mode 100644 index 00000000..2017ea65 --- /dev/null +++ b/crates/typst-library/src/visualize/pattern.rs @@ -0,0 +1,285 @@ +use std::hash::Hash; +use std::sync::Arc; + +use ecow::{eco_format, EcoString}; +use typst_syntax::{Span, Spanned}; +use typst_utils::{LazyHash, Numeric}; + +use crate::diag::{bail, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{func, repr, scope, ty, Content, Smart, StyleChain}; +use crate::introspection::Locator; +use crate::layout::{Abs, Axes, Frame, Length, Region, Size}; +use crate::visualize::RelativeTo; +use crate::World; + +/// A repeating pattern fill. +/// +/// Typst supports the most common pattern type of tiled patterns, where a +/// pattern is repeated in a grid-like fashion, covering the entire area of an +/// element that is filled or stroked. The pattern is defined by a tile size and +/// a body defining the content of each cell. You can also add horizontal or +/// vertical spacing between the cells of the pattern. +/// +/// # Examples +/// +/// ```example +/// #let pat = pattern(size: (30pt, 30pt))[ +/// #place(line(start: (0%, 0%), end: (100%, 100%))) +/// #place(line(start: (0%, 100%), end: (100%, 0%))) +/// ] +/// +/// #rect(fill: pat, width: 100%, height: 60pt, stroke: 1pt) +/// ``` +/// +/// Patterns are also supported on text, but only when setting the +/// [relativeness]($pattern.relative) to either `{auto}` (the default value) or +/// `{"parent"}`. To create word-by-word or glyph-by-glyph patterns, you can +/// wrap the words or characters of your text in [boxes]($box) manually or +/// through a [show rule]($styling/#show-rules). +/// +/// ```example +/// #let pat = pattern( +/// size: (30pt, 30pt), +/// relative: "parent", +/// square( +/// size: 30pt, +/// fill: gradient +/// .conic(..color.map.rainbow), +/// ) +/// ) +/// +/// #set text(fill: pat) +/// #lorem(10) +/// ``` +/// +/// You can also space the elements further or closer apart using the +/// [`spacing`]($pattern.spacing) feature of the pattern. If the spacing +/// is lower than the size of the pattern, the pattern will overlap. +/// If it is higher, the pattern will have gaps of the same color as the +/// background of the pattern. +/// +/// ```example +/// #let pat = pattern( +/// size: (30pt, 30pt), +/// spacing: (10pt, 10pt), +/// relative: "parent", +/// square( +/// size: 30pt, +/// fill: gradient +/// .conic(..color.map.rainbow), +/// ), +/// ) +/// +/// #rect( +/// width: 100%, +/// height: 60pt, +/// fill: pat, +/// ) +/// ``` +/// +/// # Relativeness +/// The location of the starting point of the pattern is dependent on the +/// dimensions of a container. This container can either be the shape that it is +/// being painted on, or the closest surrounding container. This is controlled +/// by the `relative` argument of a pattern constructor. By default, patterns +/// are relative to the shape they are being painted on, unless the pattern is +/// applied on text, in which case they are relative to the closest ancestor +/// container. +/// +/// Typst determines the ancestor container as follows: +/// - For shapes that are placed at the root/top level of the document, the +/// closest ancestor is the page itself. +/// - For other shapes, the ancestor is the innermost [`block`] or [`box`] that +/// contains the shape. This includes the boxes and blocks that are implicitly +/// created by show rules and elements. For example, a [`rotate`] will not +/// affect the parent of a gradient, but a [`grid`] will. +#[ty(scope, cast)] +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Pattern(Arc<Repr>); + +/// Internal representation of [`Pattern`]. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +struct Repr { + /// The pattern's rendered content. + frame: LazyHash<Frame>, + /// The pattern's tile size. + size: Size, + /// The pattern's tile spacing. + spacing: Size, + /// The pattern's relative transform. + relative: Smart<RelativeTo>, +} + +#[scope] +impl Pattern { + /// Construct a new pattern. + /// + /// ```example + /// #let pat = pattern( + /// size: (20pt, 20pt), + /// relative: "parent", + /// place( + /// dx: 5pt, + /// dy: 5pt, + /// rotate(45deg, square( + /// size: 5pt, + /// fill: black, + /// )), + /// ), + /// ) + /// + /// #rect(width: 100%, height: 60pt, fill: pat) + /// ``` + #[func(constructor)] + pub fn construct( + engine: &mut Engine, + /// The callsite span. + span: Span, + /// The bounding box of each cell of the pattern. + #[named] + #[default(Spanned::new(Smart::Auto, Span::detached()))] + size: Spanned<Smart<Axes<Length>>>, + /// The spacing between cells of the pattern. + #[named] + #[default(Spanned::new(Axes::splat(Length::zero()), Span::detached()))] + spacing: Spanned<Axes<Length>>, + /// The [relative placement](#relativeness) of the pattern. + /// + /// For an element placed at the root/top level of the document, the + /// parent is the page itself. For other elements, the parent is the + /// innermost block, box, column, grid, or stack that contains the + /// element. + #[named] + #[default(Smart::Auto)] + relative: Smart<RelativeTo>, + /// The content of each cell of the pattern. + body: Content, + ) -> SourceResult<Pattern> { + let size_span = size.span; + if let Smart::Custom(size) = size.v { + // Ensure that sizes are absolute. + if !size.x.em.is_zero() || !size.y.em.is_zero() { + bail!(size_span, "pattern tile size must be absolute"); + } + + // Ensure that sizes are non-zero and finite. + if size.x.is_zero() + || size.y.is_zero() + || !size.x.is_finite() + || !size.y.is_finite() + { + bail!(size_span, "pattern tile size must be non-zero and non-infinite"); + } + } + + // Ensure that spacing is absolute. + if !spacing.v.x.em.is_zero() || !spacing.v.y.em.is_zero() { + bail!(spacing.span, "pattern tile spacing must be absolute"); + } + + // Ensure that spacing is finite. + if !spacing.v.x.is_finite() || !spacing.v.y.is_finite() { + bail!(spacing.span, "pattern tile spacing must be finite"); + } + + // The size of the frame + let size = size.v.map(|l| l.map(|a| a.abs)); + let region = size.unwrap_or_else(|| Axes::splat(Abs::inf())); + + // Layout the pattern. + let world = engine.world; + let library = world.library(); + let locator = Locator::root(); + let styles = StyleChain::new(&library.styles); + let pod = Region::new(region, Axes::splat(false)); + let mut frame = + (engine.routines.layout_frame)(engine, &body, locator, styles, pod)?; + + // Set the size of the frame if the size is enforced. + if let Smart::Custom(size) = size { + frame.set_size(size); + } + + // Check that the frame is non-zero. + if frame.width().is_zero() || frame.height().is_zero() { + bail!( + span, "pattern tile size must be non-zero"; + hint: "try setting the size manually" + ); + } + + Ok(Self(Arc::new(Repr { + size: frame.size(), + frame: LazyHash::new(frame), + spacing: spacing.v.map(|l| l.abs), + relative, + }))) + } +} + +impl Pattern { + /// Set the relative placement of the pattern. + pub fn with_relative(mut self, relative: RelativeTo) -> Self { + if let Some(this) = Arc::get_mut(&mut self.0) { + this.relative = Smart::Custom(relative); + } else { + self.0 = Arc::new(Repr { + relative: Smart::Custom(relative), + ..self.0.as_ref().clone() + }); + } + + self + } + + /// Return the frame of the pattern. + pub fn frame(&self) -> &Frame { + &self.0.frame + } + + /// Return the size of the pattern in absolute units. + pub fn size(&self) -> Size { + self.0.size + } + + /// Return the spacing of the pattern in absolute units. + pub fn spacing(&self) -> Size { + self.0.spacing + } + + /// Returns the relative placement of the pattern. + pub fn relative(&self) -> Smart<RelativeTo> { + self.0.relative + } + + /// Returns the relative placement of the pattern. + pub fn unwrap_relative(&self, on_text: bool) -> RelativeTo { + self.0.relative.unwrap_or_else(|| { + if on_text { + RelativeTo::Parent + } else { + RelativeTo::Self_ + } + }) + } +} + +impl repr::Repr for Pattern { + fn repr(&self) -> EcoString { + let mut out = + eco_format!("pattern(({}, {})", self.0.size.x.repr(), self.0.size.y.repr()); + + if self.0.spacing.is_zero() { + out.push_str(", spacing: ("); + out.push_str(&self.0.spacing.x.repr()); + out.push_str(", "); + out.push_str(&self.0.spacing.y.repr()); + out.push(')'); + } + + out.push_str(", ..)"); + + out + } +} diff --git a/crates/typst-library/src/visualize/polygon.rs b/crates/typst-library/src/visualize/polygon.rs new file mode 100644 index 00000000..33e4fd32 --- /dev/null +++ b/crates/typst-library/src/visualize/polygon.rs @@ -0,0 +1,135 @@ +use std::f64::consts::PI; + +use typst_syntax::Span; + +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{ + elem, func, scope, Content, NativeElement, Packed, Show, Smart, StyleChain, +}; +use crate::layout::{Axes, BlockElem, Em, Length, Rel}; +use crate::visualize::{FillRule, Paint, Stroke}; + +/// A closed polygon. +/// +/// The polygon is defined by its corner points and is closed automatically. +/// +/// # Example +/// ```example +/// #polygon( +/// fill: blue.lighten(80%), +/// stroke: blue, +/// (20%, 0pt), +/// (60%, 0pt), +/// (80%, 2cm), +/// (0%, 2cm), +/// ) +/// ``` +#[elem(scope, Show)] +pub struct PolygonElem { + /// How to fill the polygon. + /// + /// When setting a fill, the default stroke disappears. To create a + /// rectangle with both fill and stroke, you have to configure both. + pub fill: Option<Paint>, + + /// The drawing rule used to fill the polygon. + /// + /// See the [path documentation]($path.fill-rule) for an example. + #[default] + pub fill_rule: FillRule, + + /// How to [stroke] the polygon. This can be: + /// + /// Can be set to `{none}` to disable the stroke or to `{auto}` for a + /// stroke of `{1pt}` black if and if only if no fill is given. + #[resolve] + #[fold] + pub stroke: Smart<Option<Stroke>>, + + /// The vertices of the polygon. Each point is specified as an array of two + /// [relative lengths]($relative). + #[variadic] + pub vertices: Vec<Axes<Rel<Length>>>, +} + +#[scope] +impl PolygonElem { + /// A regular polygon, defined by its size and number of vertices. + /// + /// ```example + /// #polygon.regular( + /// fill: blue.lighten(80%), + /// stroke: blue, + /// size: 30pt, + /// vertices: 3, + /// ) + /// ``` + #[func(title = "Regular Polygon")] + pub fn regular( + /// The call span of this function. + span: Span, + /// How to fill the polygon. See the general + /// [polygon's documentation]($polygon.fill) for more details. + #[named] + fill: Option<Option<Paint>>, + + /// How to stroke the polygon. See the general + /// [polygon's documentation]($polygon.stroke) for more details. + #[named] + stroke: Option<Smart<Option<Stroke>>>, + + /// The diameter of the [circumcircle](https://en.wikipedia.org/wiki/Circumcircle) + /// of the regular polygon. + #[named] + #[default(Em::one().into())] + size: Length, + + /// The number of vertices in the polygon. + #[named] + #[default(3)] + vertices: u64, + ) -> Content { + let radius = size / 2.0; + let angle = |i: f64| { + 2.0 * PI * i / (vertices as f64) + PI * (1.0 / 2.0 - 1.0 / vertices as f64) + }; + let (horizontal_offset, vertical_offset) = (0..=vertices) + .map(|v| { + ( + (radius * angle(v as f64).cos()) + radius, + (radius * angle(v as f64).sin()) + radius, + ) + }) + .fold((radius, radius), |(min_x, min_y), (v_x, v_y)| { + ( + if min_x < v_x { min_x } else { v_x }, + if min_y < v_y { min_y } else { v_y }, + ) + }); + let vertices = (0..=vertices) + .map(|v| { + let x = (radius * angle(v as f64).cos()) + radius - horizontal_offset; + let y = (radius * angle(v as f64).sin()) + radius - vertical_offset; + Axes::new(x, y).map(Rel::from) + }) + .collect(); + + let mut elem = PolygonElem::new(vertices); + if let Some(fill) = fill { + elem.push_fill(fill); + } + if let Some(stroke) = stroke { + elem.push_stroke(stroke); + } + elem.pack().spanned(span) + } +} + +impl Show for Packed<PolygonElem> { + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_polygon) + .pack() + .spanned(self.span())) + } +} diff --git a/crates/typst-library/src/visualize/shape.rs b/crates/typst-library/src/visualize/shape.rs new file mode 100644 index 00000000..01e316a8 --- /dev/null +++ b/crates/typst-library/src/visualize/shape.rs @@ -0,0 +1,448 @@ +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{ + elem, Cast, Content, NativeElement, Packed, Show, Smart, StyleChain, +}; +use crate::layout::{Abs, BlockElem, Corners, Length, Point, Rel, Sides, Size, Sizing}; +use crate::visualize::{FixedStroke, Paint, Path, Stroke}; + +/// A rectangle with optional content. +/// +/// # Example +/// ```example +/// // Without content. +/// #rect(width: 35%, height: 30pt) +/// +/// // With content. +/// #rect[ +/// Automatically sized \ +/// to fit the content. +/// ] +/// ``` +#[elem(title = "Rectangle", Show)] +pub struct RectElem { + /// The rectangle's width, relative to its parent container. + pub width: Smart<Rel<Length>>, + + /// The rectangle's height, relative to its parent container. + pub height: Sizing, + + /// How to fill the rectangle. + /// + /// When setting a fill, the default stroke disappears. To create a + /// rectangle with both fill and stroke, you have to configure both. + /// + /// ```example + /// #rect(fill: blue) + /// ``` + pub fill: Option<Paint>, + + /// How to stroke the rectangle. This can be: + /// + /// - `{none}` to disable stroking + /// - `{auto}` for a stroke of `{1pt + black}` if and if only if no fill is + /// given. + /// - Any kind of [stroke] + /// - A dictionary describing the stroke for each side individually. The + /// dictionary can contain the following keys in order of precedence: + /// - `top`: The top stroke. + /// - `right`: The right stroke. + /// - `bottom`: The bottom stroke. + /// - `left`: The left stroke. + /// - `x`: The horizontal stroke. + /// - `y`: The vertical stroke. + /// - `rest`: The stroke on all sides except those for which the + /// dictionary explicitly sets a size. + /// + /// ```example + /// #stack( + /// dir: ltr, + /// spacing: 1fr, + /// rect(stroke: red), + /// rect(stroke: 2pt), + /// rect(stroke: 2pt + red), + /// ) + /// ``` + #[resolve] + #[fold] + pub stroke: Smart<Sides<Option<Option<Stroke>>>>, + + /// How much to round the rectangle's corners, relative to the minimum of + /// the width and height divided by two. This can be: + /// + /// - A relative length for a uniform corner radius. + /// - A dictionary: With a dictionary, the stroke for each side can be set + /// individually. The dictionary can contain the following keys in order + /// of precedence: + /// - `top-left`: The top-left corner radius. + /// - `top-right`: The top-right corner radius. + /// - `bottom-right`: The bottom-right corner radius. + /// - `bottom-left`: The bottom-left corner radius. + /// - `left`: The top-left and bottom-left corner radii. + /// - `top`: The top-left and top-right corner radii. + /// - `right`: The top-right and bottom-right corner radii. + /// - `bottom`: The bottom-left and bottom-right corner radii. + /// - `rest`: The radii for all corners except those for which the + /// dictionary explicitly sets a size. + /// + /// ```example + /// #set rect(stroke: 4pt) + /// #rect( + /// radius: ( + /// left: 5pt, + /// top-right: 20pt, + /// bottom-right: 10pt, + /// ), + /// stroke: ( + /// left: red, + /// top: yellow, + /// right: green, + /// bottom: blue, + /// ), + /// ) + /// ``` + #[resolve] + #[fold] + pub radius: Corners<Option<Rel<Length>>>, + + /// How much to pad the rectangle's content. + /// See the [box's documentation]($box.outset) for more details. + #[resolve] + #[fold] + #[default(Sides::splat(Some(Abs::pt(5.0).into())))] + pub inset: Sides<Option<Rel<Length>>>, + + /// How much to expand the rectangle's size without affecting the layout. + /// See the [box's documentation]($box.outset) for more details. + #[resolve] + #[fold] + pub outset: Sides<Option<Rel<Length>>>, + + /// The content to place into the rectangle. + /// + /// When this is omitted, the rectangle takes on a default size of at most + /// `{45pt}` by `{30pt}`. + #[positional] + #[borrowed] + pub body: Option<Content>, +} + +impl Show for Packed<RectElem> { + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_rect) + .with_width(self.width(styles)) + .with_height(self.height(styles)) + .pack() + .spanned(self.span())) + } +} + +/// A square with optional content. +/// +/// # Example +/// ```example +/// // Without content. +/// #square(size: 40pt) +/// +/// // With content. +/// #square[ +/// Automatically \ +/// sized to fit. +/// ] +/// ``` +#[elem(Show)] +pub struct SquareElem { + /// The square's side length. This is mutually exclusive with `width` and + /// `height`. + #[external] + pub size: Smart<Length>, + + /// The square's width. This is mutually exclusive with `size` and `height`. + /// + /// In contrast to `size`, this can be relative to the parent container's + /// width. + #[parse( + let size = args.named::<Smart<Length>>("size")?.map(|s| s.map(Rel::from)); + match size { + None => args.named("width")?, + size => size, + } + )] + pub width: Smart<Rel<Length>>, + + /// The square's height. This is mutually exclusive with `size` and `width`. + /// + /// In contrast to `size`, this can be relative to the parent container's + /// height. + #[parse(match size { + None => args.named("height")?, + size => size.map(Into::into), + })] + pub height: Sizing, + + /// How to fill the square. See the [rectangle's documentation]($rect.fill) + /// for more details. + pub fill: Option<Paint>, + + /// How to stroke the square. See the + /// [rectangle's documentation]($rect.stroke) for more details. + #[resolve] + #[fold] + pub stroke: Smart<Sides<Option<Option<Stroke>>>>, + + /// How much to round the square's corners. See the + /// [rectangle's documentation]($rect.radius) for more details. + #[resolve] + #[fold] + pub radius: Corners<Option<Rel<Length>>>, + + /// How much to pad the square's content. See the + /// [box's documentation]($box.inset) for more details. + #[resolve] + #[fold] + #[default(Sides::splat(Some(Abs::pt(5.0).into())))] + pub inset: Sides<Option<Rel<Length>>>, + + /// How much to expand the square's size without affecting the layout. See + /// the [box's documentation]($box.outset) for more details. + #[resolve] + #[fold] + pub outset: Sides<Option<Rel<Length>>>, + + /// The content to place into the square. The square expands to fit this + /// content, keeping the 1-1 aspect ratio. + /// + /// When this is omitted, the square takes on a default size of at most + /// `{30pt}`. + #[positional] + #[borrowed] + pub body: Option<Content>, +} + +impl Show for Packed<SquareElem> { + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_square) + .with_width(self.width(styles)) + .with_height(self.height(styles)) + .pack() + .spanned(self.span())) + } +} + +/// An ellipse with optional content. +/// +/// # Example +/// ```example +/// // Without content. +/// #ellipse(width: 35%, height: 30pt) +/// +/// // With content. +/// #ellipse[ +/// #set align(center) +/// Automatically sized \ +/// to fit the content. +/// ] +/// ``` +#[elem(Show)] +pub struct EllipseElem { + /// The ellipse's width, relative to its parent container. + pub width: Smart<Rel<Length>>, + + /// The ellipse's height, relative to its parent container. + pub height: Sizing, + + /// How to fill the ellipse. See the [rectangle's documentation]($rect.fill) + /// for more details. + pub fill: Option<Paint>, + + /// How to stroke the ellipse. See the + /// [rectangle's documentation]($rect.stroke) for more details. + #[resolve] + #[fold] + pub stroke: Smart<Option<Stroke>>, + + /// How much to pad the ellipse's content. See the + /// [box's documentation]($box.inset) for more details. + #[resolve] + #[fold] + #[default(Sides::splat(Some(Abs::pt(5.0).into())))] + pub inset: Sides<Option<Rel<Length>>>, + + /// How much to expand the ellipse's size without affecting the layout. See + /// the [box's documentation]($box.outset) for more details. + #[resolve] + #[fold] + pub outset: Sides<Option<Rel<Length>>>, + + /// The content to place into the ellipse. + /// + /// When this is omitted, the ellipse takes on a default size of at most + /// `{45pt}` by `{30pt}`. + #[positional] + #[borrowed] + pub body: Option<Content>, +} + +impl Show for Packed<EllipseElem> { + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_ellipse) + .with_width(self.width(styles)) + .with_height(self.height(styles)) + .pack() + .spanned(self.span())) + } +} + +/// A circle with optional content. +/// +/// # Example +/// ```example +/// // Without content. +/// #circle(radius: 25pt) +/// +/// // With content. +/// #circle[ +/// #set align(center + horizon) +/// Automatically \ +/// sized to fit. +/// ] +/// ``` +#[elem(Show)] +pub struct CircleElem { + /// The circle's radius. This is mutually exclusive with `width` and + /// `height`. + #[external] + pub radius: Length, + + /// The circle's width. This is mutually exclusive with `radius` and + /// `height`. + /// + /// In contrast to `radius`, this can be relative to the parent container's + /// width. + #[parse( + let size = args + .named::<Smart<Length>>("radius")? + .map(|s| s.map(|r| 2.0 * Rel::from(r))); + match size { + None => args.named("width")?, + size => size, + } + )] + pub width: Smart<Rel<Length>>, + + /// The circle's height. This is mutually exclusive with `radius` and + /// `width`. + /// + /// In contrast to `radius`, this can be relative to the parent container's + /// height. + #[parse(match size { + None => args.named("height")?, + size => size.map(Into::into), + })] + pub height: Sizing, + + /// How to fill the circle. See the [rectangle's documentation]($rect.fill) + /// for more details. + pub fill: Option<Paint>, + + /// How to stroke the circle. See the + /// [rectangle's documentation]($rect.stroke) for more details. + #[resolve] + #[fold] + #[default(Smart::Auto)] + pub stroke: Smart<Option<Stroke>>, + + /// How much to pad the circle's content. See the + /// [box's documentation]($box.inset) for more details. + #[resolve] + #[fold] + #[default(Sides::splat(Some(Abs::pt(5.0).into())))] + pub inset: Sides<Option<Rel<Length>>>, + + /// How much to expand the circle's size without affecting the layout. See + /// the [box's documentation]($box.outset) for more details. + #[resolve] + #[fold] + pub outset: Sides<Option<Rel<Length>>>, + + /// The content to place into the circle. The circle expands to fit this + /// content, keeping the 1-1 aspect ratio. + #[positional] + #[borrowed] + pub body: Option<Content>, +} + +impl Show for Packed<CircleElem> { + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_circle) + .with_width(self.width(styles)) + .with_height(self.height(styles)) + .pack() + .spanned(self.span())) + } +} + +/// 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 fill rule. + pub fill_rule: FillRule, + /// The shape's border stroke. + pub stroke: Option<FixedStroke>, +} + +/// A path filling rule. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum FillRule { + /// Specifies that "inside" is computed by a non-zero sum of signed edge crossings. + #[default] + NonZero, + /// Specifies that "inside" is computed by an odd number of edge crossings. + EvenOdd, +} + +/// 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: impl Into<Paint>) -> Shape { + Shape { + geometry: self, + fill: Some(fill.into()), + fill_rule: FillRule::default(), + stroke: None, + } + } + + /// Stroke the geometry without a fill. + pub fn stroked(self, stroke: FixedStroke) -> Shape { + Shape { + geometry: self, + fill: None, + fill_rule: FillRule::default(), + stroke: Some(stroke), + } + } + + /// The bounding box of the geometry. + pub fn bbox_size(&self) -> Size { + match self { + Self::Line(line) => Size::new(line.x, line.y), + Self::Rect(s) => *s, + Self::Path(p) => p.bbox_size(), + } + } +} diff --git a/crates/typst-library/src/visualize/stroke.rs b/crates/typst-library/src/visualize/stroke.rs new file mode 100644 index 00000000..4ca10920 --- /dev/null +++ b/crates/typst-library/src/visualize/stroke.rs @@ -0,0 +1,617 @@ +use ecow::EcoString; +use typst_utils::{Numeric, Scalar}; + +use crate::diag::{HintedStrResult, SourceResult}; +use crate::foundations::{ + cast, dict, func, scope, ty, Args, Cast, Dict, Fold, FromValue, NoneValue, Repr, + Resolve, Smart, StyleChain, Value, +}; +use crate::layout::{Abs, Length}; +use crate::visualize::{Color, Gradient, Paint, Pattern}; + +/// Defines how to draw a line. +/// +/// A stroke has a _paint_ (a solid color or gradient), a _thickness,_ a line +/// _cap,_ a line _join,_ a _miter limit,_ and a _dash_ pattern. All of these +/// values are optional and have sensible defaults. +/// +/// # Example +/// ```example +/// #set line(length: 100%) +/// #stack( +/// spacing: 1em, +/// line(stroke: 2pt + red), +/// line(stroke: (paint: blue, thickness: 4pt, cap: "round")), +/// line(stroke: (paint: blue, thickness: 1pt, dash: "dashed")), +/// line(stroke: 2pt + gradient.linear(..color.map.rainbow)), +/// ) +/// ``` +/// +/// # Simple strokes +/// You can create a simple solid stroke from a color, a thickness, or a +/// combination of the two. Specifically, wherever a stroke is expected you can +/// pass any of the following values: +/// +/// - A length specifying the stroke's thickness. The color is inherited, +/// defaulting to black. +/// - A color to use for the stroke. The thickness is inherited, defaulting to +/// `{1pt}`. +/// - A stroke combined from color and thickness using the `+` operator as in +/// `{2pt + red}`. +/// +/// For full control, you can also provide a [dictionary] or a `{stroke}` object +/// to any function that expects a stroke. The dictionary's keys may include any +/// of the parameters for the constructor function, shown below. +/// +/// # Fields +/// On a stroke object, you can access any of the fields listed in the +/// constructor function. For example, `{(2pt + blue).thickness}` is `{2pt}`. +/// Meanwhile, `{stroke(red).cap}` is `{auto}` because it's unspecified. Fields +/// set to `{auto}` are inherited. +#[ty(scope, cast)] +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct Stroke<T: Numeric = Length> { + /// The stroke's paint. + pub paint: Smart<Paint>, + /// The stroke's thickness. + pub thickness: Smart<T>, + /// The stroke's line cap. + pub cap: Smart<LineCap>, + /// The stroke's line join. + pub join: Smart<LineJoin>, + /// The stroke's line dash pattern. + pub dash: Smart<Option<DashPattern<T>>>, + /// The miter limit. + pub miter_limit: Smart<Scalar>, +} + +impl Stroke { + /// Create a stroke from a paint and a thickness. + pub fn from_pair(paint: impl Into<Paint>, thickness: Length) -> Self { + Self { + paint: Smart::Custom(paint.into()), + thickness: Smart::Custom(thickness), + ..Default::default() + } + } +} + +#[scope] +impl Stroke { + /// Converts a value to a stroke or constructs a stroke with the given + /// parameters. + /// + /// Note that in most cases you do not need to convert values to strokes in + /// order to use them, as they will be converted automatically. However, + /// this constructor can be useful to ensure a value has all the fields of a + /// stroke. + /// + /// ```example + /// #let my-func(x) = { + /// x = stroke(x) // Convert to a stroke + /// [Stroke has thickness #x.thickness.] + /// } + /// #my-func(3pt) \ + /// #my-func(red) \ + /// #my-func(stroke(cap: "round", thickness: 1pt)) + /// ``` + #[func(constructor)] + pub fn construct( + /// The real arguments (the other arguments are just for the docs, this + /// function is a bit involved, so we parse the arguments manually). + args: &mut Args, + + /// The color or gradient to use for the stroke. + /// + /// If set to `{auto}`, the value is inherited, defaulting to `{black}`. + #[external] + paint: Smart<Paint>, + + /// The stroke's thickness. + /// + /// If set to `{auto}`, the value is inherited, defaulting to `{1pt}`. + #[external] + thickness: Smart<Length>, + + /// How the ends of the stroke are rendered. + /// + /// If set to `{auto}`, the value is inherited, defaulting to `{"butt"}`. + #[external] + cap: Smart<LineCap>, + + /// How sharp turns are rendered. + /// + /// If set to `{auto}`, the value is inherited, defaulting to `{"miter"}`. + #[external] + join: Smart<LineJoin>, + + /// The dash pattern to use. This can be: + /// + /// - One of the predefined patterns: + /// - `{"solid"}` or `{none}` + /// - `{"dotted"}` + /// - `{"densely-dotted"}` + /// - `{"loosely-dotted"}` + /// - `{"dashed"}` + /// - `{"densely-dashed"}` + /// - `{"loosely-dashed"}` + /// - `{"dash-dotted"}` + /// - `{"densely-dash-dotted"}` + /// - `{"loosely-dash-dotted"}` + /// - An [array] with alternating lengths for dashes and gaps. You can + /// also use the string `{"dot"}` for a length equal to the line + /// thickness. + /// - A [dictionary] with the keys `array` (same as the array above), + /// and `phase` (of type [length]), which defines where in the pattern + /// to start drawing. + /// + /// If set to `{auto}`, the value is inherited, defaulting to `{none}`. + /// + /// ```example + /// #set line(length: 100%, stroke: 2pt) + /// #stack( + /// spacing: 1em, + /// line(stroke: (dash: "dashed")), + /// line(stroke: (dash: (10pt, 5pt, "dot", 5pt))), + /// line(stroke: (dash: (array: (10pt, 5pt, "dot", 5pt), phase: 10pt))), + /// ) + /// ``` + #[external] + dash: Smart<Option<DashPattern>>, + + /// Number at which protruding sharp bends are rendered with a bevel + /// instead or a miter join. The higher the number, the sharper an angle + /// can be before it is bevelled. Only applicable if `join` is + /// `{"miter"}`. + /// + /// Specifically, the miter limit is the maximum ratio between the + /// corner's protrusion length and the stroke's thickness. + /// + /// If set to `{auto}`, the value is inherited, defaulting to `{4.0}`. + /// + /// ```example + /// #let points = ((15pt, 0pt), (0pt, 30pt), (30pt, 30pt), (10pt, 20pt)) + /// #set path(stroke: 6pt + blue) + /// #stack( + /// dir: ltr, + /// spacing: 1cm, + /// path(stroke: (miter-limit: 1), ..points), + /// path(stroke: (miter-limit: 4), ..points), + /// path(stroke: (miter-limit: 5), ..points), + /// ) + /// ``` + #[external] + miter_limit: Smart<f64>, + ) -> SourceResult<Stroke> { + if let Some(stroke) = args.eat::<Stroke>()? { + return Ok(stroke); + } + + fn take<T: FromValue>(args: &mut Args, arg: &str) -> SourceResult<Smart<T>> { + Ok(args.named::<Smart<T>>(arg)?.unwrap_or(Smart::Auto)) + } + + let paint = take::<Paint>(args, "paint")?; + let thickness = take::<Length>(args, "thickness")?; + let cap = take::<LineCap>(args, "cap")?; + let join = take::<LineJoin>(args, "join")?; + let dash = take::<Option<DashPattern>>(args, "dash")?; + let miter_limit = take::<f64>(args, "miter-limit")?.map(Scalar::new); + + Ok(Self { paint, thickness, cap, join, dash, miter_limit }) + } +} + +impl<T: Numeric> Stroke<T> { + /// Map the contained lengths with `f`. + pub fn map<F, U: Numeric>(self, f: F) -> Stroke<U> + where + F: Fn(T) -> U, + { + Stroke { + paint: self.paint, + thickness: self.thickness.map(&f), + cap: self.cap, + join: self.join, + dash: self.dash.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 Stroke<Abs> { + /// Unpack the stroke, filling missing fields from the `default`. + pub fn unwrap_or(self, default: FixedStroke) -> FixedStroke { + let thickness = self.thickness.unwrap_or(default.thickness); + let dash = self + .dash + .map(|pattern| { + pattern.map(|pattern| DashPattern { + array: pattern + .array + .into_iter() + .map(|l| l.finish(thickness)) + .collect(), + phase: pattern.phase, + }) + }) + .unwrap_or(default.dash); + + FixedStroke { + paint: self.paint.unwrap_or(default.paint), + thickness, + cap: self.cap.unwrap_or(default.cap), + join: self.join.unwrap_or(default.join), + dash, + 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) -> FixedStroke { + // we want to do this; the Clippy lint is not type-aware + #[allow(clippy::unwrap_or_default)] + self.unwrap_or(FixedStroke::default()) + } +} + +impl<T: Numeric + Repr> Repr for Stroke<T> { + fn repr(&self) -> EcoString { + let mut r = EcoString::new(); + let Self { paint, thickness, cap, join, dash, miter_limit } = &self; + if cap.is_auto() && join.is_auto() && dash.is_auto() && miter_limit.is_auto() { + match (&self.paint, &self.thickness) { + (Smart::Custom(paint), Smart::Custom(thickness)) => { + r.push_str(&thickness.repr()); + r.push_str(" + "); + r.push_str(&paint.repr()); + } + (Smart::Custom(paint), Smart::Auto) => r.push_str(&paint.repr()), + (Smart::Auto, Smart::Custom(thickness)) => r.push_str(&thickness.repr()), + (Smart::Auto, Smart::Auto) => r.push_str("1pt + black"), + } + } else { + r.push('('); + let mut sep = ""; + if let Smart::Custom(paint) = &paint { + r.push_str(sep); + r.push_str("paint: "); + r.push_str(&paint.repr()); + sep = ", "; + } + if let Smart::Custom(thickness) = &thickness { + r.push_str(sep); + r.push_str("thickness: "); + r.push_str(&thickness.repr()); + sep = ", "; + } + if let Smart::Custom(cap) = &cap { + r.push_str(sep); + r.push_str("cap: "); + r.push_str(&cap.repr()); + sep = ", "; + } + if let Smart::Custom(join) = &join { + r.push_str(sep); + r.push_str("join: "); + r.push_str(&join.repr()); + sep = ", "; + } + if let Smart::Custom(dash) = &dash { + r.push_str(sep); + r.push_str("dash: "); + if let Some(dash) = dash { + r.push_str(&dash.repr()); + } else { + r.push_str(&NoneValue.repr()); + } + sep = ", "; + } + if let Smart::Custom(miter_limit) = &miter_limit { + r.push_str(sep); + r.push_str("miter-limit: "); + r.push_str(&miter_limit.get().repr()); + } + r.push(')'); + } + r + } +} + +impl<T: Numeric + Fold> Fold for Stroke<T> { + fn fold(self, outer: Self) -> Self { + Self { + paint: self.paint.or(outer.paint), + thickness: self.thickness.or(outer.thickness), + cap: self.cap.or(outer.cap), + join: self.join.or(outer.join), + dash: self.dash.or(outer.dash), + miter_limit: self.miter_limit.or(outer.miter_limit), + } + } +} + +impl Resolve for Stroke { + type Output = Stroke<Abs>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + Stroke { + paint: self.paint, + thickness: self.thickness.resolve(styles), + cap: self.cap, + join: self.join, + dash: self.dash.resolve(styles), + miter_limit: self.miter_limit, + } + } +} + +cast! { + type Stroke, + thickness: Length => Self { + thickness: Smart::Custom(thickness), + ..Default::default() + }, + color: Color => Self { + paint: Smart::Custom(color.into()), + ..Default::default() + }, + gradient: Gradient => Self { + paint: Smart::Custom(gradient.into()), + ..Default::default() + }, + pattern: Pattern => Self { + paint: Smart::Custom(pattern.into()), + ..Default::default() + }, + mut dict: Dict => { + // Get a value by key, accepting either Auto or something convertible to type T. + fn take<T: FromValue>(dict: &mut Dict, key: &str) -> HintedStrResult<Smart<T>> { + Ok(dict.take(key).ok().map(Smart::<T>::from_value) + .transpose()?.unwrap_or(Smart::Auto)) + } + + let paint = take::<Paint>(&mut dict, "paint")?; + let thickness = take::<Length>(&mut dict, "thickness")?; + let cap = take::<LineCap>(&mut dict, "cap")?; + let join = take::<LineJoin>(&mut dict, "join")?; + let dash = 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, + cap, + join, + dash, + miter_limit: miter_limit.map(Scalar::new), + } + }, +} + +cast! { + Stroke<Abs>, + self => self.map(Length::from).into_value(), +} + +/// The line cap of a stroke +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum LineCap { + /// Square stroke cap with the edge at the stroke's end point. + Butt, + /// Circular stroke cap centered at the stroke's end point. + Round, + /// Square stroke cap centered at the stroke's end point. + Square, +} + +impl Repr for LineCap { + fn repr(&self) -> EcoString { + match self { + Self::Butt => "butt".repr(), + Self::Round => "round".repr(), + Self::Square => "square".repr(), + } + } +} + +/// The line join of a stroke +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum LineJoin { + /// Segments are joined with sharp edges. Sharp bends exceeding the miter + /// limit are bevelled instead. + Miter, + /// Segments are joined with circular corners. + Round, + /// Segments are joined with a bevel (a straight edge connecting the butts + /// of the joined segments). + Bevel, +} + +impl Repr for LineJoin { + fn repr(&self) -> EcoString { + match self { + Self::Miter => "miter".repr(), + Self::Round => "round".repr(), + Self::Bevel => "bevel".repr(), + } + } +} + +/// A line dash pattern. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct DashPattern<T: Numeric = Length, DT = DashLength<T>> { + /// The dash array. + pub array: Vec<DT>, + /// The dash phase. + pub phase: T, +} + +impl<T: Numeric + Repr, DT: Repr> Repr for DashPattern<T, DT> { + fn repr(&self) -> EcoString { + let mut r = EcoString::from("(array: ("); + for (i, elem) in self.array.iter().enumerate() { + if i != 0 { + r.push_str(", ") + } + r.push_str(&elem.repr()) + } + r.push_str("), phase: "); + r.push_str(&self.phase.repr()); + r.push(')'); + r + } +} + +impl<T: Numeric + 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, + self => dict! { "array" => self.array, "phase" => self.phase }.into_value(), + + "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: Numeric = Length> { + LineWidth, + Length(T), +} + +impl<T: Numeric> DashLength<T> { + fn finish(self, line_width: T) -> T { + match self { + Self::LineWidth => line_width, + Self::Length(l) => l, + } + } +} + +impl<T: Numeric + Repr> Repr for DashLength<T> { + fn repr(&self) -> EcoString { + match self { + Self::LineWidth => "dot".repr(), + Self::Length(v) => v.repr(), + } + } +} + +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)), + } + } +} + +impl From<Abs> for DashLength { + fn from(l: Abs) -> Self { + DashLength::Length(l.into()) + } +} + +cast! { + DashLength, + self => match self { + Self::LineWidth => "dot".into_value(), + Self::Length(v) => v.into_value(), + }, + "dot" => Self::LineWidth, + v: Length => Self::Length(v), +} + +/// A fully specified stroke of a geometric shape. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct FixedStroke { + /// The stroke's paint. + pub paint: Paint, + /// The stroke's thickness. + pub thickness: Abs, + /// The stroke's line cap. + pub cap: LineCap, + /// The stroke's line join. + pub join: LineJoin, + /// The stroke's line dash pattern. + pub dash: Option<DashPattern<Abs, Abs>>, + /// The miter limit. Defaults to 4.0, same as `tiny-skia`. + pub miter_limit: Scalar, +} + +impl FixedStroke { + /// Create a stroke from a paint and a thickness. + pub fn from_pair(paint: impl Into<Paint>, thickness: Abs) -> Self { + Self { + paint: paint.into(), + thickness, + ..Default::default() + } + } +} + +impl Default for FixedStroke { + fn default() -> Self { + Self { + paint: Paint::Solid(Color::BLACK), + thickness: Abs::pt(1.0), + cap: LineCap::Butt, + join: LineJoin::Miter, + dash: None, + miter_limit: Scalar::new(4.0), + } + } +} |
