diff options
| author | Laurenz <laurmaedje@gmail.com> | 2023-07-02 19:59:52 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2023-07-02 20:07:43 +0200 |
| commit | ebfdb1dafa430786db10dad2ef7d5467c1bdbed1 (patch) | |
| tree | 2bbc24ddb4124c4bb14dec0e536129d4de37b056 /crates | |
| parent | 3ab19185093d7709f824b95b979060ce125389d8 (diff) | |
Move everything into `crates/` directory
Diffstat (limited to 'crates')
190 files changed, 58787 insertions, 0 deletions
diff --git a/crates/typst-cli/Cargo.toml b/crates/typst-cli/Cargo.toml new file mode 100644 index 00000000..e77cbc2a --- /dev/null +++ b/crates/typst-cli/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "typst-cli" +description = "The command line interface for Typst." +categories = ["compilers", "command-line-utilities"] +keywords = ["typst", "cli"] +version.workspace = true +rust-version.workspace = true +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true + +[[bin]] +name = "typst" +path = "src/main.rs" +test = false +doctest = false +bench = false +doc = false + +[dependencies] +typst = { path = "../typst" } +typst-library = { path = "../typst-library" } +chrono = { version = "0.4.24", default-features = false, features = ["clock", "std"] } +clap = { version = "4.2.4", features = ["derive", "env"] } +codespan-reporting = "0.11" +comemo = "0.3" +dirs = "5" +flate2 = "1" +inferno = "0.11.15" +memmap2 = "0.5" +notify = "5" +once_cell = "1" +open = "4.0.2" +same-file = "1" +siphasher = "0.3" +tar = "0.4" +tempfile = "3.5.0" +tracing = "0.1.37" +tracing-error = "0.2" +tracing-flame = "0.2.0" +tracing-subscriber = "0.3.17" +ureq = "2" +walkdir = "2" + +[build-dependencies] +clap = { version = "4.2.4", features = ["derive", "string"] } +clap_complete = "4.2.1" +clap_mangen = "0.2.10" + +[features] +default = ["embed-fonts"] + +# Embeds some fonts into the binary: +# - For text: Linux Libertine, New Computer Modern +# - For math: New Computer Modern Math +# - For code: Deja Vu Sans Mono +embed-fonts = [] diff --git a/crates/typst-cli/build.rs b/crates/typst-cli/build.rs new file mode 100644 index 00000000..86325e1d --- /dev/null +++ b/crates/typst-cli/build.rs @@ -0,0 +1,60 @@ +use std::env; +use std::fs::{create_dir_all, File}; +use std::path::Path; +use std::process::Command; + +use clap::{CommandFactory, ValueEnum}; +use clap_complete::{generate_to, Shell}; +use clap_mangen::Man; + +#[path = "src/args.rs"] +#[allow(dead_code)] +mod args; + +fn main() { + println!("cargo:rerun-if-env-changed=TYPST_VERSION"); + println!("cargo:rerun-if-env-changed=GEN_ARTIFACTS"); + + if option_env!("TYPST_VERSION").is_none() { + println!("cargo:rustc-env=TYPST_VERSION={}", typst_version()); + } + + if let Some(dir) = env::var_os("GEN_ARTIFACTS") { + let out = &Path::new(&dir); + create_dir_all(out).unwrap(); + let cmd = &mut args::CliArguments::command(); + + Man::new(cmd.clone()) + .render(&mut File::create(out.join("typst.1")).unwrap()) + .unwrap(); + + for subcmd in cmd.get_subcommands() { + let name = format!("typst-{}", subcmd.get_name()); + Man::new(subcmd.clone().name(&name)) + .render(&mut File::create(out.join(format!("{name}.1"))).unwrap()) + .unwrap(); + } + + for shell in Shell::value_variants() { + generate_to(*shell, cmd, "typst", out).unwrap(); + } + } +} + +/// Also used by `args.rs`. +fn typst_version() -> String { + if let Some(version) = option_env!("TYPST_VERSION") { + return version.to_owned(); + } + + let pkg = env!("CARGO_PKG_VERSION"); + let hash = Command::new("git") + .args(["rev-parse", "HEAD"]) + .output() + .ok() + .filter(|output| output.status.success()) + .and_then(|output| String::from_utf8(output.stdout.get(..8)?.into()).ok()) + .unwrap_or_else(|| "unknown hash".into()); + + format!("{pkg} ({hash})") +} diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs new file mode 100644 index 00000000..7fdb041b --- /dev/null +++ b/crates/typst-cli/src/args.rs @@ -0,0 +1,119 @@ +use std::fmt::{self, Display, Formatter}; +use std::path::PathBuf; + +use clap::{ArgAction, Parser, Subcommand, ValueEnum}; + +/// typst creates PDF files from .typ files +#[derive(Debug, Clone, Parser)] +#[clap(name = "typst", version = crate::typst_version(), author)] +pub struct CliArguments { + /// The typst command to run + #[command(subcommand)] + pub command: Command, + + /// Sets the level of logging verbosity: + /// -v = warning & error, -vv = info, -vvv = debug, -vvvv = trace + #[clap(short, long, action = ArgAction::Count)] + pub verbosity: u8, +} + +/// What to do. +#[derive(Debug, Clone, Subcommand)] +#[command()] +pub enum Command { + /// Compiles the input file into a PDF file + #[command(visible_alias = "c")] + Compile(CompileCommand), + + /// Watches the input file and recompiles on changes + #[command(visible_alias = "w")] + Watch(CompileCommand), + + /// List all discovered fonts in system and custom font paths + Fonts(FontsCommand), +} + +/// Compiles the input file into a PDF file +#[derive(Debug, Clone, Parser)] +pub struct CompileCommand { + /// Path to input Typst file + pub input: PathBuf, + + /// Path to output PDF file or PNG file(s) + pub output: Option<PathBuf>, + + /// Configure the project root + #[clap(long = "root", env = "TYPST_ROOT", value_name = "DIR")] + pub root: Option<PathBuf>, + + /// Add additional directories to search for fonts + #[clap( + long = "font-path", + env = "TYPST_FONT_PATHS", + value_name = "DIR", + action = ArgAction::Append, + )] + pub font_paths: Vec<PathBuf>, + + /// Opens the output file after compilation using the default PDF viewer + #[arg(long = "open")] + pub open: Option<Option<String>>, + + /// The PPI to use if exported as PNG + #[arg(long = "ppi", default_value_t = 144.0)] + pub ppi: f32, + + /// In which format to emit diagnostics + #[clap( + long, + default_value_t = DiagnosticFormat::Human, + value_parser = clap::value_parser!(DiagnosticFormat) + )] + pub diagnostic_format: DiagnosticFormat, + + /// Produces a flamegraph of the compilation process + #[arg(long = "flamegraph", value_name = "OUTPUT_SVG")] + pub flamegraph: Option<Option<PathBuf>>, +} + +impl CompileCommand { + /// The output path. + pub fn output(&self) -> PathBuf { + self.output + .clone() + .unwrap_or_else(|| self.input.with_extension("pdf")) + } +} + +/// List all discovered fonts in system and custom font paths +#[derive(Debug, Clone, Parser)] +pub struct FontsCommand { + /// Add additional directories to search for fonts + #[clap( + long = "font-path", + env = "TYPST_FONT_PATHS", + value_name = "DIR", + action = ArgAction::Append, + )] + pub font_paths: Vec<PathBuf>, + + /// Also list style variants of each font family + #[arg(long)] + pub variants: bool, +} + +/// Which format to use for diagnostics. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)] +pub enum DiagnosticFormat { + Human, + Short, +} + +impl Display for DiagnosticFormat { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.to_possible_value() + .expect("no values are skipped") + .get_name() + .fmt(f) + } +} diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs new file mode 100644 index 00000000..3250202b --- /dev/null +++ b/crates/typst-cli/src/compile.rs @@ -0,0 +1,239 @@ +use std::fs; +use std::path::Path; + +use codespan_reporting::diagnostic::{Diagnostic, Label}; +use codespan_reporting::term::{self, termcolor}; +use termcolor::{ColorChoice, StandardStream}; +use typst::diag::{bail, SourceError, StrResult}; +use typst::doc::Document; +use typst::eval::eco_format; +use typst::file::FileId; +use typst::geom::Color; +use typst::syntax::Source; +use typst::World; + +use crate::args::{CompileCommand, DiagnosticFormat}; +use crate::watch::Status; +use crate::world::SystemWorld; +use crate::{color_stream, set_failed}; + +type CodespanResult<T> = Result<T, CodespanError>; +type CodespanError = codespan_reporting::files::Error; + +/// Execute a compilation command. +pub fn compile(mut command: CompileCommand) -> StrResult<()> { + let mut world = SystemWorld::new(&command)?; + compile_once(&mut world, &mut command, false)?; + Ok(()) +} + +/// Compile a single time. +/// +/// Returns whether it compiled without errors. +#[tracing::instrument(skip_all)] +pub fn compile_once( + world: &mut SystemWorld, + command: &mut CompileCommand, + watching: bool, +) -> StrResult<()> { + tracing::info!("Starting compilation"); + + let start = std::time::Instant::now(); + if watching { + Status::Compiling.print(command).unwrap(); + } + + // Reset everything and ensure that the main file is still present. + world.reset(); + world.source(world.main()).map_err(|err| err.to_string())?; + + let result = typst::compile(world); + let duration = start.elapsed(); + + match result { + // Export the PDF / PNG. + Ok(document) => { + export(&document, command)?; + + tracing::info!("Compilation succeeded in {duration:?}"); + if watching { + Status::Success(duration).print(command).unwrap(); + } + + if let Some(open) = command.open.take() { + open_file(open.as_deref(), &command.output())?; + } + } + + // Print diagnostics. + Err(errors) => { + set_failed(); + tracing::info!("Compilation failed"); + + if watching { + Status::Error.print(command).unwrap(); + } + + print_diagnostics(world, *errors, command.diagnostic_format) + .map_err(|_| "failed to print diagnostics")?; + } + } + + Ok(()) +} + +/// Export into the target format. +fn export(document: &Document, command: &CompileCommand) -> StrResult<()> { + match command.output().extension() { + Some(ext) if ext.eq_ignore_ascii_case("png") => export_png(document, command), + _ => export_pdf(document, command), + } +} + +/// Export to a PDF. +fn export_pdf(document: &Document, command: &CompileCommand) -> StrResult<()> { + let output = command.output(); + let buffer = typst::export::pdf(document); + fs::write(output, buffer).map_err(|_| "failed to write PDF file")?; + Ok(()) +} + +/// Export to one or multiple PNGs. +fn export_png(document: &Document, command: &CompileCommand) -> StrResult<()> { + // Determine whether we have a `{n}` numbering. + let output = command.output(); + let string = output.to_str().unwrap_or_default(); + let numbered = string.contains("{n}"); + if !numbered && document.pages.len() > 1 { + bail!("cannot export multiple PNGs without `{{n}}` in output path"); + } + + // Find a number width that accommodates all pages. For instance, the + // first page should be numbered "001" if there are between 100 and + // 999 pages. + let width = 1 + document.pages.len().checked_ilog10().unwrap_or(0) as usize; + let mut storage; + + for (i, frame) in document.pages.iter().enumerate() { + let pixmap = typst::export::render(frame, command.ppi / 72.0, Color::WHITE); + let path = if numbered { + storage = string.replace("{n}", &format!("{:0width$}", i + 1)); + Path::new(&storage) + } else { + output.as_path() + }; + pixmap.save_png(path).map_err(|_| "failed to write PNG file")?; + } + + Ok(()) +} + +/// Opens the given file using: +/// - The default file viewer if `open` is `None`. +/// - The given viewer provided by `open` if it is `Some`. +fn open_file(open: Option<&str>, path: &Path) -> StrResult<()> { + if let Some(app) = open { + open::with_in_background(path, app); + } else { + open::that_in_background(path); + } + + Ok(()) +} + +/// Print diagnostic messages to the terminal. +fn print_diagnostics( + world: &SystemWorld, + errors: Vec<SourceError>, + diagnostic_format: DiagnosticFormat, +) -> Result<(), codespan_reporting::files::Error> { + let mut w = match diagnostic_format { + DiagnosticFormat::Human => color_stream(), + DiagnosticFormat::Short => StandardStream::stderr(ColorChoice::Never), + }; + + let mut config = term::Config { tab_width: 2, ..Default::default() }; + if diagnostic_format == DiagnosticFormat::Short { + config.display_style = term::DisplayStyle::Short; + } + + for error in errors { + // The main diagnostic. + let diag = Diagnostic::error() + .with_message(error.message) + .with_notes( + error + .hints + .iter() + .map(|e| (eco_format!("hint: {e}")).into()) + .collect(), + ) + .with_labels(vec![Label::primary(error.span.id(), error.span.range(world))]); + + term::emit(&mut w, &config, world, &diag)?; + + // Stacktrace-like helper diagnostics. + for point in error.trace { + let message = point.v.to_string(); + let help = Diagnostic::help().with_message(message).with_labels(vec![ + Label::primary(point.span.id(), point.span.range(world)), + ]); + + term::emit(&mut w, &config, world, &help)?; + } + } + + Ok(()) +} + +impl<'a> codespan_reporting::files::Files<'a> for SystemWorld { + type FileId = FileId; + type Name = FileId; + type Source = Source; + + fn name(&'a self, id: FileId) -> CodespanResult<Self::Name> { + Ok(id) + } + + fn source(&'a self, id: FileId) -> CodespanResult<Self::Source> { + Ok(self.lookup(id)) + } + + fn line_index(&'a self, id: FileId, given: usize) -> CodespanResult<usize> { + let source = self.lookup(id); + source + .byte_to_line(given) + .ok_or_else(|| CodespanError::IndexTooLarge { + given, + max: source.len_bytes(), + }) + } + + fn line_range( + &'a self, + id: FileId, + given: usize, + ) -> CodespanResult<std::ops::Range<usize>> { + let source = self.lookup(id); + source + .line_to_range(given) + .ok_or_else(|| CodespanError::LineTooLarge { given, max: source.len_lines() }) + } + + fn column_number( + &'a self, + id: FileId, + _: usize, + given: usize, + ) -> CodespanResult<usize> { + let source = self.lookup(id); + source.byte_to_column(given).ok_or_else(|| { + let max = source.len_bytes(); + if given <= max { + CodespanError::InvalidCharBoundary { given } + } else { + CodespanError::IndexTooLarge { given, max } + } + }) + } +} diff --git a/crates/typst-cli/src/fonts.rs b/crates/typst-cli/src/fonts.rs new file mode 100644 index 00000000..d8209fee --- /dev/null +++ b/crates/typst-cli/src/fonts.rs @@ -0,0 +1,183 @@ +use std::cell::OnceCell; +use std::env; +use std::fs::{self, File}; +use std::path::{Path, PathBuf}; + +use memmap2::Mmap; +use typst::diag::StrResult; +use typst::font::{Font, FontBook, FontInfo, FontVariant}; +use typst::util::Bytes; +use walkdir::WalkDir; + +use crate::args::FontsCommand; + +/// Execute a font listing command. +pub fn fonts(command: FontsCommand) -> StrResult<()> { + let mut searcher = FontSearcher::new(); + searcher.search(&command.font_paths); + + for (name, infos) in searcher.book.families() { + println!("{name}"); + if command.variants { + for info in infos { + let FontVariant { style, weight, stretch } = info.variant; + println!("- Style: {style:?}, Weight: {weight:?}, Stretch: {stretch:?}"); + } + } + } + + Ok(()) +} + +/// Searches for fonts. +pub struct FontSearcher { + /// Metadata about all discovered fonts. + pub book: FontBook, + /// Slots that the fonts are loaded into. + pub fonts: Vec<FontSlot>, +} + +/// Holds details about the location of a font and lazily the font itself. +pub struct FontSlot { + /// The path at which the font can be found on the system. + path: PathBuf, + /// The index of the font in its collection. Zero if the path does not point + /// to a collection. + index: u32, + /// The lazily loaded font. + font: OnceCell<Option<Font>>, +} + +impl FontSlot { + /// Get the font for this slot. + pub fn get(&self) -> Option<Font> { + self.font + .get_or_init(|| { + let data = fs::read(&self.path).ok()?.into(); + Font::new(data, self.index) + }) + .clone() + } +} + +impl FontSearcher { + /// Create a new, empty system searcher. + pub fn new() -> Self { + Self { book: FontBook::new(), fonts: vec![] } + } + + /// Search everything that is available. + pub fn search(&mut self, font_paths: &[PathBuf]) { + self.search_system(); + + #[cfg(feature = "embed-fonts")] + self.add_embedded(); + + for path in font_paths { + self.search_dir(path) + } + } + + /// Add fonts that are embedded in the binary. + #[cfg(feature = "embed-fonts")] + fn add_embedded(&mut self) { + let mut process = |bytes: &'static [u8]| { + let buffer = Bytes::from_static(bytes); + for (i, font) in Font::iter(buffer).enumerate() { + self.book.push(font.info().clone()); + self.fonts.push(FontSlot { + path: PathBuf::new(), + index: i as u32, + font: OnceCell::from(Some(font)), + }); + } + }; + + macro_rules! add { + ($filename:literal) => { + process(include_bytes!(concat!("../../../assets/fonts/", $filename))); + }; + } + + // Embed default fonts. + add!("LinLibertine_R.ttf"); + add!("LinLibertine_RB.ttf"); + add!("LinLibertine_RBI.ttf"); + add!("LinLibertine_RI.ttf"); + add!("NewCMMath-Book.otf"); + add!("NewCMMath-Regular.otf"); + add!("NewCM10-Regular.otf"); + add!("NewCM10-Bold.otf"); + add!("NewCM10-Italic.otf"); + add!("NewCM10-BoldItalic.otf"); + add!("DejaVuSansMono.ttf"); + add!("DejaVuSansMono-Bold.ttf"); + add!("DejaVuSansMono-Oblique.ttf"); + add!("DejaVuSansMono-BoldOblique.ttf"); + } + + /// Search for fonts in the linux system font directories. + fn search_system(&mut self) { + if cfg!(target_os = "macos") { + self.search_dir("/Library/Fonts"); + self.search_dir("/Network/Library/Fonts"); + self.search_dir("/System/Library/Fonts"); + } else if cfg!(unix) { + self.search_dir("/usr/share/fonts"); + self.search_dir("/usr/local/share/fonts"); + } else if cfg!(windows) { + self.search_dir( + env::var_os("WINDIR") + .map(PathBuf::from) + .unwrap_or_else(|| "C:\\Windows".into()) + .join("Fonts"), + ); + + if let Some(roaming) = dirs::config_dir() { + self.search_dir(roaming.join("Microsoft\\Windows\\Fonts")); + } + + if let Some(local) = dirs::cache_dir() { + self.search_dir(local.join("Microsoft\\Windows\\Fonts")); + } + } + + if let Some(dir) = dirs::font_dir() { + self.search_dir(dir); + } + } + + /// Search for all fonts in a directory recursively. + fn search_dir(&mut self, path: impl AsRef<Path>) { + for entry in WalkDir::new(path) + .follow_links(true) + .sort_by(|a, b| a.file_name().cmp(b.file_name())) + .into_iter() + .filter_map(|e| e.ok()) + { + let path = entry.path(); + if matches!( + path.extension().and_then(|s| s.to_str()), + Some("ttf" | "otf" | "TTF" | "OTF" | "ttc" | "otc" | "TTC" | "OTC"), + ) { + self.search_file(path); + } + } + } + + /// Index the fonts in the file at the given path. + fn search_file(&mut self, path: &Path) { + if let Ok(file) = File::open(path) { + if let Ok(mmap) = unsafe { Mmap::map(&file) } { + for (i, info) in FontInfo::iter(&mmap).enumerate() { + self.book.push(info); + self.fonts.push(FontSlot { + path: path.into(), + index: i as u32, + font: OnceCell::new(), + }); + } + } + } + } +} diff --git a/crates/typst-cli/src/main.rs b/crates/typst-cli/src/main.rs new file mode 100644 index 00000000..425d05fd --- /dev/null +++ b/crates/typst-cli/src/main.rs @@ -0,0 +1,79 @@ +mod args; +mod compile; +mod fonts; +mod package; +mod tracing; +mod watch; +mod world; + +use std::cell::Cell; +use std::env; +use std::io::{self, IsTerminal, Write}; +use std::process::ExitCode; + +use clap::Parser; +use codespan_reporting::term::{self, termcolor}; +use termcolor::{ColorChoice, WriteColor}; + +use crate::args::{CliArguments, Command}; + +thread_local! { + /// The CLI's exit code. + static EXIT: Cell<ExitCode> = Cell::new(ExitCode::SUCCESS); +} + +/// Entry point. +fn main() -> ExitCode { + let arguments = CliArguments::parse(); + let _guard = match crate::tracing::setup_tracing(&arguments) { + Ok(guard) => guard, + Err(err) => { + eprintln!("failed to initialize tracing {}", err); + None + } + }; + + let res = match arguments.command { + Command::Compile(command) => crate::compile::compile(command), + Command::Watch(command) => crate::watch::watch(command), + Command::Fonts(command) => crate::fonts::fonts(command), + }; + + if let Err(msg) = res { + set_failed(); + print_error(&msg).expect("failed to print error"); + } + + EXIT.with(|cell| cell.get()) +} + +/// Ensure a failure exit code. +fn set_failed() { + EXIT.with(|cell| cell.set(ExitCode::FAILURE)); +} + +/// Print an application-level error (independent from a source file). +fn print_error(msg: &str) -> io::Result<()> { + let mut w = color_stream(); + let styles = term::Styles::default(); + + w.set_color(&styles.header_error)?; + write!(w, "error")?; + + w.reset()?; + writeln!(w, ": {msg}.") +} + +/// Get stderr with color support if desirable. +fn color_stream() -> termcolor::StandardStream { + termcolor::StandardStream::stderr(if std::io::stderr().is_terminal() { + ColorChoice::Auto + } else { + ColorChoice::Never + }) +} + +/// Used by `args.rs`. +fn typst_version() -> &'static str { + env!("TYPST_VERSION") +} diff --git a/crates/typst-cli/src/package.rs b/crates/typst-cli/src/package.rs new file mode 100644 index 00000000..6853796b --- /dev/null +++ b/crates/typst-cli/src/package.rs @@ -0,0 +1,77 @@ +use std::fs; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; + +use codespan_reporting::term::{self, termcolor}; +use termcolor::WriteColor; +use typst::diag::{PackageError, PackageResult}; +use typst::file::PackageSpec; + +use super::color_stream; + +/// Make a package available in the on-disk cache. +pub fn prepare_package(spec: &PackageSpec) -> PackageResult<PathBuf> { + let subdir = + format!("typst/packages/{}/{}-{}", spec.namespace, spec.name, spec.version); + + if let Some(data_dir) = dirs::data_dir() { + let dir = data_dir.join(&subdir); + if dir.exists() { + return Ok(dir); + } + } + + if let Some(cache_dir) = dirs::cache_dir() { + let dir = cache_dir.join(&subdir); + + // Download from network if it doesn't exist yet. + if spec.namespace == "preview" && !dir.exists() { + download_package(spec, &dir)?; + } + + if dir.exists() { + return Ok(dir); + } + } + + Err(PackageError::NotFound(spec.clone())) +} + +/// Download a package over the network. +fn download_package(spec: &PackageSpec, package_dir: &Path) -> PackageResult<()> { + // The `@preview` namespace is the only namespace that supports on-demand + // fetching. + assert_eq!(spec.namespace, "preview"); + + let url = format!( + "https://packages.typst.org/preview/{}-{}.tar.gz", + spec.name, spec.version + ); + + print_downloading(spec).unwrap(); + let reader = match ureq::get(&url).call() { + Ok(response) => response.into_reader(), + Err(ureq::Error::Status(404, _)) => { + return Err(PackageError::NotFound(spec.clone())) + } + Err(_) => return Err(PackageError::NetworkFailed), + }; + + let decompressed = flate2::read::GzDecoder::new(reader); + tar::Archive::new(decompressed).unpack(package_dir).map_err(|_| { + fs::remove_dir_all(package_dir).ok(); + PackageError::MalformedArchive + }) +} + +/// Print that a package downloading is happening. +fn print_downloading(spec: &PackageSpec) -> io::Result<()> { + let mut w = color_stream(); + let styles = term::Styles::default(); + + w.set_color(&styles.header_help)?; + write!(w, "downloading")?; + + w.reset()?; + writeln!(w, " {spec}") +} diff --git a/crates/typst-cli/src/tracing.rs b/crates/typst-cli/src/tracing.rs new file mode 100644 index 00000000..80c2ff65 --- /dev/null +++ b/crates/typst-cli/src/tracing.rs @@ -0,0 +1,138 @@ +use std::fs::File; +use std::io::{self, BufReader, BufWriter, Seek, SeekFrom}; +use std::path::PathBuf; + +use inferno::flamegraph::Options; +use tracing::metadata::LevelFilter; +use tracing_error::ErrorLayer; +use tracing_flame::{FlameLayer, FlushGuard}; +use tracing_subscriber::fmt; +use tracing_subscriber::prelude::*; + +use crate::args::{CliArguments, Command}; + +/// Initializes the tracing system and returns a guard that will flush the +/// flamegraph to disk when dropped. +pub fn setup_tracing(args: &CliArguments) -> io::Result<Option<impl Drop>> { + let flamegraph = match &args.command { + Command::Compile(command) => command.flamegraph.as_ref(), + Command::Watch(command) if command.flamegraph.is_some() => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "cannot use --flamegraph with watch command", + )); + } + _ => None, + }; + + // Short circuit if we don't need to initialize flamegraph or debugging. + if flamegraph.is_none() && args.verbosity == 0 { + tracing_subscriber::fmt() + .without_time() + .with_max_level(level_filter(args)) + .init(); + + return Ok(None); + } + + // Build the FMT layer printing to the console. + let fmt_layer = fmt::Layer::default().without_time().with_filter(level_filter(args)); + + // Error layer for building backtraces + let error_layer = ErrorLayer::default(); + + // Build the registry. + let registry = tracing_subscriber::registry().with(fmt_layer).with(error_layer); + + let Some(path) = flamegraph else { + registry.init(); + return Ok(None); + }; + + // Create a temporary file to store the flamegraph data. + let temp_file = tempfile::tempfile()?; + let writer = BufWriter::new(temp_file.try_clone()?); + + // Build the flamegraph layer. + let flame_layer = FlameLayer::new(writer) + .with_empty_samples(false) + .with_threads_collapsed(true) + .with_module_path(false) + .with_file_and_line(true); + let flush_guard = flame_layer.flush_on_drop(); + + // Build the subscriber. + registry.with(flame_layer).init(); + + tracing::warn!( + "Flamegraph is enabled, this can create a large temporary \ + file and slow down the compilation process." + ); + + Ok(Some(TracingGuard { + flush_guard: Some(flush_guard), + temp_file, + output_svg: path.clone().unwrap_or_else(|| "flamegraph.svg".into()), + })) +} + +/// Returns the log level filter for the given verbosity level. +fn level_filter(args: &CliArguments) -> LevelFilter { + match args.verbosity { + 0 => LevelFilter::OFF, + 1 => LevelFilter::WARN, + 2 => LevelFilter::INFO, + 3 => LevelFilter::DEBUG, + _ => LevelFilter::TRACE, + } +} + +/// Will flush the flamegraph to disk when dropped. +struct TracingGuard { + flush_guard: Option<FlushGuard<BufWriter<File>>>, + temp_file: File, + output_svg: PathBuf, +} + +impl TracingGuard { + fn finish(&mut self) -> io::Result<()> { + if self.flush_guard.is_none() { + return Ok(()); + } + + tracing::info!("Flushing tracing flamegraph..."); + + // At this point, we're done tracing, so we can drop the guard. + // This will flush the tracing output to disk. + // We can then read the file and generate the flamegraph. + drop(self.flush_guard.take()); + + // Reset the file pointer to the beginning. + self.temp_file.seek(SeekFrom::Start(0))?; + + // Create the readers and writers. + let reader = BufReader::new(&mut self.temp_file); + let output = BufWriter::new(File::create(&self.output_svg)?); + + // Create the options: default in flame chart mode + let mut options = Options::default(); + options.flame_chart = true; + + inferno::flamegraph::from_reader(&mut options, reader, output) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + + Ok(()) + } +} + +impl Drop for TracingGuard { + fn drop(&mut self) { + if !std::thread::panicking() { + if let Err(e) = self.finish() { + // Since we are finished, we cannot rely on tracing to log the + // error. + eprintln!("failed to flush tracing flamegraph: {e}"); + } + } + } +} diff --git a/crates/typst-cli/src/watch.rs b/crates/typst-cli/src/watch.rs new file mode 100644 index 00000000..2ad73f85 --- /dev/null +++ b/crates/typst-cli/src/watch.rs @@ -0,0 +1,170 @@ +use std::collections::HashSet; +use std::io::{self, IsTerminal, Write}; +use std::path::{Path, PathBuf}; + +use codespan_reporting::term::{self, termcolor}; +use notify::{RecommendedWatcher, RecursiveMode, Watcher}; +use same_file::is_same_file; +use termcolor::WriteColor; +use typst::diag::StrResult; +use typst::eval::eco_format; + +use crate::args::CompileCommand; +use crate::color_stream; +use crate::compile::compile_once; +use crate::world::SystemWorld; + +/// Execute a watching compilation command. +pub fn watch(mut command: CompileCommand) -> StrResult<()> { + // Create the world that serves sources, files, and fonts. + let mut world = SystemWorld::new(&command)?; + + // Perform initial compilation. + compile_once(&mut world, &mut command, true)?; + + // Setup file watching. + let (tx, rx) = std::sync::mpsc::channel(); + let mut watcher = RecommendedWatcher::new(tx, notify::Config::default()) + .map_err(|_| "failed to setup file watching")?; + + // Watch all the files that are used by the input file and its dependencies. + watch_dependencies(&mut world, &mut watcher, HashSet::new())?; + + // Handle events. + let timeout = std::time::Duration::from_millis(100); + let output = command.output(); + loop { + let mut recompile = false; + for event in rx + .recv() + .into_iter() + .chain(std::iter::from_fn(|| rx.recv_timeout(timeout).ok())) + { + let event = event.map_err(|_| "failed to watch directory")?; + recompile |= is_event_relevant(&event, &output); + } + + if recompile { + // Retrieve the dependencies of the last compilation. + let previous: HashSet<PathBuf> = + world.dependencies().map(ToOwned::to_owned).collect(); + + // Recompile. + compile_once(&mut world, &mut command, true)?; + comemo::evict(10); + + // Adjust the watching. + watch_dependencies(&mut world, &mut watcher, previous)?; + } + } +} + +/// Adjust the file watching. Watches all new dependencies and unwatches +/// all `previous` dependencies that are not relevant anymore. +#[tracing::instrument(skip_all)] +fn watch_dependencies( + world: &mut SystemWorld, + watcher: &mut dyn Watcher, + mut previous: HashSet<PathBuf>, +) -> StrResult<()> { + // Watch new paths that weren't watched yet. + for path in world.dependencies() { + let watched = previous.remove(path); + if path.exists() && !watched { + tracing::info!("Watching {}", path.display()); + watcher + .watch(path, RecursiveMode::NonRecursive) + .map_err(|_| eco_format!("failed to watch {path:?}"))?; + } + } + + // Unwatch old paths that don't need to be watched anymore. + for path in previous { + tracing::info!("Unwatching {}", path.display()); + watcher.unwatch(&path).ok(); + } + + Ok(()) +} + +/// Whether a watch event is relevant for compilation. +fn is_event_relevant(event: ¬ify::Event, output: &Path) -> bool { + // Never recompile because the output file changed. + if event + .paths + .iter() + .all(|path| is_same_file(path, output).unwrap_or(false)) + { + return false; + } + + match &event.kind { + notify::EventKind::Any => true, + notify::EventKind::Access(_) => false, + notify::EventKind::Create(_) => true, + notify::EventKind::Modify(kind) => match kind { + notify::event::ModifyKind::Any => true, + notify::event::ModifyKind::Data(_) => true, + notify::event::ModifyKind::Metadata(_) => false, + notify::event::ModifyKind::Name(_) => true, + notify::event::ModifyKind::Other => false, + }, + notify::EventKind::Remove(_) => true, + notify::EventKind::Other => false, + } +} + +/// The status in which the watcher can be. +pub enum Status { + Compiling, + Success(std::time::Duration), + Error, +} + +impl Status { + /// Clear the terminal and render the status message. + pub fn print(&self, command: &CompileCommand) -> io::Result<()> { + let output = command.output(); + let timestamp = chrono::offset::Local::now().format("%H:%M:%S"); + let color = self.color(); + + let mut w = color_stream(); + if std::io::stderr().is_terminal() { + // Clear the terminal. + let esc = 27 as char; + write!(w, "{esc}c{esc}[1;1H")?; + } + + w.set_color(&color)?; + write!(w, "watching")?; + w.reset()?; + writeln!(w, " {}", command.input.display())?; + + w.set_color(&color)?; + write!(w, "writing to")?; + w.reset()?; + writeln!(w, " {}", output.display())?; + + writeln!(w)?; + writeln!(w, "[{timestamp}] {}", self.message())?; + writeln!(w)?; + + w.flush() + } + + fn message(&self) -> String { + match self { + Self::Compiling => "compiling ...".into(), + Self::Success(duration) => format!("compiled successfully in {duration:.2?}"), + Self::Error => "compiled with errors".into(), + } + } + + fn color(&self) -> termcolor::ColorSpec { + let styles = term::Styles::default(); + match self { + Self::Error => styles.header_error, + _ => styles.header_note, + } + } +} diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs new file mode 100644 index 00000000..f09a3f6c --- /dev/null +++ b/crates/typst-cli/src/world.rs @@ -0,0 +1,256 @@ +use std::cell::{OnceCell, RefCell, RefMut}; +use std::collections::HashMap; +use std::fs; +use std::hash::Hash; +use std::path::{Path, PathBuf}; + +use chrono::Datelike; +use comemo::Prehashed; +use same_file::Handle; +use siphasher::sip128::{Hasher128, SipHasher13}; +use typst::diag::{FileError, FileResult, StrResult}; +use typst::eval::{eco_format, Datetime, Library}; +use typst::file::FileId; +use typst::font::{Font, FontBook}; +use typst::syntax::Source; +use typst::util::{Bytes, PathExt}; +use typst::World; + +use crate::args::CompileCommand; +use crate::fonts::{FontSearcher, FontSlot}; +use crate::package::prepare_package; + +/// A world that provides access to the operating system. +pub struct SystemWorld { + /// The root relative to which absolute paths are resolved. + root: PathBuf, + /// The input path. + main: FileId, + /// Typst's standard library. + library: Prehashed<Library>, + /// Metadata about discovered fonts. + book: Prehashed<FontBook>, + /// Locations of and storage for lazily loaded fonts. + fonts: Vec<FontSlot>, + /// Maps package-path combinations to canonical hashes. All package-path + /// combinations that point to the same file are mapped to the same hash. To + /// be used in conjunction with `paths`. + hashes: RefCell<HashMap<FileId, FileResult<PathHash>>>, + /// Maps canonical path hashes to source files and buffers. + paths: RefCell<HashMap<PathHash, PathSlot>>, + /// The current date if requested. This is stored here to ensure it is + /// always the same within one compilation. Reset between compilations. + today: OnceCell<Option<Datetime>>, +} + +impl SystemWorld { + /// Create a new system world. + pub fn new(command: &CompileCommand) -> StrResult<Self> { + let mut searcher = FontSearcher::new(); + searcher.search(&command.font_paths); + + // Resolve the system-global input path. + let system_input = command.input.canonicalize().map_err(|_| { + eco_format!("input file not found (searched at {})", command.input.display()) + })?; + + // Resolve the system-global root directory. + let root = { + let path = command + .root + .as_deref() + .or_else(|| system_input.parent()) + .unwrap_or(Path::new(".")); + path.canonicalize().map_err(|_| { + eco_format!("root directory not found (searched at {})", path.display()) + })? + }; + + // Resolve the input path within the project. + let project_input = system_input + .strip_prefix(&root) + .map(|path| Path::new("/").join(path)) + .map_err(|_| "input file must be contained in project root")?; + + Ok(Self { + root, + main: FileId::new(None, &project_input), + library: Prehashed::new(typst_library::build()), + book: Prehashed::new(searcher.book), + fonts: searcher.fonts, + hashes: RefCell::default(), + paths: RefCell::default(), + today: OnceCell::new(), + }) + } + + /// The id of the main source file. + pub fn main(&self) -> FileId { + self.main + } + + /// Return all paths the last compilation depended on. + pub fn dependencies(&mut self) -> impl Iterator<Item = &Path> { + self.paths.get_mut().values().map(|slot| slot.system_path.as_path()) + } + + /// Reset the compilation state in preparation of a new compilation. + pub fn reset(&mut self) { + self.hashes.borrow_mut().clear(); + self.paths.borrow_mut().clear(); + self.today.take(); + } + + /// Lookup a source file by id. + #[track_caller] + pub fn lookup(&self, id: FileId) -> Source { + self.source(id).expect("file id does not point to any source file") + } +} + +impl World for SystemWorld { + fn library(&self) -> &Prehashed<Library> { + &self.library + } + + fn book(&self) -> &Prehashed<FontBook> { + &self.book + } + + fn main(&self) -> Source { + self.source(self.main).unwrap() + } + + fn source(&self, id: FileId) -> FileResult<Source> { + self.slot(id)?.source() + } + + fn file(&self, id: FileId) -> FileResult<Bytes> { + self.slot(id)?.file() + } + + fn font(&self, index: usize) -> Option<Font> { + self.fonts[index].get() + } + + fn today(&self, offset: Option<i64>) -> Option<Datetime> { + *self.today.get_or_init(|| { + let naive = match offset { + None => chrono::Local::now().naive_local(), + Some(o) => (chrono::Utc::now() + chrono::Duration::hours(o)).naive_utc(), + }; + + Datetime::from_ymd( + naive.year(), + naive.month().try_into().ok()?, + naive.day().try_into().ok()?, + ) + }) + } +} + +impl SystemWorld { + /// Access the canonical slot for the given file id. + #[tracing::instrument(skip_all)] + fn slot(&self, id: FileId) -> FileResult<RefMut<PathSlot>> { + let mut system_path = PathBuf::new(); + let hash = self + .hashes + .borrow_mut() + .entry(id) + .or_insert_with(|| { + // Determine the root path relative to which the file path + // will be resolved. + let root = match id.package() { + Some(spec) => prepare_package(spec)?, + None => self.root.clone(), + }; + + // Join the path to the root. If it tries to escape, deny + // access. Note: It can still escape via symlinks. + system_path = + root.join_rooted(id.path()).ok_or(FileError::AccessDenied)?; + + PathHash::new(&system_path) + }) + .clone()?; + + Ok(RefMut::map(self.paths.borrow_mut(), |paths| { + paths.entry(hash).or_insert_with(|| PathSlot { + id, + // This will only trigger if the `or_insert_with` above also + // triggered. + system_path, + source: OnceCell::new(), + buffer: OnceCell::new(), + }) + })) + } +} + +/// Holds canonical data for all paths pointing to the same entity. +/// +/// Both fields can be populated if the file is both imported and read(). +struct PathSlot { + /// The slot's canonical file id. + id: FileId, + /// The slot's path on the system. + system_path: PathBuf, + /// The lazily loaded source file for a path hash. + source: OnceCell<FileResult<Source>>, + /// The lazily loaded buffer for a path hash. + buffer: OnceCell<FileResult<Bytes>>, +} + +impl PathSlot { + fn source(&self) -> FileResult<Source> { + self.source + .get_or_init(|| { + let buf = read(&self.system_path)?; + let text = decode_utf8(buf)?; + Ok(Source::new(self.id, text)) + }) + .clone() + } + + fn file(&self) -> FileResult<Bytes> { + self.buffer + .get_or_init(|| read(&self.system_path).map(Bytes::from)) + .clone() + } +} + +/// A hash that is the same for all paths pointing to the same entity. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +struct PathHash(u128); + +impl PathHash { + fn new(path: &Path) -> FileResult<Self> { + let f = |e| FileError::from_io(e, path); + let handle = Handle::from_path(path).map_err(f)?; + let mut state = SipHasher13::new(); + handle.hash(&mut state); + Ok(Self(state.finish128().as_u128())) + } +} + +/// Read a file. +fn read(path: &Path) -> FileResult<Vec<u8>> { + let f = |e| FileError::from_io(e, path); + if fs::metadata(path).map_err(f)?.is_dir() { + Err(FileError::IsDirectory) + } else { + fs::read(path).map_err(f) + } +} + +/// Decode UTF-8 with an optional BOM. +fn decode_utf8(buf: Vec<u8>) -> FileResult<String> { + Ok(if buf.starts_with(b"\xef\xbb\xbf") { + // Remove UTF-8 BOM. + std::str::from_utf8(&buf[3..])?.into() + } else { + // Assume UTF-8. + String::from_utf8(buf)? + }) +} diff --git a/crates/typst-docs/Cargo.toml b/crates/typst-docs/Cargo.toml new file mode 100644 index 00000000..47088815 --- /dev/null +++ b/crates/typst-docs/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "typst-docs" +version.workspace = true +rust-version.workspace = true +authors.workspace = true +edition.workspace = true +publish = false + +[lib] +doctest = false +bench = false + +[dependencies] +typst = { path = "../typst" } +typst-library = { path = "../typst-library" } +comemo = "0.3" +heck = "0.4" +include_dir = "0.7" +once_cell = "1" +pulldown-cmark = "0.9" +serde = { version = "1", features = ["derive"] } +serde_yaml = "0.8" +syntect = { version = "5", default-features = false, features = ["parsing", "html", "regex-fancy"] } +typed-arena = "2" +unicode_names2 = "0.6.0" +unscanny = "0.1" +yaml-front-matter = "0.1" diff --git a/crates/typst-docs/src/contribs.rs b/crates/typst-docs/src/contribs.rs new file mode 100644 index 00000000..901b5f69 --- /dev/null +++ b/crates/typst-docs/src/contribs.rs @@ -0,0 +1,86 @@ +use std::cmp::Reverse; +use std::collections::HashMap; +use std::fmt::Write; + +use serde::Deserialize; + +use super::{Html, Resolver}; + +/// Build HTML detailing the contributors between two tags. +pub fn contributors(resolver: &dyn Resolver, from: &str, to: &str) -> Option<Html> { + let staff = ["laurmaedje", "reknih"]; + + // Determine number of contributions per person. + let mut contributors = HashMap::<String, Contributor>::new(); + for commit in resolver.commits(from, to) { + contributors + .entry(commit.author.login.clone()) + .or_insert_with(|| Contributor { + login: commit.author.login, + avatar: commit.author.avatar_url, + contributions: 0, + }) + .contributions += 1; + } + + // Keep only non-staff people. + let mut contributors: Vec<_> = contributors + .into_values() + .filter(|c| !staff.contains(&c.login.as_str())) + .collect(); + + // Sort by highest number of commits. + contributors.sort_by_key(|c| (Reverse(c.contributions), c.login.clone())); + if contributors.is_empty() { + return None; + } + + let mut html = "Thanks to everyone who contributed to this release!".to_string(); + html += "<ul class=\"contribs\">"; + + for Contributor { login, avatar, contributions } in contributors { + let login = login.replace('\"', """).replace('&', "&"); + let avatar = avatar.replace("?v=", "?s=64&v="); + let s = if contributions > 1 { "s" } else { "" }; + write!( + html, + r#"<li> + <a href="https://github.com/{login}" target="_blank"> + <img + width="64" + height="64" + src="{avatar}" + alt="GitHub avatar of {login}" + title="@{login} made {contributions} contribution{s}" + crossorigin="anonymous" + > + </a> + </li>"# + ) + .unwrap(); + } + + html += "</ul>"; + + Some(Html::new(html)) +} + +#[derive(Debug)] +struct Contributor { + login: String, + avatar: String, + contributions: usize, +} + +/// A commit on the `typst` repository. +#[derive(Debug, Deserialize)] +pub struct Commit { + author: Author, +} + +/// A commit author. +#[derive(Debug, Deserialize)] +pub struct Author { + login: String, + avatar_url: String, +} diff --git a/crates/typst-docs/src/html.rs b/crates/typst-docs/src/html.rs new file mode 100644 index 00000000..0d40f46b --- /dev/null +++ b/crates/typst-docs/src/html.rs @@ -0,0 +1,513 @@ +use std::ops::Range; + +use comemo::Prehashed; +use pulldown_cmark as md; +use typed_arena::Arena; +use typst::diag::FileResult; +use typst::eval::Datetime; +use typst::file::FileId; +use typst::font::{Font, FontBook}; +use typst::geom::{Point, Size}; +use typst::syntax::Source; +use typst::util::Bytes; +use typst::World; +use yaml_front_matter::YamlFrontMatter; + +use super::*; + +/// HTML documentation. +#[derive(Serialize)] +#[serde(transparent)] +pub struct Html { + raw: String, + #[serde(skip)] + md: String, + #[serde(skip)] + description: Option<String>, + #[serde(skip)] + outline: Vec<OutlineItem>, +} + +impl Html { + /// Create HTML from a raw string. + pub fn new(raw: String) -> Self { + Self { + md: String::new(), + raw, + description: None, + outline: vec![], + } + } + + /// Convert markdown to HTML. + #[track_caller] + pub fn markdown(resolver: &dyn Resolver, md: &str) -> Self { + Self::markdown_with_id_base(resolver, md, "") + } + + /// Convert markdown to HTML, preceding all fragment identifiers with the + /// `id_base`. + #[track_caller] + pub fn markdown_with_id_base( + resolver: &dyn Resolver, + md: &str, + id_base: &str, + ) -> Self { + let mut text = md; + let mut description = None; + let document = YamlFrontMatter::parse::<Metadata>(md); + if let Ok(document) = &document { + text = &document.content; + description = Some(document.metadata.description.clone()) + } + + let options = md::Options::ENABLE_TABLES | md::Options::ENABLE_HEADING_ATTRIBUTES; + + let ids = Arena::new(); + let mut handler = Handler::new(resolver, id_base.into(), &ids); + let iter = md::Parser::new_ext(text, options) + .filter_map(|mut event| handler.handle(&mut event).then_some(event)); + + let mut raw = String::new(); + md::html::push_html(&mut raw, iter); + raw.truncate(raw.trim_end().len()); + + Html { + md: text.into(), + raw, + description, + outline: handler.outline, + } + } + + /// The raw HTML. + pub fn as_str(&self) -> &str { + &self.raw + } + + /// The original Markdown, if any. + pub fn md(&self) -> &str { + &self.md + } + + /// The title of the HTML. + /// + /// Returns `None` if the HTML doesn't start with an `h1` tag. + pub fn title(&self) -> Option<&str> { + let mut s = Scanner::new(&self.raw); + s.eat_if("<h1>").then(|| s.eat_until("</h1>")) + } + + /// The outline of the HTML. + pub fn outline(&self) -> Vec<OutlineItem> { + self.outline.clone() + } + + /// The description from the front matter. + pub fn description(&self) -> Option<String> { + self.description.clone() + } +} + +impl Debug for Html { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Html({:?})", self.title().unwrap_or("..")) + } +} + +/// Front matter metadata. +#[derive(Deserialize)] +struct Metadata { + description: String, +} + +struct Handler<'a> { + resolver: &'a dyn Resolver, + lang: Option<String>, + code: String, + outline: Vec<OutlineItem>, + id_base: String, + ids: &'a Arena<String>, +} + +impl<'a> Handler<'a> { + fn new(resolver: &'a dyn Resolver, id_base: String, ids: &'a Arena<String>) -> Self { + Self { + resolver, + lang: None, + code: String::new(), + outline: vec![], + id_base, + ids, + } + } + + fn handle(&mut self, event: &mut md::Event<'a>) -> bool { + match event { + // Rewrite Markdown images. + md::Event::Start(md::Tag::Image(_, path, _)) => { + *path = self.handle_image(path).into(); + } + + // Rewrite HTML images. + md::Event::Html(html) if html.starts_with("<img") => { + let range = html_attr_range(html, "src").unwrap(); + let path = &html[range.clone()]; + let mut buf = html.to_string(); + buf.replace_range(range, &self.handle_image(path)); + *html = buf.into(); + } + + // Register HTML headings for the outline. + md::Event::Start(md::Tag::Heading(level, Some(id), _)) => { + self.handle_heading(id, level); + } + + // Also handle heading closings. + md::Event::End(md::Tag::Heading(level, Some(_), _)) => { + if *level > md::HeadingLevel::H1 && !self.id_base.is_empty() { + nest_heading(level); + } + } + + // Rewrite contributor sections. + md::Event::Html(html) if html.starts_with("<contributors") => { + let from = html_attr(html, "from").unwrap(); + let to = html_attr(html, "to").unwrap(); + let Some(output) = contributors(self.resolver, from, to) else { return false }; + *html = output.raw.into(); + } + + // Rewrite links. + md::Event::Start(md::Tag::Link(ty, dest, _)) => { + assert!( + matches!(ty, md::LinkType::Inline | md::LinkType::Reference), + "unsupported link type: {ty:?}", + ); + + *dest = self + .handle_link(dest) + .unwrap_or_else(|| panic!("invalid link: {dest}")) + .into(); + } + + // Inline raw. + md::Event::Code(code) => { + let mut chars = code.chars(); + let parser = match (chars.next(), chars.next_back()) { + (Some('['), Some(']')) => typst::syntax::parse, + (Some('{'), Some('}')) => typst::syntax::parse_code, + _ => return true, + }; + + let root = parser(&code[1..code.len() - 1]); + let html = typst::ide::highlight_html(&root); + *event = md::Event::Html(html.into()); + } + + // Code blocks. + md::Event::Start(md::Tag::CodeBlock(md::CodeBlockKind::Fenced(lang))) => { + self.lang = Some(lang.as_ref().into()); + self.code = String::new(); + return false; + } + md::Event::End(md::Tag::CodeBlock(md::CodeBlockKind::Fenced(_))) => { + let Some(lang) = self.lang.take() else { return false }; + let html = code_block(self.resolver, &lang, &self.code); + *event = md::Event::Html(html.raw.into()); + } + + // Example with preview. + md::Event::Text(text) => { + if self.lang.is_some() { + self.code.push_str(text); + return false; + } + } + + _ => {} + } + + true + } + + fn handle_image(&self, link: &str) -> String { + if let Some(file) = FILES.get_file(link) { + self.resolver.image(link, file.contents()) + } else if let Some(url) = self.resolver.link(link) { + url + } else { + panic!("missing image: {link}") + } + } + + fn handle_heading(&mut self, id: &mut &'a str, level: &mut md::HeadingLevel) { + if *level == md::HeadingLevel::H1 { + return; + } + + // Special case for things like "v0.3.0". + let name = if id.starts_with('v') && id.contains('.') { + id.to_string() + } else { + id.to_title_case() + }; + + let mut children = &mut self.outline; + let mut depth = *level as usize; + while depth > 2 { + if !children.is_empty() { + children = &mut children.last_mut().unwrap().children; + } + depth -= 1; + } + + // Put base before id. + if !self.id_base.is_empty() { + nest_heading(level); + *id = self.ids.alloc(format!("{}-{id}", self.id_base)).as_str(); + } + + children.push(OutlineItem { id: id.to_string(), name, children: vec![] }); + } + + fn handle_link(&self, link: &str) -> Option<String> { + if link.starts_with('#') || link.starts_with("http") { + return Some(link.into()); + } + + if !link.starts_with('$') { + return self.resolver.link(link); + } + + let root = link.split('/').next()?; + let rest = &link[root.len()..].trim_matches('/'); + let base = match root { + "$tutorial" => "/docs/tutorial/", + "$reference" => "/docs/reference/", + "$category" => "/docs/reference/", + "$syntax" => "/docs/reference/syntax/", + "$styling" => "/docs/reference/styling/", + "$scripting" => "/docs/reference/scripting/", + "$types" => "/docs/reference/types/", + "$type" => "/docs/reference/types/", + "$func" => "/docs/reference/", + "$guides" => "/docs/guides/", + "$packages" => "/docs/packages/", + "$changelog" => "/docs/changelog/", + "$community" => "/docs/community/", + _ => panic!("unknown link root: {root}"), + }; + + let mut route = base.to_string(); + if root == "$type" && rest.contains('.') { + let mut parts = rest.split('.'); + let ty = parts.next()?; + let method = parts.next()?; + route.push_str(ty); + route.push_str("/#methods-"); + route.push_str(method); + } else if root == "$func" { + let mut parts = rest.split('.').peekable(); + let first = parts.peek().copied(); + let mut focus = &LIBRARY.global; + while let Some(m) = first.and_then(|name| module(focus, name).ok()) { + focus = m; + parts.next(); + } + + let name = parts.next()?; + + let value = focus.get(name).ok()?; + let Value::Func(func) = value else { return None }; + let info = func.info()?; + route.push_str(info.category); + route.push('/'); + + if let Some(group) = GROUPS + .iter() + .filter(|_| first == Some("math")) + .find(|group| group.functions.iter().any(|func| func == info.name)) + { + route.push_str(&group.name); + route.push_str("/#"); + route.push_str(info.name); + if let Some(param) = parts.next() { + route.push_str("-parameters-"); + route.push_str(param); + } + } else { + route.push_str(name); + route.push('/'); + if let Some(next) = parts.next() { + if info.params.iter().any(|param| param.name == next) { + route.push_str("#parameters-"); + route.push_str(next); + } else if info.scope.iter().any(|(name, _)| name == next) { + route.push('#'); + route.push_str(info.name); + route.push('-'); + route.push_str(next); + } else { + return None; + } + } + } + } else { + route.push_str(rest); + } + + if !route.contains('#') && !route.ends_with('/') { + route.push('/'); + } + + Some(route) + } +} + +/// Render a code block to HTML. +fn code_block(resolver: &dyn Resolver, lang: &str, text: &str) -> Html { + let mut display = String::new(); + let mut compile = String::new(); + for line in text.lines() { + if let Some(suffix) = line.strip_prefix(">>>") { + compile.push_str(suffix); + compile.push('\n'); + } else if let Some(suffix) = line.strip_prefix("<<< ") { + display.push_str(suffix); + display.push('\n'); + } else { + display.push_str(line); + display.push('\n'); + compile.push_str(line); + compile.push('\n'); + } + } + + let mut parts = lang.split(':'); + let lang = parts.next().unwrap_or(lang); + + let mut zoom: Option<[Abs; 4]> = None; + let mut single = false; + if let Some(args) = parts.next() { + single = true; + if !args.contains("single") { + zoom = args + .split(',') + .take(4) + .map(|s| Abs::pt(s.parse().unwrap())) + .collect::<Vec<_>>() + .try_into() + .ok(); + } + } + + if lang.is_empty() { + let mut buf = String::from("<pre>"); + md::escape::escape_html(&mut buf, &display).unwrap(); + buf.push_str("</pre>"); + return Html::new(buf); + } else if !matches!(lang, "example" | "typ") { + let set = &*typst_library::text::SYNTAXES; + let buf = syntect::html::highlighted_html_for_string( + &display, + set, + set.find_syntax_by_token(lang) + .unwrap_or_else(|| panic!("unsupported highlighting language: {lang}")), + &typst_library::text::THEME, + ) + .expect("failed to highlight code"); + return Html::new(buf); + } + + let root = typst::syntax::parse(&display); + let highlighted = Html::new(typst::ide::highlight_html(&root)); + if lang == "typ" { + return Html::new(format!("<pre>{}</pre>", highlighted.as_str())); + } + + let id = FileId::new(None, Path::new("/main.typ")); + let source = Source::new(id, compile); + let world = DocWorld(source); + let mut frames = match typst::compile(&world) { + Ok(doc) => doc.pages, + Err(err) => { + let msg = &err[0].message; + panic!("while trying to compile:\n{text}:\n\nerror: {msg}"); + } + }; + + if let Some([x, y, w, h]) = zoom { + frames[0].translate(Point::new(-x, -y)); + *frames[0].size_mut() = Size::new(w, h); + } + + if single { + frames.truncate(1); + } + + resolver.example(highlighted, &frames) +} + +/// Extract an attribute value from an HTML element. +fn html_attr<'a>(html: &'a str, attr: &str) -> Option<&'a str> { + html.get(html_attr_range(html, attr)?) +} + +/// Extract the range of the attribute value of an HTML element. +fn html_attr_range(html: &str, attr: &str) -> Option<Range<usize>> { + let needle = format!("{attr}=\""); + let offset = html.find(&needle)? + needle.len(); + let len = html[offset..].find('"')?; + Some(offset..offset + len) +} + +/// Increase the nesting level of a Markdown heading. +fn nest_heading(level: &mut md::HeadingLevel) { + *level = match &level { + md::HeadingLevel::H1 => md::HeadingLevel::H2, + md::HeadingLevel::H2 => md::HeadingLevel::H3, + md::HeadingLevel::H3 => md::HeadingLevel::H4, + md::HeadingLevel::H4 => md::HeadingLevel::H5, + md::HeadingLevel::H5 => md::HeadingLevel::H6, + v => **v, + }; +} + +/// A world for example compilations. +struct DocWorld(Source); + +impl World for DocWorld { + fn library(&self) -> &Prehashed<Library> { + &LIBRARY + } + + fn book(&self) -> &Prehashed<FontBook> { + &FONTS.0 + } + + fn main(&self) -> Source { + self.0.clone() + } + + fn source(&self, _: FileId) -> FileResult<Source> { + Ok(self.0.clone()) + } + + fn file(&self, id: FileId) -> FileResult<Bytes> { + assert!(id.package().is_none()); + Ok(FILES + .get_file(id.path().strip_prefix("/").unwrap()) + .unwrap_or_else(|| panic!("failed to load {:?}", id.path().display())) + .contents() + .into()) + } + + fn font(&self, index: usize) -> Option<Font> { + Some(FONTS.1[index].clone()) + } + + fn today(&self, _: Option<i64>) -> Option<Datetime> { + Some(Datetime::from_ymd(1970, 1, 1).unwrap()) + } +} diff --git a/crates/typst-docs/src/lib.rs b/crates/typst-docs/src/lib.rs new file mode 100644 index 00000000..d7f816d2 --- /dev/null +++ b/crates/typst-docs/src/lib.rs @@ -0,0 +1,1002 @@ +//! Documentation provider for Typst. + +mod contribs; +mod html; + +pub use contribs::{contributors, Author, Commit}; +pub use html::Html; + +use std::fmt::{self, Debug, Formatter}; +use std::path::Path; + +use comemo::Prehashed; +use heck::ToTitleCase; +use include_dir::{include_dir, Dir}; +use once_cell::sync::Lazy; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use serde_yaml as yaml; +use typst::doc::Frame; +use typst::eval::{CastInfo, Func, FuncInfo, Library, Module, ParamInfo, Value}; +use typst::font::{Font, FontBook}; +use typst::geom::{Abs, Smart}; +use typst_library::layout::{Margin, PageElem}; +use unscanny::Scanner; + +static DOCS: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../docs"); +static FILES: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../assets/files"); +static DETAILS: Lazy<yaml::Mapping> = Lazy::new(|| yaml("reference/details.yml")); +static GROUPS: Lazy<Vec<GroupData>> = Lazy::new(|| yaml("reference/groups.yml")); + +static FONTS: Lazy<(Prehashed<FontBook>, Vec<Font>)> = Lazy::new(|| { + static DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../assets/fonts"); + let fonts: Vec<_> = DIR + .files() + .flat_map(|file| Font::iter(file.contents().into())) + .collect(); + let book = FontBook::from_fonts(&fonts); + (Prehashed::new(book), fonts) +}); + +static LIBRARY: Lazy<Prehashed<Library>> = Lazy::new(|| { + let mut lib = typst_library::build(); + lib.styles + .set(PageElem::set_width(Smart::Custom(Abs::pt(240.0).into()))); + lib.styles.set(PageElem::set_height(Smart::Auto)); + lib.styles.set(PageElem::set_margin(Margin::splat(Some(Smart::Custom( + Abs::pt(15.0).into(), + ))))); + typst::eval::set_lang_items(lib.items.clone()); + Prehashed::new(lib) +}); + +/// Build documentation pages. +pub fn provide(resolver: &dyn Resolver) -> Vec<PageModel> { + vec![ + markdown_page(resolver, "/docs/", "overview.md").with_route("/docs/"), + tutorial_pages(resolver), + reference_pages(resolver), + guides_pages(resolver), + packages_page(resolver), + markdown_page(resolver, "/docs/", "changelog.md"), + markdown_page(resolver, "/docs/", "community.md"), + ] +} + +/// Resolve consumer dependencies. +pub trait Resolver { + /// Try to resolve a link that the system cannot resolve itself. + fn link(&self, link: &str) -> Option<String>; + + /// Produce an URL for an image file. + fn image(&self, filename: &str, data: &[u8]) -> String; + + /// Produce HTML for an example. + fn example(&self, source: Html, frames: &[Frame]) -> Html; + + /// Determine the commits between two tags. + fn commits(&self, from: &str, to: &str) -> Vec<Commit>; +} + +/// Details about a documentation page and its children. +#[derive(Debug, Serialize)] +pub struct PageModel { + pub route: String, + pub title: String, + pub description: String, + pub part: Option<&'static str>, + pub outline: Vec<OutlineItem>, + pub body: BodyModel, + pub children: Vec<Self>, +} + +impl PageModel { + fn with_route(self, route: &str) -> Self { + Self { route: route.into(), ..self } + } + + fn with_part(self, part: &'static str) -> Self { + Self { part: Some(part), ..self } + } +} + +/// An element in the "On This Page" outline. +#[derive(Debug, Clone, Serialize)] +pub struct OutlineItem { + id: String, + name: String, + children: Vec<Self>, +} + +/// Details about the body of a documentation page. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "kind", content = "content")] +pub enum BodyModel { + Html(Html), + Category(CategoryModel), + Func(FuncModel), + Funcs(FuncsModel), + Type(TypeModel), + Symbols(SymbolsModel), + Packages(Html), +} + +/// Build the tutorial. +fn tutorial_pages(resolver: &dyn Resolver) -> PageModel { + let mut page = markdown_page(resolver, "/docs/", "tutorial/welcome.md"); + page.children = DOCS + .get_dir("tutorial") + .unwrap() + .files() + .filter(|file| file.path() != Path::new("tutorial/welcome.md")) + .map(|file| markdown_page(resolver, "/docs/tutorial/", file.path())) + .collect(); + page +} + +/// Build the reference. +fn reference_pages(resolver: &dyn Resolver) -> PageModel { + let mut page = markdown_page(resolver, "/docs/", "reference/welcome.md"); + page.children = vec![ + markdown_page(resolver, "/docs/reference/", "reference/syntax.md") + .with_part("Language"), + markdown_page(resolver, "/docs/reference/", "reference/styling.md"), + markdown_page(resolver, "/docs/reference/", "reference/scripting.md"), + types_page(resolver, "/docs/reference/"), + category_page(resolver, "text").with_part("Content"), + category_page(resolver, "math"), + category_page(resolver, "layout"), + category_page(resolver, "visualize"), + category_page(resolver, "meta"), + category_page(resolver, "symbols"), + category_page(resolver, "foundations").with_part("Compute"), + category_page(resolver, "calculate"), + category_page(resolver, "construct"), + category_page(resolver, "data-loading"), + ]; + page +} + +/// Build the guides section. +fn guides_pages(resolver: &dyn Resolver) -> PageModel { + let mut page = markdown_page(resolver, "/docs/", "guides/welcome.md"); + page.children = + vec![markdown_page(resolver, "/docs/guides/", "guides/guide-for-latex-users.md")]; + page +} + +/// Build the packages section. +fn packages_page(resolver: &dyn Resolver) -> PageModel { + PageModel { + route: "/docs/packages/".into(), + title: "Packages".into(), + description: "Packages for Typst.".into(), + part: None, + outline: vec![], + body: BodyModel::Packages(Html::markdown(resolver, details("packages"))), + children: vec![], + } +} + +/// Create a page from a markdown file. +#[track_caller] +fn markdown_page( + resolver: &dyn Resolver, + parent: &str, + path: impl AsRef<Path>, +) -> PageModel { + assert!(parent.starts_with('/') && parent.ends_with('/')); + let md = DOCS.get_file(path).unwrap().contents_utf8().unwrap(); + let html = Html::markdown(resolver, md); + let title = html.title().expect("chapter lacks a title").to_string(); + PageModel { + route: format!("{parent}{}/", urlify(&title)), + title, + description: html.description().unwrap(), + part: None, + outline: html.outline(), + body: BodyModel::Html(html), + children: vec![], + } +} + +/// Details about a category. +#[derive(Debug, Serialize)] +pub struct CategoryModel { + pub name: String, + pub details: Html, + pub kind: &'static str, + pub items: Vec<CategoryItem>, +} + +/// Details about a category item. +#[derive(Debug, Serialize)] +pub struct CategoryItem { + pub name: String, + pub route: String, + pub oneliner: String, + pub code: bool, +} + +/// Create a page for a category. +#[track_caller] +fn category_page(resolver: &dyn Resolver, category: &str) -> PageModel { + let route = format!("/docs/reference/{category}/"); + let mut children = vec![]; + let mut items = vec![]; + + let focus = match category { + "math" => &LIBRARY.math, + "calculate" => module(&LIBRARY.global, "calc").unwrap(), + _ => &LIBRARY.global, + }; + + let parents: &[&str] = match category { + "math" => &[], + "calculate" => &["calc"], + _ => &[], + }; + + let grouped = match category { + "math" => GROUPS.as_slice(), + _ => &[], + }; + + // Add functions. + for (_, value) in focus.scope().iter() { + let Value::Func(func) = value else { continue }; + let Some(info) = func.info() else { continue }; + if info.category != category { + continue; + } + + // Skip grouped functions. + if grouped + .iter() + .flat_map(|group| &group.functions) + .any(|f| f == info.name) + { + continue; + } + + let subpage = function_page(resolver, &route, func, info, parents); + items.push(CategoryItem { + name: info.name.into(), + route: subpage.route.clone(), + oneliner: oneliner(info.docs).into(), + code: true, + }); + children.push(subpage); + } + + // Add grouped functions. + for group in grouped { + let mut functions = vec![]; + let mut outline = vec![OutlineItem { + id: "summary".into(), + name: "Summary".into(), + children: vec![], + }]; + + for name in &group.functions { + let value = focus.get(name).unwrap(); + let Value::Func(func) = value else { panic!("not a function") }; + let info = func.info().unwrap(); + let func = func_model(resolver, func, info, &[], info.name); + let id = urlify(&func.path.join("-")); + let children = func_outline(&func, &id, false); + outline.push(OutlineItem { id, name: func.display.into(), children }); + functions.push(func); + } + + let route = format!("{}{}/", route, group.name); + items.push(CategoryItem { + name: group.name.clone(), + route: route.clone(), + oneliner: oneliner(&group.description).into(), + code: false, + }); + + children.push(PageModel { + route, + title: group.display.clone(), + description: format!("Documentation for {} group of functions.", group.name), + part: None, + outline, + body: BodyModel::Funcs(FuncsModel { + name: group.name.clone(), + display: group.display.clone(), + details: Html::markdown(resolver, &group.description), + functions, + }), + children: vec![], + }); + } + + children.sort_by_cached_key(|child| child.title.clone()); + items.sort_by_cached_key(|item| item.name.clone()); + + // Add symbol pages. These are ordered manually. + if category == "symbols" { + for module in ["sym", "emoji"] { + let subpage = symbol_page(resolver, &route, module); + items.push(CategoryItem { + name: module.into(), + route: subpage.route.clone(), + oneliner: oneliner(details(module)).into(), + code: true, + }); + children.push(subpage); + } + } + + let name = category.to_title_case(); + let kind = match category { + "symbols" => "Modules", + _ => "Functions", + }; + + PageModel { + route, + title: name.clone(), + description: format!("Documentation for functions related to {name} in Typst."), + part: None, + outline: category_outline(kind), + body: BodyModel::Category(CategoryModel { + name, + details: Html::markdown(resolver, details(category)), + kind, + items, + }), + children, + } +} + +/// Produce an outline for a category page. +fn category_outline(kind: &str) -> Vec<OutlineItem> { + vec![ + OutlineItem { + id: "summary".into(), + name: "Summary".into(), + children: vec![], + }, + OutlineItem { + id: urlify(kind), + name: kind.into(), + children: vec![], + }, + ] +} + +/// Details about a function. +#[derive(Debug, Serialize)] +pub struct FuncModel { + pub path: Vec<&'static str>, + pub display: &'static str, + pub keywords: Option<&'static str>, + pub oneliner: &'static str, + pub element: bool, + pub details: Html, + pub params: Vec<ParamModel>, + pub returns: Vec<&'static str>, + pub methods: Vec<MethodModel>, + pub scope: Vec<Self>, +} + +/// Details about a group of functions. +#[derive(Debug, Serialize)] +pub struct FuncsModel { + pub name: String, + pub display: String, + pub details: Html, + pub functions: Vec<FuncModel>, +} + +/// Create a page for a function. +fn function_page( + resolver: &dyn Resolver, + parent: &str, + func: &Func, + info: &FuncInfo, + parents: &[&'static str], +) -> PageModel { + let model = func_model(resolver, func, info, parents, ""); + PageModel { + route: format!("{parent}{}/", urlify(info.name)), + title: info.display.to_string(), + description: format!("Documentation for the `{}` function.", info.name), + part: None, + outline: func_outline(&model, "", true), + body: BodyModel::Func(model), + children: vec![], + } +} + +/// Produce a function's model. +fn func_model( + resolver: &dyn Resolver, + func: &Func, + info: &FuncInfo, + parents: &[&'static str], + id_base: &str, +) -> FuncModel { + let mut s = unscanny::Scanner::new(info.docs); + let docs = s.eat_until("\n## Methods").trim(); + + let mut path = parents.to_vec(); + let mut name = info.name; + for parent in parents.iter().rev() { + name = name + .strip_prefix(parent) + .or(name.strip_prefix(parent.strip_suffix('s').unwrap_or(parent))) + .unwrap_or(name) + .trim_matches('-'); + } + path.push(name); + + let scope = info + .scope + .iter() + .filter_map(|(_, value)| { + let Value::Func(func) = value else { return None }; + let info = func.info().unwrap(); + Some(func_model(resolver, func, info, &path, id_base)) + }) + .collect(); + + let mut returns = vec![]; + casts(resolver, &mut returns, &mut vec![], &info.returns); + returns.sort_by_key(|ty| type_index(ty)); + if returns == ["none"] { + returns.clear(); + } + + FuncModel { + path, + display: info.display, + keywords: info.keywords, + oneliner: oneliner(docs), + element: func.element().is_some(), + details: Html::markdown_with_id_base(resolver, docs, id_base), + params: info.params.iter().map(|param| param_model(resolver, param)).collect(), + returns, + methods: method_models(resolver, info.docs), + scope, + } +} + +/// Produce an outline for a function page. +fn func_outline(model: &FuncModel, base: &str, summary: bool) -> Vec<OutlineItem> { + let mut outline = vec![]; + + if summary { + outline.push(OutlineItem { + id: "summary".into(), + name: "Summary".into(), + children: vec![], + }); + } + + outline.extend(model.details.outline()); + + if !model.params.is_empty() { + let join = if base.is_empty() { "" } else { "-" }; + outline.push(OutlineItem { + id: format!("{base}{join}parameters"), + name: "Parameters".into(), + children: model + .params + .iter() + .map(|param| OutlineItem { + id: format!("{base}{join}parameters-{}", urlify(param.name)), + name: param.name.into(), + children: vec![], + }) + .collect(), + }); + } + + for func in &model.scope { + let id = urlify(&func.path.join("-")); + let children = func_outline(func, &id, false); + outline.push(OutlineItem { id, name: func.display.into(), children }) + } + + outline.extend(methods_outline(&model.methods)); + outline +} + +/// Details about a function parameter. +#[derive(Debug, Serialize)] +pub struct ParamModel { + pub name: &'static str, + pub details: Html, + pub example: Option<Html>, + pub types: Vec<&'static str>, + pub strings: Vec<StrParam>, + pub default: Option<Html>, + pub positional: bool, + pub named: bool, + pub required: bool, + pub variadic: bool, + pub settable: bool, +} + +/// A specific string that can be passed as an argument. +#[derive(Debug, Serialize)] +pub struct StrParam { + pub string: String, + pub details: Html, +} + +/// Produce a parameter's model. +fn param_model(resolver: &dyn Resolver, info: &ParamInfo) -> ParamModel { + let mut types = vec![]; + let mut strings = vec![]; + casts(resolver, &mut types, &mut strings, &info.cast); + if !strings.is_empty() && !types.contains(&"string") { + types.push("string"); + } + types.sort_by_key(|ty| type_index(ty)); + + let mut details = info.docs; + let mut example = None; + if let Some(mut i) = info.docs.find("```example") { + while info.docs[..i].ends_with('`') { + i -= 1; + } + details = &info.docs[..i]; + example = Some(&info.docs[i..]); + } + + ParamModel { + name: info.name, + details: Html::markdown(resolver, details), + example: example.map(|md| Html::markdown(resolver, md)), + types, + strings, + default: info.default.map(|default| { + let node = typst::syntax::parse_code(&default().repr()); + Html::new(typst::ide::highlight_html(&node)) + }), + positional: info.positional, + named: info.named, + required: info.required, + variadic: info.variadic, + settable: info.settable, + } +} + +/// Process cast information into types and strings. +fn casts( + resolver: &dyn Resolver, + types: &mut Vec<&'static str>, + strings: &mut Vec<StrParam>, + info: &CastInfo, +) { + match info { + CastInfo::Any => types.push("any"), + CastInfo::Value(Value::Str(string), docs) => strings.push(StrParam { + string: string.to_string(), + details: Html::markdown(resolver, docs), + }), + CastInfo::Value(..) => {} + CastInfo::Type(ty) => types.push(ty), + CastInfo::Union(options) => { + for option in options { + casts(resolver, types, strings, option); + } + } + } +} + +/// A collection of symbols. +#[derive(Debug, Serialize)] +pub struct TypeModel { + pub name: String, + pub oneliner: &'static str, + pub details: Html, + pub methods: Vec<MethodModel>, +} + +/// Details about a built-in method on a type. +#[derive(Debug, Serialize)] +pub struct MethodModel { + pub name: &'static str, + pub details: Html, + pub params: Vec<ParamModel>, + pub returns: Vec<&'static str>, +} + +/// Create a page for the types. +fn types_page(resolver: &dyn Resolver, parent: &str) -> PageModel { + let route = format!("{parent}types/"); + let mut children = vec![]; + let mut items = vec![]; + + for model in type_models(resolver) { + let route = format!("{route}{}/", urlify(&model.name)); + items.push(CategoryItem { + name: model.name.clone(), + route: route.clone(), + oneliner: model.oneliner.into(), + code: true, + }); + children.push(PageModel { + route, + title: model.name.to_title_case(), + description: format!("Documentation for the `{}` type.", model.name), + part: None, + outline: type_outline(&model), + body: BodyModel::Type(model), + children: vec![], + }); + } + + PageModel { + route, + title: "Types".into(), + description: "Documentation for Typst's built-in types.".into(), + part: None, + outline: category_outline("Types"), + body: BodyModel::Category(CategoryModel { + name: "Types".into(), + details: Html::markdown(resolver, details("types")), + kind: "Types", + items, + }), + children, + } +} + +/// Produce the types' models. +fn type_models(resolver: &dyn Resolver) -> Vec<TypeModel> { + let file = DOCS.get_file("reference/types.md").unwrap(); + let text = file.contents_utf8().unwrap(); + + let mut s = unscanny::Scanner::new(text); + let mut types = vec![]; + + while s.eat_if("# ") { + let part = s.eat_until("\n# "); + types.push(type_model(resolver, part)); + s.eat_if('\n'); + } + + types +} + +/// Produce a type's model. +fn type_model(resolver: &dyn Resolver, part: &'static str) -> TypeModel { + let mut s = unscanny::Scanner::new(part); + let display = s.eat_until('\n').trim(); + let docs = s.eat_until("\n## Methods").trim(); + TypeModel { + name: display.to_lowercase(), + oneliner: oneliner(docs), + details: Html::markdown(resolver, docs), + methods: method_models(resolver, part), + } +} + +/// Produce multiple methods' models. +fn method_models(resolver: &dyn Resolver, docs: &'static str) -> Vec<MethodModel> { + let mut s = unscanny::Scanner::new(docs); + s.eat_until("\n## Methods"); + s.eat_whitespace(); + + let mut methods = vec![]; + if s.eat_if("## Methods") { + s.eat_until("\n### "); + while s.eat_if("\n### ") { + methods.push(method_model(resolver, s.eat_until("\n### "))); + } + } + + methods +} + +/// Produce a method's model. +fn method_model(resolver: &dyn Resolver, part: &'static str) -> MethodModel { + let mut s = unscanny::Scanner::new(part); + let mut params = vec![]; + let mut returns = vec![]; + + let name = s.eat_until('(').trim(); + s.expect("()"); + let docs = s.eat_until("\n- ").trim(); + + while s.eat_if("\n- ") { + let name = s.eat_until(':'); + s.expect(": "); + let types: Vec<_> = + s.eat_until(['(', '\n']).split(" or ").map(str::trim).collect(); + if !types.iter().all(|ty| type_index(ty) != usize::MAX) { + panic!( + "unknown type in method {} parameter {}", + name, + types.iter().find(|ty| type_index(ty) == usize::MAX).unwrap() + ) + } + + if name == "returns" { + returns = types; + continue; + } + + s.expect('('); + + let mut named = false; + let mut positional = false; + let mut required = false; + let mut variadic = false; + for part in s.eat_until(')').split(',').map(str::trim) { + match part { + "named" => named = true, + "positional" => positional = true, + "required" => required = true, + "variadic" => variadic = true, + _ => panic!("unknown parameter flag {:?}", part), + } + } + + s.expect(')'); + + params.push(ParamModel { + name, + details: Html::markdown(resolver, s.eat_until("\n- ").trim()), + example: None, + types, + strings: vec![], + default: None, + positional, + named, + required, + variadic, + settable: false, + }); + } + + MethodModel { + name, + details: Html::markdown(resolver, docs), + params, + returns, + } +} + +/// Produce an outline for a type page. +fn type_outline(model: &TypeModel) -> Vec<OutlineItem> { + let mut outline = vec![OutlineItem { + id: "summary".into(), + name: "Summary".into(), + children: vec![], + }]; + + outline.extend(methods_outline(&model.methods)); + outline +} + +/// Produce an outline for a type's method. +fn methods_outline(methods: &[MethodModel]) -> Option<OutlineItem> { + (!methods.is_empty()).then(|| OutlineItem { + id: "methods".into(), + name: "Methods".into(), + children: methods.iter().map(method_outline).collect(), + }) +} + +/// Produce an outline for a type's method. +fn method_outline(model: &MethodModel) -> OutlineItem { + OutlineItem { + id: format!("methods-{}", urlify(model.name)), + name: model.name.into(), + children: model + .params + .iter() + .map(|param| OutlineItem { + id: format!( + "methods-{}-parameters-{}", + urlify(model.name), + urlify(param.name) + ), + name: param.name.into(), + children: vec![], + }) + .collect(), + } +} + +/// A collection of symbols. +#[derive(Debug, Serialize)] +pub struct SymbolsModel { + pub name: &'static str, + pub details: Html, + pub list: Vec<SymbolModel>, +} + +/// Details about a symbol. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SymbolModel { + pub name: String, + pub shorthand: Option<&'static str>, + pub codepoint: u32, + pub accent: bool, + pub unicode_name: Option<String>, + pub alternates: Vec<String>, +} + +/// Create a page for symbols. +fn symbol_page(resolver: &dyn Resolver, parent: &str, name: &str) -> PageModel { + let module = module(&LIBRARY.global, name).unwrap(); + + let mut list = vec![]; + for (name, value) in module.scope().iter() { + let Value::Symbol(symbol) = value else { continue }; + let complete = |variant: &str| { + if variant.is_empty() { + name.into() + } else { + format!("{}.{}", name, variant) + } + }; + + for (variant, c) in symbol.variants() { + list.push(SymbolModel { + name: complete(variant), + shorthand: typst::syntax::ast::Shorthand::LIST + .iter() + .copied() + .find(|&(_, x)| x == c) + .map(|(s, _)| s), + codepoint: c as u32, + accent: typst::eval::Symbol::combining_accent(c).is_some(), + unicode_name: unicode_names2::name(c) + .map(|s| s.to_string().to_title_case()), + alternates: symbol + .variants() + .filter(|(other, _)| other != &variant) + .map(|(other, _)| complete(other)) + .collect(), + }); + } + } + + let title = match name { + "sym" => "General", + "emoji" => "Emoji", + _ => unreachable!(), + }; + + PageModel { + route: format!("{parent}{name}/"), + title: title.into(), + description: format!("Documentation for the `{name}` module."), + part: None, + outline: vec![], + body: BodyModel::Symbols(SymbolsModel { + name: title, + details: Html::markdown(resolver, details(name)), + list, + }), + children: vec![], + } +} + +/// Data about a collection of functions. +#[derive(Debug, Deserialize)] +struct GroupData { + name: String, + display: String, + functions: Vec<String>, + description: String, +} + +/// Extract a module from another module. +#[track_caller] +fn module<'a>(parent: &'a Module, name: &str) -> Result<&'a Module, String> { + match parent.scope().get(name) { + Some(Value::Module(module)) => Ok(module), + _ => Err(format!("module doesn't contain module `{name}`")), + } +} + +/// Load YAML from a path. +#[track_caller] +fn yaml<T: DeserializeOwned>(path: &str) -> T { + let file = DOCS.get_file(path).unwrap(); + yaml::from_slice(file.contents()).unwrap() +} + +/// Load details for an identifying key. +#[track_caller] +fn details(key: &str) -> &str { + DETAILS + .get(&yaml::Value::String(key.into())) + .and_then(|value| value.as_str()) + .unwrap_or_else(|| panic!("missing details for {key}")) +} + +/// Turn a title into an URL fragment. +pub fn urlify(title: &str) -> String { + title + .chars() + .map(|c| c.to_ascii_lowercase()) + .map(|c| match c { + 'a'..='z' | '0'..='9' => c, + _ => '-', + }) + .collect() +} + +/// Extract the first line of documentation. +fn oneliner(docs: &str) -> &str { + docs.lines().next().unwrap_or_default() +} + +/// The order of types in the documentation. +fn type_index(ty: &str) -> usize { + TYPE_ORDER.iter().position(|&v| v == ty).unwrap_or(usize::MAX) +} + +const TYPE_ORDER: &[&str] = &[ + "any", + "none", + "auto", + "boolean", + "integer", + "float", + "length", + "angle", + "ratio", + "relative length", + "fraction", + "color", + "datetime", + "string", + "regex", + "label", + "content", + "array", + "dictionary", + "function", + "arguments", + "location", + "dir", + "alignment", + "2d alignment", + "selector", + "stroke", +]; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_docs() { + provide(&TestResolver); + } + + struct TestResolver; + + impl Resolver for TestResolver { + fn link(&self, _: &str) -> Option<String> { + None + } + + fn example(&self, _: Html, _: &[Frame]) -> Html { + Html::new(String::new()) + } + + fn image(&self, _: &str, _: &[u8]) -> String { + String::new() + } + + fn commits(&self, _: &str, _: &str) -> Vec<Commit> { + vec![] + } + } +} diff --git a/crates/typst-library/Cargo.toml b/crates/typst-library/Cargo.toml new file mode 100644 index 00000000..23c37d75 --- /dev/null +++ b/crates/typst-library/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "typst-library" +description = "The standard library for Typst." +categories = ["compilers", "science"] +keywords = ["typst"] +version.workspace = true +rust-version.workspace = true +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true + +[lib] +test = false +doctest = false +bench = false + +[dependencies] +typst = { path = "../typst" } +az = "1.2" +chinese-number = { version = "0.7.2", default-features = false, features = ["number-to-chinese"] } +comemo = "0.3" +csv = "1" +ecow = "0.1" +hayagriva = "0.3" +hypher = "0.1" +icu_properties = { version = "1.2.0", features = ["serde"] } +icu_provider = { version = "1.2.0", features = ["sync"] } +icu_provider_adapters = "1.2.0" +icu_provider_blob = "1.2.0" +icu_segmenter = { version = "1.2.1", features = ["serde"] } +kurbo = "0.9" +lipsum = "0.9" +log = "0.4" +once_cell = "1" +roxmltree = "0.18" +rustybuzz = "0.7" +serde_json = "1" +serde_yaml = "0.8" +smallvec = "1.10" +syntect = { version = "5", default-features = false, features = ["parsing", "regex-fancy"] } +time = { version = "0.3.20", features = ["formatting"] } +toml = { version = "0.7.3", default-features = false, features = ["parse"] } +tracing = "0.1.37" +ttf-parser = "0.18.1" +typed-arena = "2" +unicode-bidi = "0.3.13" +unicode-math-class = "0.1" +unicode-script = "0.5" +unicode-segmentation = "1" diff --git a/crates/typst-library/assets/cj_linebreak_data.postcard b/crates/typst-library/assets/cj_linebreak_data.postcard Binary files differnew file mode 100644 index 00000000..910dd167 --- /dev/null +++ b/crates/typst-library/assets/cj_linebreak_data.postcard diff --git a/crates/typst-library/assets/icudata.postcard b/crates/typst-library/assets/icudata.postcard Binary files differnew file mode 100644 index 00000000..e910bf8e --- /dev/null +++ b/crates/typst-library/assets/icudata.postcard diff --git a/crates/typst-library/assets/syntect.bin b/crates/typst-library/assets/syntect.bin Binary files differnew file mode 100644 index 00000000..043602a4 --- /dev/null +++ b/crates/typst-library/assets/syntect.bin diff --git a/crates/typst-library/src/compute/calc.rs b/crates/typst-library/src/compute/calc.rs new file mode 100644 index 00000000..81715007 --- /dev/null +++ b/crates/typst-library/src/compute/calc.rs @@ -0,0 +1,1024 @@ +//! Calculations and processing of numeric values. + +use std::cmp; +use std::cmp::Ordering; +use std::ops::{Div, Rem}; + +use typst::eval::{Module, Scope}; + +use crate::prelude::*; + +/// A module with computational functions. +pub fn module() -> Module { + let mut scope = Scope::new(); + scope.define("abs", abs_func()); + scope.define("pow", pow_func()); + scope.define("exp", exp_func()); + scope.define("sqrt", sqrt_func()); + scope.define("sin", sin_func()); + scope.define("cos", cos_func()); + scope.define("tan", tan_func()); + scope.define("asin", asin_func()); + scope.define("acos", acos_func()); + scope.define("atan", atan_func()); + scope.define("atan2", atan2_func()); + scope.define("sinh", sinh_func()); + scope.define("cosh", cosh_func()); + scope.define("tanh", tanh_func()); + scope.define("log", log_func()); + scope.define("ln", ln_func()); + scope.define("fact", fact_func()); + scope.define("perm", perm_func()); + scope.define("binom", binom_func()); + scope.define("gcd", gcd_func()); + scope.define("lcm", lcm_func()); + scope.define("floor", floor_func()); + scope.define("ceil", ceil_func()); + scope.define("trunc", trunc_func()); + scope.define("fract", fract_func()); + scope.define("round", round_func()); + scope.define("clamp", clamp_func()); + scope.define("min", min_func()); + scope.define("max", max_func()); + scope.define("even", even_func()); + scope.define("odd", odd_func()); + scope.define("rem", rem_func()); + scope.define("quo", quo_func()); + scope.define("inf", f64::INFINITY); + scope.define("nan", f64::NAN); + scope.define("pi", std::f64::consts::PI); + scope.define("e", std::f64::consts::E); + Module::new("calc").with_scope(scope) +} + +/// Calculates the absolute value of a numeric value. +/// +/// ## Example { #example } +/// ```example +/// #calc.abs(-5) \ +/// #calc.abs(5pt - 2cm) \ +/// #calc.abs(2fr) +/// ``` +/// +/// Display: Absolute +/// Category: calculate +#[func] +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())), +} + +/// Raises a value to some exponent. +/// +/// ## Example { #example } +/// ```example +/// #calc.pow(2, 3) +/// ``` +/// +/// Display: Power +/// Category: calculate +#[func] +pub fn pow( + /// The base of the power. + base: Num, + /// The exponent of the power. + exponent: Spanned<Num>, + /// The callsite span. + span: Span, +) -> SourceResult<Num> { + match exponent.v { + _ if exponent.v.float() == 0.0 && base.float() == 0.0 => { + 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") + } + _ => {} + }; + + let result = match (base, exponent.v) { + (Num::Int(a), Num::Int(b)) if b >= 0 => a + .checked_pow(b as u32) + .map(Num::Int) + .ok_or("the result is too large") + .at(span)?, + (a, b) => Num::Float(if a.float() == std::f64::consts::E { + b.float().exp() + } else if a.float() == 2.0 { + b.float().exp2() + } else if let Num::Int(b) = b { + a.float().powi(b as i32) + } else { + a.float().powf(b.float()) + }), + }; + + if result.float().is_nan() { + bail!(span, "the result is not a real number") + } + + Ok(result) +} + +/// Raises a value to some exponent of e. +/// +/// ## Example { #example } +/// ```example +/// #calc.exp(1) +/// ``` +/// +/// Display: Exponential +/// Category: calculate +#[func] +pub fn exp( + /// The exponent of the power. + exponent: Spanned<Num>, + /// The callsite span. + span: Span, +) -> 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) +} + +/// Extracts the square root of a number. +/// +/// ## Example { #example } +/// ```example +/// #calc.sqrt(16) \ +/// #calc.sqrt(2.5) +/// ``` +/// +/// Display: Square Root +/// Category: calculate +#[func] +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 sine of an angle. +/// +/// When called with an integer or a float, they will be interpreted as +/// radians. +/// +/// ## Example { #example } +/// ```example +/// #assert(calc.sin(90deg) == calc.sin(-270deg)) +/// #calc.sin(1.5) \ +/// #calc.sin(90deg) +/// ``` +/// +/// Display: Sine +/// Category: calculate +#[func] +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 { #example } +/// ```example +/// #calc.cos(90deg) \ +/// #calc.cos(1.5) \ +/// #calc.cos(90deg) +/// ``` +/// +/// Display: Cosine +/// Category: calculate +#[func] +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 { #example } +/// ```example +/// #calc.tan(1.5) \ +/// #calc.tan(90deg) +/// ``` +/// +/// Display: Tangent +/// Category: calculate +#[func] +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 { #example } +/// ```example +/// #calc.asin(0) \ +/// #calc.asin(1) +/// ``` +/// +/// Display: Arcsine +/// Category: calculate +#[func] +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 { #example } +/// ```example +/// #calc.acos(0) \ +/// #calc.acos(1) +/// ``` +/// +/// Display: Arccosine +/// Category: calculate +#[func] +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 { #example } +/// ```example +/// #calc.atan(0) \ +/// #calc.atan(1) +/// ``` +/// +/// Display: Arctangent +/// Category: calculate +#[func] +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 { #example } +/// ```example +/// #calc.atan2(1, 1) \ +/// #calc.atan2(-2, -3) +/// ``` +/// +/// Display: Four-quadrant Arctangent +/// Category: calculate +#[func] +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 an angle. +/// +/// When called with an integer or a float, they will be interpreted as radians. +/// +/// ## Example { #example } +/// ```example +/// #calc.sinh(0) \ +/// #calc.sinh(45deg) +/// ``` +/// +/// Display: Hyperbolic sine +/// Category: calculate +#[func] +pub fn sinh( + /// The angle whose hyperbolic sine to calculate. + angle: AngleLike, +) -> f64 { + match angle { + AngleLike::Angle(a) => a.to_rad().sinh(), + AngleLike::Int(n) => (n as f64).sinh(), + AngleLike::Float(n) => n.sinh(), + } +} + +/// Calculates the hyperbolic cosine of an angle. +/// +/// When called with an integer or a float, they will be interpreted as radians. +/// +/// ## Example { #example } +/// ```example +/// #calc.cosh(0) \ +/// #calc.cosh(45deg) +/// ``` +/// +/// Display: Hyperbolic cosine +/// Category: calculate +#[func] +pub fn cosh( + /// The angle whose hyperbolic cosine to calculate. + angle: AngleLike, +) -> f64 { + match angle { + AngleLike::Angle(a) => a.to_rad().cosh(), + AngleLike::Int(n) => (n as f64).cosh(), + AngleLike::Float(n) => n.cosh(), + } +} + +/// Calculates the hyperbolic tangent of an angle. +/// +/// When called with an integer or a float, they will be interpreted as radians. +/// +/// ## Example { #example } +/// ```example +/// #calc.tanh(0) \ +/// #calc.tanh(45deg) +/// ``` +/// +/// Display: Hyperbolic tangent +/// Category: calculate +#[func] +pub fn tanh( + /// The angle whose hyperbolic tangent to calculate. + angle: AngleLike, +) -> f64 { + match angle { + AngleLike::Angle(a) => a.to_rad().tanh(), + AngleLike::Int(n) => (n as f64).tanh(), + AngleLike::Float(n) => n.tanh(), + } +} + +/// Calculates the logarithm of a number. +/// +/// If the base is not specified, the logarithm is calculated in base 10. +/// +/// ## Example { #example } +/// ```example +/// #calc.log(100) +/// ``` +/// +/// Display: Logarithm +/// Category: calculate +#[func] +pub fn log( + /// 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>, + /// The callsite span. + span: Span, +) -> 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 { #example } +/// ```example +/// #calc.ln(calc.e) +/// ``` +/// +/// Display: Natural Logarithm +/// Category: calculate +#[func] +pub fn ln( + /// The number whose logarithm to calculate. Must be strictly positive. + value: Spanned<Num>, + /// The callsite span. + span: Span, +) -> 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 { #example } +/// ```example +/// #calc.fact(5) +/// ``` +/// +/// Display: Factorial +/// Category: calculate +#[func] +pub fn fact( + /// The number whose factorial to calculate. Must be non-negative. + number: u64, +) -> StrResult<i64> { + Ok(fact_impl(1, number).ok_or("the result is too large")?) +} + +/// Calculates a permutation. +/// +/// ## Example { #example } +/// ```example +/// #calc.perm(10, 5) +/// ``` +/// +/// Display: Permutation +/// Category: calculate +#[func] +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("the result is 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. +/// +/// ## Example { #example } +/// ```example +/// #calc.binom(10, 5) +/// ``` +/// +/// Display: Binomial +/// Category: calculate +#[func] +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("the result is 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 { #example } +/// ```example +/// #calc.gcd(7, 42) +/// ``` +/// +/// Display: Greatest Common Divisor +/// Category: calculate +#[func] +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 { #example } +/// ```example +/// #calc.lcm(96, 13) +/// ``` +/// +/// Display: Least Common Multiple +/// Category: calculate +#[func] +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("the return value is too large")?) +} + +/// Rounds a number down to the nearest integer. +/// +/// If the number is already an integer, it is returned unchanged. +/// +/// ## Example { #example } +/// ```example +/// #assert(calc.floor(3.14) == 3) +/// #assert(calc.floor(3) == 3) +/// #calc.floor(500.1) +/// ``` +/// +/// Display: Round down +/// Category: calculate +#[func] +pub fn floor( + /// The number to round down. + value: Num, +) -> i64 { + match value { + Num::Int(n) => n, + Num::Float(n) => n.floor() as i64, + } +} + +/// Rounds a number up to the nearest integer. +/// +/// If the number is already an integer, it is returned unchanged. +/// +/// ## Example { #example } +/// ```example +/// #assert(calc.ceil(3.14) == 4) +/// #assert(calc.ceil(3) == 3) +/// #calc.ceil(500.1) +/// ``` +/// +/// Display: Round up +/// Category: calculate +#[func] +pub fn ceil( + /// The number to round up. + value: Num, +) -> i64 { + match value { + Num::Int(n) => n, + Num::Float(n) => n.ceil() as i64, + } +} + +/// Returns the integer part of a number. +/// +/// If the number is already an integer, it is returned unchanged. +/// +/// ## Example { #example } +/// ```example +/// #assert(calc.trunc(3) == 3) +/// #assert(calc.trunc(-3.7) == -3) +/// #assert(calc.trunc(15.9) == 15) +/// ``` +/// +/// Display: Truncate +/// Category: calculate +#[func] +pub fn trunc( + /// The number to truncate. + value: Num, +) -> i64 { + match value { + Num::Int(n) => n, + Num::Float(n) => n.trunc() as i64, + } +} + +/// Returns the fractional part of a number. +/// +/// If the number is an integer, returns `0`. +/// +/// ## Example { #example } +/// ```example +/// #assert(calc.fract(3) == 0) +/// #calc.fract(-3.1) +/// ``` +/// +/// Display: Fractional +/// Category: calculate +#[func] +pub fn fract( + /// The number to truncate. + value: Num, +) -> Num { + match value { + Num::Int(_) => Num::Int(0), + Num::Float(n) => Num::Float(n.fract()), + } +} + +/// Rounds a number to the nearest integer. +/// +/// Optionally, a number of decimal places can be specified. +/// +/// ## Example { #example } +/// ```example +/// #assert(calc.round(3.14) == 3) +/// #assert(calc.round(3.5) == 4) +/// #calc.round(3.1415, digits: 2) +/// ``` +/// +/// Display: Round +/// Category: calculate +#[func] +pub fn round( + /// The number to round. + value: Num, + /// The number of decimal places. + #[named] + #[default(0)] + digits: i64, +) -> Num { + match value { + Num::Int(n) if digits == 0 => Num::Int(n), + _ => { + let n = value.float(); + let factor = 10.0_f64.powi(digits as i32); + Num::Float((n * factor).round() / factor) + } + } +} + +/// Clamps a number between a minimum and maximum value. +/// +/// ## Example { #example } +/// ```example +/// #assert(calc.clamp(5, 0, 10) == 5) +/// #assert(calc.clamp(5, 6, 10) == 6) +/// #calc.clamp(5, 0, 4) +/// ``` +/// +/// Display: Clamp +/// Category: calculate +#[func] +pub fn clamp( + /// The number to clamp. + value: Num, + /// The inclusive minimum value. + min: Num, + /// The inclusive maximum value. + max: Spanned<Num>, +) -> SourceResult<Num> { + if max.v.float() < min.float() { + bail!(max.span, "max must be greater than or equal to min") + } + Ok(value.apply3(min, max.v, i64::clamp, f64::clamp)) +} + +/// Determines the minimum of a sequence of values. +/// +/// ## Example { #example } +/// ```example +/// #calc.min(1, -3, -5, 20, 3, 6) \ +/// #calc.min("typst", "in", "beta") +/// ``` +/// +/// Display: Minimum +/// Category: calculate +#[func] +pub fn min( + /// The sequence of values from which to extract the minimum. + /// Must not be empty. + #[variadic] + values: Vec<Spanned<Value>>, + /// The callsite span. + span: Span, +) -> SourceResult<Value> { + minmax(span, values, Ordering::Less) +} + +/// Determines the maximum of a sequence of values. +/// +/// ## Example { #example } +/// ```example +/// #calc.max(1, -3, -5, 20, 3, 6) \ +/// #calc.max("typst", "in", "beta") +/// ``` +/// +/// Display: Maximum +/// Category: calculate +#[func] +pub fn max( + /// The sequence of values from which to extract the maximum. + /// Must not be empty. + #[variadic] + values: Vec<Spanned<Value>>, + /// The callsite span. + span: Span, +) -> 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 = typst::eval::ops::compare(&v, &extremum).at(span)?; + if ordering == goal { + extremum = v; + } + } + + Ok(extremum) +} + +/// Determines whether an integer is even. +/// +/// ## Example { #example } +/// ```example +/// #calc.even(4) \ +/// #calc.even(5) \ +/// #range(10).filter(calc.even) +/// ``` +/// +/// Display: Even +/// Category: calculate +#[func] +pub fn even( + /// The number to check for evenness. + value: i64, +) -> bool { + value % 2 == 0 +} + +/// Determines whether an integer is odd. +/// +/// ## Example { #example } +/// ```example +/// #calc.odd(4) \ +/// #calc.odd(5) \ +/// #range(10).filter(calc.odd) +/// ``` +/// +/// Display: Odd +/// Category: calculate +#[func] +pub fn odd( + /// The number to check for oddness. + value: i64, +) -> bool { + value % 2 != 0 +} + +/// Calculates the remainder of two numbers. +/// +/// ## Example { #example } +/// ```example +/// #calc.rem(20, 6) \ +/// #calc.rem(1.75, 0.5) +/// ``` +/// +/// Display: Remainder +/// Category: calculate +#[func] +pub fn rem( + /// The dividend of the remainder. + dividend: Num, + /// The divisor of the remainder. + divisor: Spanned<Num>, +) -> SourceResult<Num> { + if divisor.v.float() == 0.0 { + bail!(divisor.span, "divisor must not be zero"); + } + Ok(dividend.apply2(divisor.v, Rem::rem, Rem::rem)) +} + +/// Calculates the quotient of two numbers. +/// +/// ## Example { #example } +/// ```example +/// #calc.quo(14, 5) \ +/// #calc.quo(3.46, 0.5) +/// ``` +/// +/// Display: Quotient +/// Category: calculate +#[func] +pub fn quo( + /// The dividend of the quotient. + dividend: Num, + /// The divisor of the quotient. + divisor: Spanned<Num>, +) -> SourceResult<i64> { + if divisor.v.float() == 0.0 { + bail!(divisor.span, "divisor must not be zero"); + } + + Ok(floor(dividend.apply2(divisor.v, Div::div, Div::div))) +} + +/// 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 { + pub fn apply2( + self, + other: Self, + int: impl FnOnce(i64, i64) -> i64, + float: impl FnOnce(f64, f64) -> f64, + ) -> Num { + match (self, other) { + (Self::Int(a), Self::Int(b)) => Num::Int(int(a, b)), + (a, b) => Num::Float(float(a.float(), b.float())), + } + } + + pub fn apply3( + self, + other: Self, + third: Self, + int: impl FnOnce(i64, i64, i64) -> i64, + float: impl FnOnce(f64, f64, f64) -> f64, + ) -> Num { + match (self, other, third) { + (Self::Int(a), Self::Int(b), Self::Int(c)) => Num::Int(int(a, b, c)), + (a, b, c) => Num::Float(float(a.float(), b.float(), c.float())), + } + } + + pub 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 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), +} diff --git a/crates/typst-library/src/compute/construct.rs b/crates/typst-library/src/compute/construct.rs new file mode 100644 index 00000000..956212ee --- /dev/null +++ b/crates/typst-library/src/compute/construct.rs @@ -0,0 +1,743 @@ +use std::num::NonZeroI64; +use std::str::FromStr; + +use time::{Month, PrimitiveDateTime}; + +use typst::eval::{Datetime, Regex}; + +use crate::prelude::*; + +/// Converts a value to an integer. +/// +/// - Booleans are converted to `0` or `1`. +/// - Floats are floored to the next 64-bit integer. +/// - Strings are parsed in base 10. +/// +/// ## Example { #example } +/// ```example +/// #int(false) \ +/// #int(true) \ +/// #int(2.7) \ +/// #{ int("27") + int("4") } +/// ``` +/// +/// Display: Integer +/// Category: construct +#[func] +pub fn int( + /// The value that should be converted to an integer. + value: ToInt, +) -> i64 { + value.0 +} + +/// A value that can be cast to an integer. +pub struct ToInt(i64); + +cast! { + ToInt, + v: bool => Self(v as i64), + v: i64 => Self(v), + v: f64 => Self(v as i64), + v: EcoString => Self(v.parse().map_err(|_| eco_format!("invalid integer: {}", v))?), +} + +/// Converts a value to a float. +/// +/// - Booleans are converted to `0.0` or `1.0`. +/// - Integers are converted to the closest 64-bit float. +/// - Ratios are divided by 100%. +/// - Strings are parsed in base 10 to the closest 64-bit float. +/// Exponential notation is supported. +/// +/// ## Example { #example } +/// ```example +/// #float(false) \ +/// #float(true) \ +/// #float(4) \ +/// #float(40%) \ +/// #float("2.7") \ +/// #float("1e5") +/// ``` +/// +/// Display: Float +/// Category: construct +#[func] +pub fn float( + /// The value that should be converted to a float. + value: ToFloat, +) -> f64 { + value.0 +} + +/// A value that can be cast to a float. +pub struct ToFloat(f64); + +cast! { + ToFloat, + v: bool => Self(v as i64 as f64), + v: i64 => Self(v as f64), + v: f64 => Self(v), + v: Ratio => Self(v.get()), + v: EcoString => Self(v.parse().map_err(|_| eco_format!("invalid float: {}", v))?), +} + +/// Creates a grayscale color. +/// +/// ## Example { #example } +/// ```example +/// #for x in range(250, step: 50) { +/// box(square(fill: luma(x))) +/// } +/// ``` +/// +/// Display: Luma +/// Category: construct +#[func] +pub fn luma( + /// The gray component. + gray: Component, +) -> Color { + LumaColor::new(gray.0).into() +} + +/// Creates an RGB(A) color. +/// +/// The color is specified in the sRGB color space. +/// +/// _Note:_ While you can specify transparent colors and Typst's preview will +/// render them correctly, the PDF export does not handle them properly at the +/// moment. This will be fixed in the future. +/// +/// ## Example { #example } +/// ```example +/// #square(fill: rgb("#b1f2eb")) +/// #square(fill: rgb(87, 127, 230)) +/// #square(fill: rgb(25%, 13%, 65%)) +/// ``` +/// +/// Display: RGB +/// Category: construct +#[func] +pub fn rgb( + /// The color in hexadecimal notation. + /// + /// Accepts three, four, six or eight hexadecimal digits and optionally + /// a leading hashtag. + /// + /// If this string is given, the individual components should not be given. + /// + /// ```example + /// #text(16pt, rgb("#239dad"))[ + /// *Typst* + /// ] + /// ``` + #[external] + hex: EcoString, + /// The red component. + #[external] + red: Component, + /// The green component. + #[external] + green: Component, + /// The blue component. + #[external] + blue: Component, + /// The alpha component. + #[external] + alpha: Component, + /// The arguments. + args: Args, +) -> SourceResult<Color> { + let mut args = args; + Ok(if let Some(string) = args.find::<Spanned<EcoString>>()? { + match RgbaColor::from_str(&string.v) { + Ok(color) => color.into(), + Err(msg) => bail!(string.span, "{msg}"), + } + } 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(255)); + RgbaColor::new(r, g, b, a).into() + }) +} + +/// An integer or ratio component. +pub struct Component(u8); + +cast! { + Component, + v: i64 => match v { + 0 ..= 255 => Self(v as u8), + _ => bail!("number must be between 0 and 255"), + }, + v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) { + Self((v.get() * 255.0).round() as u8) + } else { + bail!("ratio must be between 0% and 100%"); + }, +} + +/// Creates a new datetime. +/// +/// You can specify the [datetime]($type/datetime) using a year, month, day, +/// hour, minute, and second. You can also get the current date with +/// [`datetime.today`]($func/datetime.today). +/// +/// ## Example +/// ```example +/// #let date = datetime( +/// year: 2012, +/// month: 8, +/// day: 3, +/// ) +/// +/// #date.display() \ +/// #date.display( +/// "[day].[month].[year]" +/// ) +/// ``` +/// +/// ## Format +/// _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`]($type/datetime.display) +/// method will choose a different formatting by default. +/// +/// Display: Datetime +/// Category: construct +#[func] +#[scope( + scope.define("today", datetime_today_func()); + scope +)] +pub fn datetime( + /// The year of the datetime. + #[named] + year: Option<YearComponent>, + /// The month of the datetime. + #[named] + month: Option<MonthComponent>, + /// The day of the datetime. + #[named] + day: Option<DayComponent>, + /// The hour of the datetime. + #[named] + hour: Option<HourComponent>, + /// The minute of the datetime. + #[named] + minute: Option<MinuteComponent>, + /// The second of the datetime. + #[named] + second: Option<SecondComponent>, +) -> StrResult<Datetime> { + let time = match (hour, minute, second) { + (Some(hour), Some(minute), Some(second)) => { + match time::Time::from_hms(hour.0, minute.0, second.0) { + 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.0, month.0, day.0) { + 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") + } + }) +} + +pub struct YearComponent(i32); +pub struct MonthComponent(Month); +pub struct DayComponent(u8); +pub struct HourComponent(u8); +pub struct MinuteComponent(u8); +pub struct SecondComponent(u8); + +cast! { + YearComponent, + v: i32 => Self(v), +} + +cast! { + MonthComponent, + v: u8 => Self(Month::try_from(v).map_err(|_| "month is invalid")?) +} + +cast! { + DayComponent, + v: u8 => Self(v), +} + +cast! { + HourComponent, + v: u8 => Self(v), +} + +cast! { + MinuteComponent, + v: u8 => Self(v), +} + +cast! { + SecondComponent, + v: u8 => Self(v), +} + +/// Returns the current date. +/// +/// Refer to the documentation of the [`display`]($type/datetime.display) method +/// for details on how to affect the formatting of the date. +/// +/// ## Example +/// ```example +/// Today's date is +/// #datetime.today().display(). +/// ``` +/// +/// Display: Today +/// Category: construct +#[func] +pub fn datetime_today( + /// 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>, + /// The virtual machine. + vt: &mut Vt, +) -> StrResult<Datetime> { + Ok(vt + .world + .today(offset.as_custom()) + .ok_or("unable to get the current date")?) +} + +/// Creates 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. +/// +/// ## Example { #example } +/// ```example +/// #square( +/// fill: cmyk(27%, 0%, 3%, 5%) +/// ) +/// ```` +/// +/// Display: CMYK +/// Category: construct +#[func] +pub fn cmyk( + /// The cyan component. + cyan: RatioComponent, + /// The magenta component. + magenta: RatioComponent, + /// The yellow component. + yellow: RatioComponent, + /// The key component. + key: RatioComponent, +) -> Color { + CmykColor::new(cyan.0, magenta.0, yellow.0, key.0).into() +} + +/// A component that must be a ratio. +pub struct RatioComponent(u8); + +cast! { + RatioComponent, + v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) { + Self((v.get() * 255.0).round() as u8) + } else { + bail!("ratio must be between 0% and 100%"); + }, +} + +/// Creates a custom symbol with modifiers. +/// +/// ## Example { #example } +/// ```example +/// #let envelope = symbol( +/// "🖂", +/// ("stamped", "🖃"), +/// ("stamped.pen", "🖆"), +/// ("lightning", "🖄"), +/// ("fly", "🖅"), +/// ) +/// +/// #envelope +/// #envelope.stamped +/// #envelope.stamped.pen +/// #envelope.lightning +/// #envelope.fly +/// ``` +/// +/// Display: Symbol +/// Category: construct +#[func] +pub fn symbol( + /// 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<Variant>>, + /// The callsite span. + span: Span, +) -> 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, v.1)); + } + Ok(Symbol::runtime(list.into_boxed_slice())) +} + +/// A value that can be cast to a symbol. +pub struct Variant(EcoString, char); + +cast! { + Variant, + 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()?), + _ => bail!("point array must contain exactly two entries"), + } + }, +} + +/// 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. +/// +/// If you wish to convert from and to Unicode code points, see +/// [`str.to-unicode`]($func/str.to-unicode) and +/// [`str.from-unicode`]($func/str.from-unicode). +/// +/// ## Example { #example } +/// ```example +/// #str(10) \ +/// #str(4000, base: 16) \ +/// #str(2.7) \ +/// #str(1e8) \ +/// #str(<intro>) +/// ``` +/// +/// Display: String +/// Category: construct +#[func] +#[scope( + scope.define("to-unicode", str_to_unicode_func()); + scope.define("from-unicode", str_from_unicode_func()); + scope +)] +pub fn str( + /// 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"); + } + int_to_base(n, base.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(format_str!("{}", v)), + v: Label => Self::Str(v.0.into()), + v: Str => Self::Str(v), +} + +/// Format an integer in a base. +fn int_to_base(mut n: i64, base: i64) -> EcoString { + if n == 0 { + return "0".into(); + } + + // In Rust, `format!("{:x}", -14i64)` is not `-e` but `fffffffffffffff2`. + // So we can only use the built-in for decimal, not bin/oct/hex. + if base == 10 { + return eco_format!("{n}"); + } + + // The largest output is `to_base(i64::MIN, 2)`, which is 65 chars long. + const SIZE: usize = 65; + 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 { + i -= 1; + digits[i] = b'-'; + } + + std::str::from_utf8(&digits[i..]).unwrap_or_default().into() +} + +/// Converts a character into its corresponding code point. +/// +/// ## Example +/// ```example +/// #str.to-unicode("a") \ +/// #"a\u{0300}".codepoints().map(str.to-unicode) +/// ``` +/// +/// Display: String To Unicode +/// Category: construct +#[func] +pub fn str_to_unicode( + /// The character that should be converted. + value: char, +) -> u32 { + value.into() +} + +/// Converts a Unicode code point into its corresponding string. +/// +/// ```example +/// #str.from-unicode(97) +/// ``` +/// +/// Display: String From Unicode +/// Category: construct +#[func] +pub fn str_from_unicode( + /// The code point that should be converted. + value: CodePoint, +) -> Str { + format_str!("{}", value.0) +} + +/// The numeric representation of a single unicode code point. +pub struct CodePoint(char); + +cast! { + CodePoint, + v: i64 => { + Self(v.try_into().ok().and_then(|v: u32| v.try_into().ok()).ok_or_else( + || eco_format!("{:#x} is not a valid codepoint", v), + )?) + }, +} + +/// Creates a label from a string. +/// +/// Inserting a label into content attaches it to the closest previous element +/// that is not a space. Then, the element can be [referenced]($func/ref) and +/// styled through the label. +/// +/// ## Example { #example } +/// ```example +/// #show <a>: set text(blue) +/// #show label("b"): set text(red) +/// +/// = Heading <a> +/// *Strong* #label("b") +/// ``` +/// +/// ## Syntax { #syntax } +/// This function also has dedicated syntax: You can create a label by enclosing +/// its name in angle brackets. This works both in markup and code. +/// +/// Display: Label +/// Category: construct +#[func] +pub fn label( + /// The name of the label. + name: EcoString, +) -> Label { + Label(name) +} + +/// Creates a regular expression from a string. +/// +/// The result can be used as a +/// [show rule selector]($styling/#show-rules) and with +/// [string methods]($type/string) like `find`, `split`, and `replace`. +/// +/// See [the specification of the supported syntax](https://docs.rs/regex/latest/regex/#syntax). +/// +/// ## Example { #example } +/// ```example +/// // Works with show rules. +/// #show regex("\d+"): set text(red) +/// +/// The numbers 1 to 10. +/// +/// // Works with string methods. +/// #("a,b;c" +/// .split(regex("[,;]"))) +/// ``` +/// +/// Display: Regex +/// Category: construct +#[func] +pub fn regex( + /// 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<EcoString>, +) -> SourceResult<Regex> { + Regex::new(®ex.v).at(regex.span) +} + +/// Creates an array consisting of consecutive integers. +/// +/// 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. +/// +/// ## Example { #example } +/// ```example +/// #range(5) \ +/// #range(2, 5) \ +/// #range(20, step: 4) \ +/// #range(21, step: 4) \ +/// #range(5, 2, step: -1) +/// ``` +/// +/// Display: Range +/// Category: construct +#[func] +pub fn range( + /// 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, + /// The arguments. + args: Args, +) -> SourceResult<Array> { + let mut args = args; + 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 = Array::new(); + + while x.cmp(&end) == 0.cmp(&step) { + array.push(Value::Int(x)); + x += step; + } + + Ok(array) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_to_base() { + assert_eq!(&int_to_base(0, 10), "0"); + assert_eq!(&int_to_base(0, 16), "0"); + assert_eq!(&int_to_base(0, 36), "0"); + assert_eq!( + &int_to_base(i64::MAX, 2), + "111111111111111111111111111111111111111111111111111111111111111" + ); + assert_eq!( + &int_to_base(i64::MIN, 2), + "-1000000000000000000000000000000000000000000000000000000000000000" + ); + assert_eq!(&int_to_base(i64::MAX, 10), "9223372036854775807"); + assert_eq!(&int_to_base(i64::MIN, 10), "-9223372036854775808"); + assert_eq!(&int_to_base(i64::MAX, 16), "7fffffffffffffff"); + assert_eq!(&int_to_base(i64::MIN, 16), "-8000000000000000"); + assert_eq!(&int_to_base(i64::MAX, 36), "1y2p0ij32e8e7"); + assert_eq!(&int_to_base(i64::MIN, 36), "-1y2p0ij32e8e8"); + } +} diff --git a/crates/typst-library/src/compute/data.rs b/crates/typst-library/src/compute/data.rs new file mode 100644 index 00000000..6e3a298e --- /dev/null +++ b/crates/typst-library/src/compute/data.rs @@ -0,0 +1,492 @@ +use typst::diag::{format_xml_like_error, FileError}; +use typst::eval::Datetime; + +use crate::prelude::*; + +/// Reads plain text from a file. +/// +/// The file will be read and returned as a string. +/// +/// ## Example { #example } +/// ```example +/// #let text = read("data.html") +/// +/// An example for a HTML file:\ +/// #raw(text, lang: "html") +/// ``` +/// +/// Display: Read +/// Category: data-loading +#[func] +pub fn read( + /// Path to a file. + path: Spanned<EcoString>, + /// The virtual machine. + vm: &mut Vm, +) -> SourceResult<Str> { + let Spanned { v: path, span } = path; + let id = vm.location().join(&path).at(span)?; + let data = vm.world().file(id).at(span)?; + let text = std::str::from_utf8(&data) + .map_err(|_| "file is not valid utf-8") + .at(span)?; + Ok(text.into()) +} + +/// 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 } +/// ```example +/// #let results = csv("data.csv") +/// +/// #table( +/// columns: 2, +/// [*Condition*], [*Result*], +/// ..results.flatten(), +/// ) +/// ``` +/// +/// Display: CSV +/// Category: data-loading +#[func] +pub fn csv( + /// Path to a CSV file. + path: Spanned<EcoString>, + /// The delimiter that separates columns in the CSV file. + /// Must be a single ASCII character. + #[named] + #[default] + delimiter: Delimiter, + /// The virtual machine. + vm: &mut Vm, +) -> SourceResult<Array> { + let Spanned { v: path, span } = path; + let id = vm.location().join(&path).at(span)?; + let data = vm.world().file(id).at(span)?; + + let mut builder = csv::ReaderBuilder::new(); + builder.has_headers(false); + builder.delimiter(delimiter.0 as u8); + + let mut reader = builder.from_reader(data.as_slice()); + let mut array = Array::new(); + + for (line, result) in reader.records().enumerate() { + // Original solution 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 + 1; // Counting lines from 1 + let row = result.map_err(|err| format_csv_error(err, line)).at(span)?; + let sub = row.into_iter().map(|field| field.into_value()).collect(); + array.push(Value::Array(sub)) + } + + 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) + }, +} + +/// Format the user-facing CSV error message. +fn format_csv_error(error: csv::Error, line: usize) -> EcoString { + match error.kind() { + csv::ErrorKind::Utf8 { .. } => "file is not valid utf-8".into(), + csv::ErrorKind::UnequalLengths { expected_len, len, .. } => { + eco_format!( + "failed to parse csv file: found {len} instead of {expected_len} fields in line {line}" + ) + } + _ => "failed to parse csv file".into(), + } +} + +/// Reads structured data from a JSON file. +/// +/// The file must contain a valid JSON 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. +/// +/// The function returns a dictionary or an array, depending on the JSON file. +/// +/// The JSON files in the example contain objects with the keys `temperature`, +/// `unit`, and `weather`. +/// +/// ## Example { #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")) +/// ``` +/// +/// Display: JSON +/// Category: data-loading +#[func] +pub fn json( + /// Path to a JSON file. + path: Spanned<EcoString>, + /// The virtual machine. + vm: &mut Vm, +) -> SourceResult<Value> { + let Spanned { v: path, span } = path; + let id = vm.location().join(&path).at(span)?; + let data = vm.world().file(id).at(span)?; + let value: serde_json::Value = + serde_json::from_slice(&data).map_err(format_json_error).at(span)?; + Ok(convert_json(value)) +} + +/// Convert a JSON value to a Typst value. +fn convert_json(value: serde_json::Value) -> Value { + match value { + serde_json::Value::Null => Value::None, + serde_json::Value::Bool(v) => v.into_value(), + serde_json::Value::Number(v) => match v.as_i64() { + Some(int) => int.into_value(), + None => v.as_f64().unwrap_or(f64::NAN).into_value(), + }, + serde_json::Value::String(v) => v.into_value(), + serde_json::Value::Array(v) => { + v.into_iter().map(convert_json).collect::<Array>().into_value() + } + serde_json::Value::Object(v) => v + .into_iter() + .map(|(key, value)| (key.into(), convert_json(value))) + .collect::<Dict>() + .into_value(), + } +} + +/// Format the user-facing JSON error message. +fn format_json_error(error: serde_json::Error) -> EcoString { + assert!(error.is_syntax() || error.is_eof()); + eco_format!("failed to parse json file: syntax error in line {}", error.line()) +} + +/// 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 and booleans will be converted into the Typst +/// equivalents and numbers will be converted to floats or integers depending on +/// whether they are whole numbers. For the time being, datetimes will be +/// converted to strings as Typst does not have a built-in datetime yet. +/// +/// The TOML file in the example consists of a table with the keys `title`, +/// `version`, and `authors`. +/// +/// ## Example { #example } +/// ```example +/// #let details = toml("details.toml") +/// +/// Title: #details.title \ +/// Version: #details.version \ +/// Authors: #(details.authors +/// .join(", ", last: " and ")) +/// ``` +/// +/// Display: TOML +/// Category: data-loading +#[func] +pub fn toml( + /// Path to a TOML file. + path: Spanned<EcoString>, + /// The virtual machine. + vm: &mut Vm, +) -> SourceResult<Value> { + let Spanned { v: path, span } = path; + let id = vm.location().join(&path).at(span)?; + let data = vm.world().file(id).at(span)?; + + let raw = std::str::from_utf8(&data) + .map_err(|_| "file is not valid utf-8") + .at(span)?; + + let value: toml::Value = toml::from_str(raw).map_err(format_toml_error).at(span)?; + Ok(convert_toml(value)) +} + +/// Convert a TOML value to a Typst value. +fn convert_toml(value: toml::Value) -> Value { + match value { + toml::Value::String(v) => v.into_value(), + toml::Value::Integer(v) => v.into_value(), + toml::Value::Float(v) => v.into_value(), + toml::Value::Boolean(v) => v.into_value(), + toml::Value::Array(v) => { + v.into_iter().map(convert_toml).collect::<Array>().into_value() + } + toml::Value::Table(v) => v + .into_iter() + .map(|(key, value)| (key.into(), convert_toml(value))) + .collect::<Dict>() + .into_value(), + toml::Value::Datetime(v) => match (v.date, v.time) { + (None, None) => Value::None, + (Some(date), None) => { + Datetime::from_ymd(date.year as i32, date.month, date.day).into_value() + } + (None, Some(time)) => { + Datetime::from_hms(time.hour, time.minute, time.second).into_value() + } + (Some(date), Some(time)) => Datetime::from_ymd_hms( + date.year as i32, + date.month, + date.day, + time.hour, + time.minute, + time.second, + ) + .into_value(), + }, + } +} + +/// Format the user-facing TOML error message. +fn format_toml_error(error: toml::de::Error) -> EcoString { + if let Some(range) = error.span() { + eco_format!( + "failed to parse toml file: {}, index {}-{}", + error.message(), + range.start, + range.end + ) + } else { + eco_format!("failed to parse toml file: {}", error.message()) + } +} + +/// 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. +/// +/// Note that mapping keys that are not a string cause the entry to be +/// discarded. +/// +/// Custom YAML tags are ignored, though the loaded value will still be +/// present. +/// +/// The function returns a dictionary or value or an array, depending on +/// the YAML file. +/// +/// 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 } +/// ```example +/// #let bookshelf(contents) = { +/// for (author, works) in contents { +/// author +/// for work in works [ +/// - #work.title (#work.published) +/// ] +/// } +/// } +/// +/// #bookshelf( +/// yaml("scifi-authors.yaml") +/// ) +/// ``` +/// +/// Display: YAML +/// Category: data-loading +#[func] +pub fn yaml( + /// Path to a YAML file. + path: Spanned<EcoString>, + /// The virtual machine. + vm: &mut Vm, +) -> SourceResult<Value> { + let Spanned { v: path, span } = path; + let id = vm.location().join(&path).at(span)?; + let data = vm.world().file(id).at(span)?; + let value: serde_yaml::Value = + serde_yaml::from_slice(&data).map_err(format_yaml_error).at(span)?; + Ok(convert_yaml(value)) +} + +/// Convert a YAML value to a Typst value. +fn convert_yaml(value: serde_yaml::Value) -> Value { + match value { + serde_yaml::Value::Null => Value::None, + serde_yaml::Value::Bool(v) => v.into_value(), + serde_yaml::Value::Number(v) => match v.as_i64() { + Some(int) => int.into_value(), + None => v.as_f64().unwrap_or(f64::NAN).into_value(), + }, + serde_yaml::Value::String(v) => v.into_value(), + serde_yaml::Value::Sequence(v) => { + v.into_iter().map(convert_yaml).collect::<Array>().into_value() + } + serde_yaml::Value::Mapping(v) => v + .into_iter() + .map(|(key, value)| (convert_yaml_key(key), convert_yaml(value))) + .filter_map(|(key, value)| key.map(|key| (key, value))) + .collect::<Dict>() + .into_value(), + } +} + +/// Converts an arbitrary YAML mapping key into a Typst Dict Key. +/// Currently it only does so for strings, everything else +/// returns None +fn convert_yaml_key(key: serde_yaml::Value) -> Option<Str> { + match key { + serde_yaml::Value::String(v) => Some(Str::from(v)), + _ => None, + } +} + +/// Format the user-facing YAML error message. +fn format_yaml_error(error: serde_yaml::Error) -> EcoString { + eco_format!("failed to parse yaml file: {}", error.to_string().trim()) +} + +/// 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 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 } +/// ```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) +/// } +/// } +/// ``` +/// +/// Display: XML +/// Category: data-loading +#[func] +pub fn xml( + /// Path to an XML file. + path: Spanned<EcoString>, + /// The virtual machine. + vm: &mut Vm, +) -> SourceResult<Value> { + let Spanned { v: path, span } = path; + let id = vm.location().join(&path).at(span)?; + let data = vm.world().file(id).at(span)?; + let text = std::str::from_utf8(&data).map_err(FileError::from).at(span)?; + let document = roxmltree::Document::parse(text).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 file", error) +} diff --git a/crates/typst-library/src/compute/foundations.rs b/crates/typst-library/src/compute/foundations.rs new file mode 100644 index 00000000..f83d71a0 --- /dev/null +++ b/crates/typst-library/src/compute/foundations.rs @@ -0,0 +1,215 @@ +use crate::prelude::*; + +/// Determines the type of a value. +/// +/// Returns the name of the value's type. +/// +/// ## Example { #example } +/// ```example +/// #type(12) \ +/// #type(14.7) \ +/// #type("hello") \ +/// #type(none) \ +/// #type([Hi]) \ +/// #type(x => x + 1) +/// ``` +/// +/// Display: Type +/// Category: foundations +#[func] +pub fn type_( + /// The value whose type's to determine. + value: Value, +) -> Str { + value.type_name().into() +} + +/// 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 } +/// ```example +/// #none vs #repr(none) \ +/// #"hello" vs #repr("hello") \ +/// #(1, 2) vs #repr((1, 2)) \ +/// #[*Hi*] vs #repr([*Hi*]) +/// ``` +/// +/// Display: Representation +/// Category: foundations +#[func] +pub fn repr( + /// The value whose string representation to produce. + value: Value, +) -> Str { + value.repr() +} + +/// Fails with an error. +/// +/// ## Example { #example } +/// The code below produces the error `panicked with: "this is wrong"`. +/// ```typ +/// #panic("this is wrong") +/// ``` +/// +/// Display: Panic +/// Category: foundations +#[func] +pub fn panic( + /// The values to panic with. + #[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`]($func/assert.eq) and [`assert.ne`]($func/assert.ne). +/// +/// ## Example { #example } +/// ```typ +/// #assert(1 < 2, message: "math broke") +/// ``` +/// +/// Display: Assert +/// Category: foundations +#[func] +#[scope( + scope.define("eq", assert_eq_func()); + scope.define("ne", assert_ne_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) +} + +/// 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. +/// +/// ## Example { #example } +/// ```typ +/// #assert.eq(10, 10) +/// ``` +/// +/// Display: Assert Equals +/// Category: foundations +#[func] +pub fn assert_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 {left:?} was not equal to {right:?}"); + } + } + 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. +/// +/// ## Example { #example } +/// ```typ +/// #assert.ne(3, 4) +/// ``` +/// +/// Display: Assert Not Equals +/// Category: foundations +#[func] +pub fn assert_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 {left:?} was equal to {right:?}"); + } + } + Ok(NoneValue) +} + +/// Evaluates a string as Typst code. +/// +/// This function should only be used as a last resort. +/// +/// ## Example { #example } +/// ```example +/// #eval("1 + 1") \ +/// #eval("(1, 2, 3, 4)").len() \ +/// #eval("[*Strong text*]") +/// ``` +/// +/// Display: Evaluate +/// Category: foundations +#[func] +pub fn eval( + /// A string of Typst code to evaluate. + /// + /// The code in the string cannot interact with the file system. + source: Spanned<String>, + /// The virtual machine. + vm: &mut Vm, +) -> SourceResult<Value> { + let Spanned { v: text, span } = source; + typst::eval::eval_string(vm.world(), &text, span) +} diff --git a/crates/typst-library/src/compute/mod.rs b/crates/typst-library/src/compute/mod.rs new file mode 100644 index 00000000..e9e4870c --- /dev/null +++ b/crates/typst-library/src/compute/mod.rs @@ -0,0 +1,39 @@ +//! Computational functions. + +pub mod calc; +mod construct; +mod data; +mod foundations; + +pub use self::construct::*; +pub use self::data::*; +pub use self::foundations::*; + +use crate::prelude::*; + +/// Hook up all compute definitions. +pub(super) fn define(global: &mut Scope) { + global.define("type", type_func()); + global.define("repr", repr_func()); + global.define("panic", panic_func()); + global.define("assert", assert_func()); + global.define("eval", eval_func()); + global.define("int", int_func()); + global.define("float", float_func()); + global.define("luma", luma_func()); + global.define("rgb", rgb_func()); + global.define("cmyk", cmyk_func()); + global.define("datetime", datetime_func()); + global.define("symbol", symbol_func()); + global.define("str", str_func()); + global.define("label", label_func()); + global.define("regex", regex_func()); + global.define("range", range_func()); + global.define("read", read_func()); + global.define("csv", csv_func()); + global.define("json", json_func()); + global.define("toml", toml_func()); + global.define("yaml", yaml_func()); + global.define("xml", xml_func()); + global.define("calc", calc::module()); +} diff --git a/crates/typst-library/src/layout/align.rs b/crates/typst-library/src/layout/align.rs new file mode 100644 index 00000000..bbfe9f7e --- /dev/null +++ b/crates/typst-library/src/layout/align.rs @@ -0,0 +1,67 @@ +use crate::prelude::*; + +/// Aligns content horizontally and vertically. +/// +/// ## Example { #example } +/// ```example +/// #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 +/// ``` +/// +/// Display: Align +/// Category: layout +#[element(Show)] +pub struct AlignElem { + /// The alignment along both axes. + /// + /// Possible values for horizontal alignments are: + /// - `start` + /// - `end` + /// - `left` + /// - `center` + /// - `right` + /// + /// The `start` and `end` alignments are relative to the current [text + /// direction]($func/text.dir). + /// + /// Possible values for vertical alignments are: + /// - `top` + /// - `horizon` + /// - `bottom` + /// + /// To align along both axes at the same time, add the two alignments using + /// the `+` operator to get a `2d alignment`. For example, `top + right` + /// aligns the content to the top right corner. + /// + /// ```example + /// #set page(height: 6cm) + /// #set text(lang: "ar") + /// + /// مثال + /// #align( + /// end + horizon, + /// rect(inset: 12pt)[ركن] + /// ) + /// ``` + #[positional] + #[fold] + #[default(Axes::new(GenAlign::Start, GenAlign::Specific(Align::Top)))] + pub alignment: Axes<Option<GenAlign>>, + + /// The content to align. + #[required] + pub body: Content, +} + +impl Show for AlignElem { + #[tracing::instrument(name = "AlignElem::show", skip_all)] + fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + Ok(self + .body() + .styled(Self::set_alignment(self.alignment(styles).map(Some)))) + } +} diff --git a/crates/typst-library/src/layout/columns.rs b/crates/typst-library/src/layout/columns.rs new file mode 100644 index 00000000..d2138491 --- /dev/null +++ b/crates/typst-library/src/layout/columns.rs @@ -0,0 +1,172 @@ +use crate::prelude::*; +use crate::text::TextElem; + +/// Separates a region into multiple equally sized columns. +/// +/// The `column` function allows to separate the interior of any container into +/// multiple columns. It will not equalize the height of the columns, instead, +/// the columns will take up the height of their container or the remaining +/// height on the page. The columns function can break across pages if +/// necessary. +/// +/// ## Example { #example } +/// ```example +/// = Towards Advanced Deep Learning +/// +/// #box(height: 68pt, +/// columns(2, gutter: 11pt)[ +/// #set par(justify: true) +/// This research was funded by the +/// National Academy of Sciences. +/// NAoS provided support for field +/// tests and interviews with a +/// grant of up to USD 40.000 for a +/// period of 6 months. +/// ] +/// ) +/// +/// In recent years, deep learning has +/// increasingly been used to solve a +/// variety of problems. +/// ``` +/// +/// Display: Columns +/// Category: layout +#[element(Layout)] +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 Layout for ColumnsElem { + #[tracing::instrument(name = "ColumnsElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let body = self.body(); + + // Separating the infinite space into infinite columns does not make + // much sense. + if !regions.size.x.is_finite() { + return body.layout(vt, styles, regions); + } + + // Determine the width of the gutter and each column. + let columns = self.count(styles).get(); + let gutter = self.gutter(styles).relative_to(regions.base().x); + let width = (regions.size.x - gutter * (columns - 1) as f64) / columns as f64; + + let backlog: Vec<_> = std::iter::once(®ions.size.y) + .chain(regions.backlog) + .flat_map(|&height| std::iter::repeat(height).take(columns)) + .skip(1) + .collect(); + + // Create the pod regions. + let pod = Regions { + size: Size::new(width, regions.size.y), + full: regions.full, + backlog: &backlog, + last: regions.last, + expand: Axes::new(true, regions.expand.y), + root: regions.root, + }; + + // Layout the children. + let mut frames = body.layout(vt, styles, pod)?.into_iter(); + let mut finished = vec![]; + + let dir = TextElem::dir_in(styles); + let total_regions = (frames.len() as f32 / columns as f32).ceil() as usize; + + // Stitch together the columns for each region. + for region in regions.iter().take(total_regions) { + // The height should be the parent height if we should expand. + // Otherwise its the maximum column height for the frame. In that + // case, the frame is first created with zero height and then + // resized. + let height = if regions.expand.y { region.y } else { Abs::zero() }; + let mut output = Frame::new(Size::new(regions.size.x, height)); + let mut cursor = Abs::zero(); + + for _ in 0..columns { + let Some(frame) = frames.next() else { break }; + if !regions.expand.y { + output.size_mut().y.set_max(frame.height()); + } + + let width = frame.width(); + let x = if dir == Dir::LTR { + cursor + } else { + regions.size.x - cursor - width + }; + + output.push_frame(Point::with_x(x), frame); + cursor += width + gutter; + } + + finished.push(output); + } + + Ok(Fragment::frames(finished)) + } +} + +/// Forces a column break. +/// +/// The function will behave like a [page break]($func/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 } +/// ```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. +/// ``` +/// +/// Display: Column Break +/// Category: layout +#[element(Behave)] +pub struct ColbreakElem { + /// If `{true}`, the column break is skipped if the current column is + /// already empty. + #[default(false)] + pub weak: bool, +} + +impl Behave for ColbreakElem { + fn behaviour(&self) -> Behaviour { + if self.weak(StyleChain::default()) { + Behaviour::Weak(1) + } else { + Behaviour::Destructive + } + } +} diff --git a/crates/typst-library/src/layout/container.rs b/crates/typst-library/src/layout/container.rs new file mode 100644 index 00000000..c79669d0 --- /dev/null +++ b/crates/typst-library/src/layout/container.rs @@ -0,0 +1,497 @@ +use typst::eval::AutoValue; + +use super::VElem; +use crate::layout::Spacing; +use crate::prelude::*; + +/// 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 } +/// ```example +/// Refer to the docs +/// #box( +/// height: 9pt, +/// image("docs.svg") +/// ) +/// for more information. +/// ``` +/// +/// Display: Box +/// Category: layout +#[element(Layout)] +pub struct BoxElem { + /// The width of the box. + /// + /// Boxes can have [fractional]($type/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]($func/rect.fill) for more details. + pub fill: Option<Paint>, + + /// The box's border color. See the + /// [rectangle's documentation]($func/rect.stroke) for more details. + #[resolve] + #[fold] + pub stroke: Sides<Option<Option<PartialStroke>>>, + + /// How much to round the box's corners. See the [rectangle's + /// documentation]($func/rect.radius) for more details. + #[resolve] + #[fold] + pub radius: Corners<Option<Rel<Length>>>, + + /// How much to pad the box's content. See the [rectangle's + /// documentation]($func/rect.inset) for more details. + #[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]($func/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. + #[default(false)] + pub clip: bool, + + /// The contents of the box. + #[positional] + pub body: Option<Content>, +} + +impl Layout for BoxElem { + #[tracing::instrument(name = "BoxElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let width = match self.width(styles) { + Sizing::Auto => Smart::Auto, + Sizing::Rel(rel) => Smart::Custom(rel), + Sizing::Fr(_) => Smart::Custom(Ratio::one().into()), + }; + + // Resolve the sizing to a concrete size. + let sizing = Axes::new(width, self.height(styles)); + let expand = sizing.as_ref().map(Smart::is_custom); + let size = sizing + .resolve(styles) + .zip(regions.base()) + .map(|(s, b)| s.map(|v| v.relative_to(b))) + .unwrap_or(regions.base()); + + // Apply inset. + let mut body = self.body(styles).unwrap_or_default(); + let inset = self.inset(styles); + if inset.iter().any(|v| !v.is_zero()) { + body = body.padded(inset.map(|side| side.map(Length::from))); + } + + // Select the appropriate base and expansion for the child depending + // on whether it is automatically or relatively sized. + let pod = Regions::one(size, expand); + let mut frame = body.layout(vt, styles, pod)?.into_frame(); + + // Enforce correct size. + *frame.size_mut() = expand.select(size, frame.size()); + + // Apply baseline shift. + let shift = self.baseline(styles).relative_to(frame.height()); + if !shift.is_zero() { + frame.set_baseline(frame.baseline() - shift); + } + + // Clip the contents + if self.clip(styles) { + frame.clip(); + } + + // Prepare fill and stroke. + let fill = self.fill(styles); + let stroke = self.stroke(styles).map(|s| s.map(PartialStroke::unwrap_or_default)); + + // Add fill and/or stroke. + if fill.is_some() || stroke.iter().any(Option::is_some) { + let outset = self.outset(styles); + let radius = self.radius(styles); + frame.fill_and_stroke(fill, stroke, outset, radius, self.span()); + } + + // Apply metadata. + frame.meta(styles, false); + + Ok(Fragment::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 { #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. +/// ``` +/// +/// Display: Block +/// Category: layout +#[element(Layout)] +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`]($func/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: Smart<Rel<Length>>, + + /// 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]($func/rect.fill) for more details. + pub fill: Option<Paint>, + + /// The block's border color. See the + /// [rectangle's documentation]($func/rect.stroke) for more details. + #[resolve] + #[fold] + pub stroke: Sides<Option<Option<PartialStroke>>>, + + /// How much to round the block's corners. See the [rectangle's + /// documentation]($func/rect.radius) for more details. + #[resolve] + #[fold] + pub radius: Corners<Option<Rel<Length>>>, + + /// How much to pad the block's content. See the [rectangle's + /// documentation]($func/rect.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 [rectangle's documentation]($func/rect.outset) for more details. + #[resolve] + #[fold] + pub outset: Sides<Option<Rel<Length>>>, + + /// The spacing around this block. This is shorthand to set `above` and + /// `below` to the same value. + /// + /// ```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. Takes precedence + /// over `spacing`. Can be used in combination with a show rule to adjust + /// the spacing around arbitrary block-level elements. + #[external] + #[default(Em::new(1.2).into())] + pub above: Spacing, + #[internal] + #[parse( + let spacing = args.named("spacing")?; + args.named("above")? + .map(VElem::block_around) + .or_else(|| spacing.map(VElem::block_spacing)) + )] + #[default(VElem::block_spacing(Em::new(1.2).into()))] + pub above: VElem, + + /// The spacing between this block and its successor. Takes precedence + /// over `spacing`. + #[external] + #[default(Em::new(1.2).into())] + pub below: Spacing, + #[internal] + #[parse( + args.named("below")? + .map(VElem::block_around) + .or_else(|| spacing.map(VElem::block_spacing)) + )] + #[default(VElem::block_spacing(Em::new(1.2).into()))] + pub below: VElem, + + /// Whether to clip the content inside the block. + #[default(false)] + pub clip: bool, + + /// The contents of the block. + #[positional] + pub body: Option<Content>, + + /// Whether this block must stick to the following one. + /// + /// Use this to prevent page breaks between e.g. a heading and its body. + #[internal] + #[default(false)] + pub sticky: bool, +} + +impl Layout for BlockElem { + #[tracing::instrument(name = "BlockElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + // Apply inset. + let mut body = self.body(styles).unwrap_or_default(); + let inset = self.inset(styles); + if inset.iter().any(|v| !v.is_zero()) { + body = body.clone().padded(inset.map(|side| side.map(Length::from))); + } + + // Resolve the sizing to a concrete size. + let sizing = Axes::new(self.width(styles), self.height(styles)); + let mut expand = sizing.as_ref().map(Smart::is_custom); + let mut size = sizing + .resolve(styles) + .zip(regions.base()) + .map(|(s, b)| s.map(|v| v.relative_to(b))) + .unwrap_or(regions.base()); + + // Layout the child. + let mut frames = if self.breakable(styles) { + // Measure to ensure frames for all regions have the same width. + if sizing.x == Smart::Auto { + let pod = Regions::one(size, Axes::splat(false)); + let frame = body.measure(vt, styles, pod)?.into_frame(); + size.x = frame.width(); + expand.x = true; + } + + let mut pod = regions; + pod.size.x = size.x; + pod.expand = expand; + + if expand.y { + pod.full = size.y; + } + + // Generate backlog for fixed height. + let mut heights = vec![]; + if sizing.y.is_custom() { + let mut remaining = size.y; + for region in regions.iter() { + let limited = region.y.min(remaining); + heights.push(limited); + remaining -= limited; + if Abs::zero().fits(remaining) { + break; + } + } + + if let Some(last) = heights.last_mut() { + *last += remaining; + } + + pod.size.y = heights[0]; + pod.backlog = &heights[1..]; + pod.last = None; + } + + let mut frames = body.layout(vt, styles, pod)?.into_frames(); + for (frame, &height) in frames.iter_mut().zip(&heights) { + *frame.size_mut() = + expand.select(Size::new(size.x, height), frame.size()); + } + frames + } else { + let pod = Regions::one(size, expand); + let mut frames = body.layout(vt, styles, pod)?.into_frames(); + *frames[0].size_mut() = expand.select(size, frames[0].size()); + frames + }; + + // Clip the contents + if self.clip(styles) { + for frame in frames.iter_mut() { + frame.clip(); + } + } + + // Prepare fill and stroke. + let fill = self.fill(styles); + let stroke = self.stroke(styles).map(|s| s.map(PartialStroke::unwrap_or_default)); + + // Add fill and/or stroke. + if fill.is_some() || stroke.iter().any(Option::is_some) { + let mut skip = false; + if let [first, rest @ ..] = frames.as_slice() { + skip = first.is_empty() && rest.iter().any(|frame| !frame.is_empty()); + } + + let outset = self.outset(styles); + let radius = self.radius(styles); + for frame in frames.iter_mut().skip(skip as usize) { + frame.fill_and_stroke( + fill.clone(), + stroke.clone(), + outset, + radius, + self.span(), + ); + } + } + + // Apply metadata. + for frame in &mut frames { + frame.meta(styles, false); + } + + Ok(Fragment::frames(frames)) + } +} + +/// Defines how to size a grid cell along an axis. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Sizing { + /// A track that fits its cell's contents. + Auto, + /// A track size specified in absolute terms and relative to the parent's + /// size. + Rel(Rel<Length>), + /// A track size specified as a fraction of the remaining free space in the + /// parent. + Fr(Fr), +} + +impl Sizing { + /// 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<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), +} diff --git a/crates/typst-library/src/layout/enum.rs b/crates/typst-library/src/layout/enum.rs new file mode 100644 index 00000000..d66477fc --- /dev/null +++ b/crates/typst-library/src/layout/enum.rs @@ -0,0 +1,318 @@ +use std::str::FromStr; + +use crate::layout::{BlockElem, ParElem, Sizing, Spacing}; +use crate::meta::{Numbering, NumberingPattern}; +use crate::prelude::*; +use crate::text::TextElem; + +use super::GridLayouter; + +/// A numbered list. +/// +/// Displays a sequence of items vertically and numbers them consecutively. +/// +/// ## Example { #example } +/// ```example +/// Automatically numbered: +/// + Preparations +/// + Analysis +/// + Conclusions +/// +/// Manually numbered: +/// 2. What is the first step? +/// 5. I am confused. +/// + Moving on ... +/// +/// 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`]($func/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 { #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 plus sign or dot +/// becomes part of that item. +/// +/// Display: Numbered List +/// Category: layout +#[element(Layout)] +#[scope( + scope.define("item", EnumItem::func()); + scope +)] +pub struct EnumElem { + /// If this is `{false}`, the items are spaced apart with + /// [enum spacing]($func/enum.spacing). If it is `{true}`, they use normal + /// [leading]($func/par.leading) instead. This makes the enumeration 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}`. + /// + /// ```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]($func/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()))] + 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 integredients + /// + 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 a wide (non-tight) enumeration. + /// + /// If set to `{auto}`, uses the spacing [below blocks]($func/block.below). + pub spacing: Smart<Spacing>, + + /// The horizontal alignment that enum numbers should have. + /// + /// By default, this is set to `{end}`, which aligns enum numbers + /// towards end of the current text direction (in left-to-right script, + /// for example, this is the same as `{right}`). 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 + /// behavior, however. + /// + /// ````example + /// #set enum(number-align: start) + /// + /// Here are some powers of two: + /// 1. One + /// 2. Two + /// 4. Four + /// 8. Eight + /// 16. Sixteen + /// 32. Thirty two + /// ```` + #[default(HorizontalAlign(GenAlign::End))] + pub number_align: HorizontalAlign, + + /// 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<EnumItem>, + + /// The numbers of parent items. + #[internal] + #[fold] + parents: Parent, +} + +impl Layout for EnumElem { + #[tracing::instrument(name = "EnumElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let numbering = self.numbering(styles); + let indent = self.indent(styles); + let body_indent = self.body_indent(styles); + let gutter = if self.tight(styles) { + ParElem::leading_in(styles).into() + } else { + self.spacing(styles) + .unwrap_or_else(|| BlockElem::below_in(styles).amount()) + }; + + let mut cells = vec![]; + let mut number = self.start(styles); + let mut parents = self.parents(styles); + let full = self.full(styles); + + // Horizontally align based on the given respective parameter. + // Vertically align to the top to avoid inheriting 'horizon' or + // 'bottom' alignment from the context and having the number be + // displaced in relation to the item it refers to. + let number_align: Axes<Option<GenAlign>> = + Axes::new(self.number_align(styles).into(), Align::Top.into()).map(Some); + + for item in self.children() { + number = item.number(styles).unwrap_or(number); + + let resolved = if full { + parents.push(number); + let content = numbering.apply_vt(vt, &parents)?.display(); + parents.pop(); + content + } else { + match &numbering { + Numbering::Pattern(pattern) => { + TextElem::packed(pattern.apply_kth(parents.len(), number)) + } + other => other.apply_vt(vt, &[number])?.display(), + } + }; + + // Disable overhang as a workaround to end-aligned dots glitching + // and decreasing spacing between numbers and items. + let resolved = + resolved.aligned(number_align).styled(TextElem::set_overhang(false)); + + cells.push(Content::empty()); + cells.push(resolved); + cells.push(Content::empty()); + cells.push(item.body().styled(Self::set_parents(Parent(number)))); + number = number.saturating_add(1); + } + + let layouter = GridLayouter::new( + Axes::with_x(&[ + Sizing::Rel(indent.into()), + Sizing::Auto, + Sizing::Rel(body_indent.into()), + Sizing::Auto, + ]), + Axes::with_y(&[gutter.into()]), + &cells, + regions, + styles, + ); + + Ok(layouter.layout(vt)?.fragment) + } +} + +/// An enumeration item. +/// +/// Display: Numbered List Item +/// Category: layout +#[element] +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.to::<Self>().cloned().unwrap_or_else(|| Self::new(v.clone())), +} + +struct Parent(usize); + +cast! { + Parent, + self => self.0.into_value(), + v: usize => Self(v), +} + +impl Fold for Parent { + type Output = Vec<usize>; + + fn fold(self, mut outer: Self::Output) -> Self::Output { + outer.push(self.0); + outer + } +} diff --git a/crates/typst-library/src/layout/flow.rs b/crates/typst-library/src/layout/flow.rs new file mode 100644 index 00000000..accd092a --- /dev/null +++ b/crates/typst-library/src/layout/flow.rs @@ -0,0 +1,583 @@ +use std::mem; + +use super::{ + AlignElem, BlockElem, ColbreakElem, ColumnsElem, ParElem, PlaceElem, Spacing, VElem, +}; +use crate::meta::{FootnoteElem, FootnoteEntry}; +use crate::prelude::*; +use crate::visualize::{ + CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem, + SquareElem, +}; + +/// Arranges spacing, paragraphs and block-level elements into a flow. +/// +/// This element is responsible for layouting both the top-level content flow +/// and the contents of boxes. +/// +/// Display: Flow +/// Category: layout +#[element(Layout)] +pub struct FlowElem { + /// The children that will be arranges into a flow. + #[variadic] + pub children: Vec<Content>, +} + +impl Layout for FlowElem { + #[tracing::instrument(name = "FlowElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let mut layouter = FlowLayouter::new(regions, styles); + + for mut child in &self.children() { + let outer = styles; + let mut styles = styles; + if let Some((elem, map)) = child.to_styled() { + child = elem; + styles = outer.chain(map); + } + + if let Some(elem) = child.to::<VElem>() { + layouter.layout_spacing(vt, elem, styles)?; + } else if let Some(elem) = child.to::<ParElem>() { + layouter.layout_par(vt, elem, styles)?; + } else if child.is::<LineElem>() + || child.is::<RectElem>() + || child.is::<SquareElem>() + || child.is::<EllipseElem>() + || child.is::<CircleElem>() + || child.is::<ImageElem>() + || child.is::<PolygonElem>() + || child.is::<PathElem>() + { + let layoutable = child.with::<dyn Layout>().unwrap(); + layouter.layout_single(vt, layoutable, styles)?; + } else if child.is::<MetaElem>() { + let mut frame = Frame::new(Size::zero()); + frame.meta(styles, true); + layouter.items.push(FlowItem::Frame { + frame, + aligns: Axes::new(Align::Top, Align::Left), + sticky: true, + movable: false, + }); + } else if child.can::<dyn Layout>() { + layouter.layout_multiple(vt, child, styles)?; + } else if child.is::<ColbreakElem>() { + if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some() + { + layouter.finish_region()?; + } + } else { + bail!(child.span(), "unexpected flow child"); + } + } + + layouter.finish() + } +} + +/// Performs flow layout. +struct FlowLayouter<'a> { + /// Whether this is the root flow. + root: bool, + /// The regions to layout children into. + regions: Regions<'a>, + /// The shared styles. + styles: StyleChain<'a>, + /// Whether the flow should expand to fill the region. + expand: Axes<bool>, + /// The initial size of `regions.size` that was available before we started + /// subtracting. + initial: Size, + /// Whether the last block was a paragraph. + last_was_par: bool, + /// Spacing and layouted blocks for the current region. + items: Vec<FlowItem>, + /// Whether we have any footnotes in the current region. + has_footnotes: bool, + /// Footnote configuration. + footnote_config: FootnoteConfig, + /// Finished frames for previous regions. + finished: Vec<Frame>, +} + +/// Cached footnote configuration. +struct FootnoteConfig { + separator: Content, + clearance: Abs, + gap: Abs, +} + +/// A prepared item in a flow layout. +#[derive(Debug)] +enum FlowItem { + /// Spacing between other items and whether it is weak. + Absolute(Abs, bool), + /// Fractional spacing between other items. + Fractional(Fr), + /// A frame for a layouted block, how to align it, whether it sticks to the + /// item after it (for orphan prevention), and whether it is movable + /// (to keep it together with its footnotes). + Frame { frame: Frame, aligns: Axes<Align>, sticky: bool, movable: bool }, + /// An absolutely placed frame. + Placed(Frame), + /// A footnote frame (can also be the separator). + Footnote(Frame), +} + +impl FlowItem { + /// The inherent height of the item. + fn height(&self) -> Abs { + match self { + Self::Absolute(v, _) => *v, + Self::Fractional(_) | Self::Placed(_) => Abs::zero(), + Self::Frame { frame, .. } | Self::Footnote(frame) => frame.height(), + } + } +} + +impl<'a> FlowLayouter<'a> { + /// Create a new flow layouter. + fn new(mut regions: Regions<'a>, styles: StyleChain<'a>) -> Self { + let expand = regions.expand; + + // Disable vertical expansion & root for children. + regions.expand.y = false; + let root = mem::replace(&mut regions.root, false); + + Self { + root, + regions, + styles, + expand, + initial: regions.size, + last_was_par: false, + items: vec![], + has_footnotes: false, + footnote_config: FootnoteConfig { + separator: FootnoteEntry::separator_in(styles), + clearance: FootnoteEntry::clearance_in(styles), + gap: FootnoteEntry::gap_in(styles), + }, + finished: vec![], + } + } + + /// Layout vertical spacing. + #[tracing::instrument(name = "FlowLayouter::layout_spacing", skip_all)] + fn layout_spacing( + &mut self, + vt: &mut Vt, + v: &VElem, + styles: StyleChain, + ) -> SourceResult<()> { + self.layout_item( + vt, + match v.amount() { + Spacing::Rel(rel) => FlowItem::Absolute( + rel.resolve(styles).relative_to(self.initial.y), + v.weakness(styles) > 0, + ), + Spacing::Fr(fr) => FlowItem::Fractional(fr), + }, + ) + } + + /// Layout a paragraph. + #[tracing::instrument(name = "FlowLayouter::layout_par", skip_all)] + fn layout_par( + &mut self, + vt: &mut Vt, + par: &ParElem, + styles: StyleChain, + ) -> SourceResult<()> { + let aligns = AlignElem::alignment_in(styles).resolve(styles); + let leading = ParElem::leading_in(styles); + let consecutive = self.last_was_par; + let lines = par + .layout(vt, styles, consecutive, self.regions.base(), self.regions.expand.x)? + .into_frames(); + + let mut sticky = self.items.len(); + for (i, item) in self.items.iter().enumerate().rev() { + match *item { + FlowItem::Absolute(_, _) => {} + FlowItem::Frame { sticky: true, .. } => sticky = i, + _ => break, + } + } + + if let Some(first) = lines.first() { + if !self.regions.size.y.fits(first.height()) && !self.regions.in_last() { + let carry: Vec<_> = self.items.drain(sticky..).collect(); + self.finish_region()?; + for item in carry { + self.layout_item(vt, item)?; + } + } + } + + for (i, frame) in lines.into_iter().enumerate() { + if i > 0 { + self.layout_item(vt, FlowItem::Absolute(leading, true))?; + } + + self.layout_item( + vt, + FlowItem::Frame { frame, aligns, sticky: false, movable: true }, + )?; + } + + self.last_was_par = true; + Ok(()) + } + + /// Layout into a single region. + #[tracing::instrument(name = "FlowLayouter::layout_single", skip_all)] + fn layout_single( + &mut self, + vt: &mut Vt, + content: &dyn Layout, + styles: StyleChain, + ) -> SourceResult<()> { + let aligns = AlignElem::alignment_in(styles).resolve(styles); + let sticky = BlockElem::sticky_in(styles); + let pod = Regions::one(self.regions.base(), Axes::splat(false)); + let frame = content.layout(vt, styles, pod)?.into_frame(); + self.layout_item(vt, FlowItem::Frame { frame, aligns, sticky, movable: true })?; + self.last_was_par = false; + Ok(()) + } + + /// Layout into multiple regions. + fn layout_multiple( + &mut self, + vt: &mut Vt, + block: &Content, + styles: StyleChain, + ) -> SourceResult<()> { + // Placed elements that are out of flow produce placed items which + // aren't aligned later. + if let Some(placed) = block.to::<PlaceElem>() { + if placed.out_of_flow(styles) { + let frame = block.layout(vt, styles, self.regions)?.into_frame(); + self.layout_item(vt, FlowItem::Placed(frame))?; + return Ok(()); + } + } else if self.regions.is_full() { + // Skip directly if region is already full. + self.finish_region()?; + } + + // How to align the block. + let aligns = if let Some(align) = block.to::<AlignElem>() { + align.alignment(styles) + } else if let Some((_, local)) = block.to_styled() { + AlignElem::alignment_in(styles.chain(local)) + } else { + AlignElem::alignment_in(styles) + } + .resolve(styles); + + // Temporarily delegerate rootness to the columns. + let is_root = self.root; + if is_root && block.is::<ColumnsElem>() { + self.root = false; + self.regions.root = true; + } + + // Layout the block itself. + let sticky = BlockElem::sticky_in(styles); + let fragment = block.layout(vt, styles, self.regions)?; + let mut notes = Vec::new(); + + for (i, frame) in fragment.into_iter().enumerate() { + // Find footnotes in the frame. + if self.root { + find_footnotes(&mut notes, &frame); + } + + if i > 0 { + self.finish_region()?; + } + + self.layout_item( + vt, + FlowItem::Frame { frame, aligns, sticky, movable: false }, + )?; + } + + if self.root && !self.handle_footnotes(vt, &mut notes, false, false)? { + self.finish_region()?; + self.handle_footnotes(vt, &mut notes, false, true)?; + } + + self.root = is_root; + self.regions.root = false; + self.last_was_par = false; + + Ok(()) + } + + /// Layout a finished frame. + #[tracing::instrument(name = "FlowLayouter::layout_item", skip_all)] + fn layout_item(&mut self, vt: &mut Vt, item: FlowItem) -> SourceResult<()> { + match item { + FlowItem::Absolute(v, weak) => { + if weak + && !self + .items + .iter() + .any(|item| matches!(item, FlowItem::Frame { .. })) + { + return Ok(()); + } + self.regions.size.y -= v + } + FlowItem::Fractional(_) => {} + FlowItem::Frame { ref frame, movable, .. } => { + let size = frame.size(); + if !self.regions.size.y.fits(size.y) && !self.regions.in_last() { + self.finish_region()?; + } + + self.regions.size.y -= size.y; + if self.root && movable { + let mut notes = Vec::new(); + find_footnotes(&mut notes, frame); + self.items.push(item); + if !self.handle_footnotes(vt, &mut notes, true, false)? { + let item = self.items.pop(); + self.finish_region()?; + self.items.extend(item); + self.regions.size.y -= size.y; + self.handle_footnotes(vt, &mut notes, true, true)?; + } + return Ok(()); + } + } + FlowItem::Placed(_) => {} + FlowItem::Footnote(_) => {} + } + + self.items.push(item); + Ok(()) + } + + /// Finish the frame for one region. + fn finish_region(&mut self) -> SourceResult<()> { + // Trim weak spacing. + while self + .items + .last() + .map_or(false, |item| matches!(item, FlowItem::Absolute(_, true))) + { + self.items.pop(); + } + + // Determine the used size. + let mut fr = Fr::zero(); + let mut used = Size::zero(); + let mut footnote_height = Abs::zero(); + let mut first_footnote = true; + for item in &self.items { + match item { + FlowItem::Absolute(v, _) => used.y += *v, + FlowItem::Fractional(v) => fr += *v, + FlowItem::Frame { frame, .. } => { + let size = frame.size(); + used.y += size.y; + used.x.set_max(size.x); + } + FlowItem::Placed(_) => {} + FlowItem::Footnote(frame) => { + let size = frame.size(); + footnote_height += size.y; + if !first_footnote { + footnote_height += self.footnote_config.gap; + } + first_footnote = false; + used.x.set_max(size.x); + } + } + } + used.y += footnote_height; + + // Determine the size of the flow in this region depending on whether + // the region expands. Also account for fractional spacing and + // footnotes. + let mut size = self.expand.select(self.initial, used).min(self.initial); + if (fr.get() > 0.0 || self.has_footnotes) && self.initial.y.is_finite() { + size.y = self.initial.y; + } + + let mut output = Frame::new(size); + let mut offset = Abs::zero(); + let mut ruler = Align::Top; + let mut footnote_offset = size.y - footnote_height; + + // Place all frames. + for item in self.items.drain(..) { + match item { + FlowItem::Absolute(v, _) => { + offset += v; + } + FlowItem::Fractional(v) => { + let remaining = self.initial.y - used.y; + offset += v.share(fr, remaining); + } + FlowItem::Frame { frame, aligns, .. } => { + ruler = ruler.max(aligns.y); + let x = aligns.x.position(size.x - frame.width()); + let y = offset + ruler.position(size.y - used.y); + let pos = Point::new(x, y); + offset += frame.height(); + output.push_frame(pos, frame); + } + FlowItem::Footnote(frame) => { + let pos = Point::with_y(footnote_offset); + footnote_offset += frame.height() + self.footnote_config.gap; + output.push_frame(pos, frame); + } + FlowItem::Placed(frame) => { + output.push_frame(Point::zero(), frame); + } + } + } + + // Advance to the next region. + self.finished.push(output); + self.regions.next(); + self.initial = self.regions.size; + self.has_footnotes = false; + Ok(()) + } + + /// Finish layouting and return the resulting fragment. + fn finish(mut self) -> SourceResult<Fragment> { + if self.expand.y { + while !self.regions.backlog.is_empty() { + self.finish_region()?; + } + } + + self.finish_region()?; + Ok(Fragment::frames(self.finished)) + } +} + +impl FlowLayouter<'_> { + /// Processes all footnotes in the frame. + #[tracing::instrument(skip_all)] + fn handle_footnotes( + &mut self, + vt: &mut Vt, + notes: &mut Vec<FootnoteElem>, + movable: bool, + force: bool, + ) -> SourceResult<bool> { + let items_len = self.items.len(); + let notes_len = notes.len(); + + // Process footnotes one at a time. + let mut k = 0; + while k < notes.len() { + if notes[k].is_ref() { + k += 1; + continue; + } + + if !self.has_footnotes { + self.layout_footnote_separator(vt)?; + } + + self.regions.size.y -= self.footnote_config.gap; + let frames = FootnoteEntry::new(notes[k].clone()) + .pack() + .layout(vt, self.styles, self.regions.with_root(false))? + .into_frames(); + + // If the entries didn't fit, abort (to keep footnote and entry + // together). + if !force + && (k == 0 || movable) + && frames.first().map_or(false, Frame::is_empty) + { + // Remove existing footnotes attempts because we need to + // move the item to the next page. + notes.truncate(notes_len); + + // Undo region modifications. + for item in self.items.drain(items_len..) { + self.regions.size.y -= item.height(); + } + + return Ok(false); + } + + let prev = notes.len(); + for (i, frame) in frames.into_iter().enumerate() { + find_footnotes(notes, &frame); + if i > 0 { + self.finish_region()?; + self.layout_footnote_separator(vt)?; + self.regions.size.y -= self.footnote_config.gap; + } + self.regions.size.y -= frame.height(); + self.items.push(FlowItem::Footnote(frame)); + } + + k += 1; + + // Process the nested notes before dealing with further top-level + // notes. + let nested = notes.len() - prev; + if nested > 0 { + notes[k..].rotate_right(nested); + } + } + + Ok(true) + } + + /// Layout and save the footnote separator, typically a line. + #[tracing::instrument(skip_all)] + fn layout_footnote_separator(&mut self, vt: &mut Vt) -> SourceResult<()> { + let expand = Axes::new(self.regions.expand.x, false); + let pod = Regions::one(self.regions.base(), expand); + let separator = &self.footnote_config.separator; + + let mut frame = separator.layout(vt, self.styles, pod)?.into_frame(); + frame.size_mut().y += self.footnote_config.clearance; + frame.translate(Point::with_y(self.footnote_config.clearance)); + + self.has_footnotes = true; + self.regions.size.y -= frame.height(); + self.items.push(FlowItem::Footnote(frame)); + + Ok(()) + } +} + +/// Finds all footnotes in the frame. +#[tracing::instrument(skip_all)] +fn find_footnotes(notes: &mut Vec<FootnoteElem>, frame: &Frame) { + for (_, item) in frame.items() { + match item { + FrameItem::Group(group) => find_footnotes(notes, &group.frame), + FrameItem::Meta(Meta::Elem(content), _) + if !notes.iter().any(|note| note.0.location() == content.location()) => + { + let Some(footnote) = content.to::<FootnoteElem>() else { continue }; + notes.push(footnote.clone()); + } + _ => {} + } + } +} diff --git a/crates/typst-library/src/layout/fragment.rs b/crates/typst-library/src/layout/fragment.rs new file mode 100644 index 00000000..3550df2a --- /dev/null +++ b/crates/typst-library/src/layout/fragment.rs @@ -0,0 +1,87 @@ +use crate::prelude::*; + +/// 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 + } + + /// 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/grid.rs b/crates/typst-library/src/layout/grid.rs new file mode 100644 index 00000000..4f5175e9 --- /dev/null +++ b/crates/typst-library/src/layout/grid.rs @@ -0,0 +1,708 @@ +use crate::prelude::*; +use crate::text::TextElem; + +use super::Sizing; + +/// 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. +/// +/// The sizing of the grid 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 +/// which, 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)}`. +/// +/// ## Example { #example } +/// ```example +/// #set text(10pt, style: "italic") +/// #let cell = rect.with( +/// inset: 8pt, +/// fill: rgb("e4e5ea"), +/// width: 100%, +/// radius: 6pt +/// ) +/// #grid( +/// columns: (60pt, 1fr, 60pt), +/// rows: (60pt, auto), +/// gutter: 3pt, +/// cell(height: 100%)[Easy to learn], +/// cell(height: 100%)[Great output], +/// cell(height: 100%)[Intuitive], +/// cell[Our best Typst yet], +/// cell[ +/// Responsive design in print +/// for everyone +/// ], +/// cell[One more thing...], +/// ) +/// ``` +/// +/// Display: Grid +/// Category: layout +#[element(Layout)] +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. + 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. + pub rows: TrackSizings, + + /// The gaps between rows & columns. + /// + /// If there are more gutters than defined sizes, the last gutter is repeated. + #[external] + pub gutter: TrackSizings, + + /// The gaps between columns. Takes precedence over `gutter`. + #[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`. + #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))] + pub row_gutter: TrackSizings, + + /// The contents of the grid cells. + /// + /// The cells are populated in row-major order. + #[variadic] + pub children: Vec<Content>, +} + +impl Layout for GridElem { + #[tracing::instrument(name = "GridElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + // Prepare grid layout by unifying content and gutter tracks. + let cells = self.children(); + let layouter = GridLayouter::new( + Axes::new(&self.columns(styles).0, &self.rows(styles).0), + Axes::new(&self.column_gutter(styles).0, &self.row_gutter(styles).0), + &cells, + regions, + styles, + ); + + // Measure the columns and layout the grid row-by-row. + Ok(layouter.layout(vt)?.fragment) + } +} + +/// Track sizing definitions. +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct TrackSizings(pub Vec<Sizing>); + +cast! { + TrackSizings, + self => self.0.into_value(), + sizing: Sizing => Self(vec![sizing]), + count: NonZeroUsize => Self(vec![Sizing::Auto; count.get()]), + values: Array => Self(values.into_iter().map(Value::cast).collect::<StrResult<_>>()?), +} + +/// Performs grid layout. +pub struct GridLayouter<'a> { + /// The grid cells. + cells: &'a [Content], + /// Whether this is an RTL grid. + is_rtl: bool, + /// Whether this grid has gutters. + has_gutter: bool, + /// The column tracks including gutter tracks. + cols: Vec<Sizing>, + /// The row tracks including gutter tracks. + rows: Vec<Sizing>, + /// The regions to layout children into. + regions: Regions<'a>, + /// The inherited styles. + styles: StyleChain<'a>, + /// Resolved column sizes. + rcols: Vec<Abs>, + /// The sum of `rcols`. + width: Abs, + /// Resolve row sizes, by region. + rrows: Vec<Vec<RowPiece>>, + /// Rows in the current region. + lrows: Vec<Row>, + /// The initial size of the current region before we started subtracting. + initial: Size, + /// Frames for finished regions. + finished: Vec<Frame>, +} + +/// The resulting sizes of columns and rows in a grid. +#[derive(Debug)] +pub struct GridLayout { + /// The fragment. + pub fragment: Fragment, + /// The column widths. + pub cols: Vec<Abs>, + /// The heights of the resulting rows segments, by region. + pub rows: Vec<Vec<RowPiece>>, +} + +/// Details about a resulting row piece. +#[derive(Debug)] +pub struct RowPiece { + /// The height of the segment. + pub height: Abs, + /// The index of the row. + pub y: usize, +} + +/// Produced by initial row layout, auto and relative rows are already finished, +/// fractional rows not yet. +enum Row { + /// Finished row frame of auto or relative row with y index. + Frame(Frame, usize), + /// Fractional row with y index. + Fr(Fr, usize), +} + +impl<'a> GridLayouter<'a> { + /// Create a new grid layouter. + /// + /// This prepares grid layout by unifying content and gutter tracks. + pub fn new( + tracks: Axes<&[Sizing]>, + gutter: Axes<&[Sizing]>, + cells: &'a [Content], + regions: Regions<'a>, + styles: StyleChain<'a>, + ) -> Self { + let mut cols = vec![]; + let mut rows = vec![]; + + // Number of content columns: Always at least one. + let c = tracks.x.len().max(1); + + // Number of content rows: At least as many as given, but also at least + // as many as needed to place each item. + let r = { + let len = cells.len(); + let given = tracks.y.len(); + let needed = len / c + (len % c).clamp(0, 1); + given.max(needed) + }; + + let has_gutter = gutter.any(|tracks| !tracks.is_empty()); + let auto = Sizing::Auto; + let zero = Sizing::Rel(Rel::zero()); + let get_or = |tracks: &[_], idx, default| { + tracks.get(idx).or(tracks.last()).copied().unwrap_or(default) + }; + + // Collect content and gutter columns. + for x in 0..c { + cols.push(get_or(tracks.x, x, auto)); + if has_gutter { + cols.push(get_or(gutter.x, x, zero)); + } + } + + // Collect content and gutter rows. + for y in 0..r { + rows.push(get_or(tracks.y, y, auto)); + if has_gutter { + rows.push(get_or(gutter.y, y, zero)); + } + } + + // Remove superfluous gutter tracks. + if has_gutter { + cols.pop(); + rows.pop(); + } + + // Reverse for RTL. + let is_rtl = TextElem::dir_in(styles) == Dir::RTL; + if is_rtl { + cols.reverse(); + } + + // We use these regions for auto row measurement. Since at that moment, + // columns are already sized, we can enable horizontal expansion. + let mut regions = regions; + regions.expand = Axes::new(true, false); + + Self { + cells, + is_rtl, + has_gutter, + rows, + regions, + styles, + rcols: vec![Abs::zero(); cols.len()], + cols, + width: Abs::zero(), + rrows: vec![], + lrows: vec![], + initial: regions.size, + finished: vec![], + } + } + + /// Determines the columns sizes and then layouts the grid row-by-row. + pub fn layout(mut self, vt: &mut Vt) -> SourceResult<GridLayout> { + self.measure_columns(vt)?; + + for y in 0..self.rows.len() { + // Skip to next region if current one is full, but only for content + // rows, not for gutter rows. + if self.regions.is_full() && (!self.has_gutter || y % 2 == 0) { + self.finish_region(vt)?; + } + + match self.rows[y] { + Sizing::Auto => self.layout_auto_row(vt, y)?, + Sizing::Rel(v) => self.layout_relative_row(vt, v, y)?, + Sizing::Fr(v) => self.lrows.push(Row::Fr(v, y)), + } + } + + self.finish_region(vt)?; + + Ok(GridLayout { + fragment: Fragment::frames(self.finished), + cols: self.rcols, + rows: self.rrows, + }) + } + + /// Determine all column sizes. + #[tracing::instrument(name = "GridLayouter::measure_columns", skip_all)] + fn measure_columns(&mut self, vt: &mut Vt) -> SourceResult<()> { + // Sum of sizes of resolved relative tracks. + let mut rel = Abs::zero(); + + // Sum of fractions of all fractional tracks. + let mut fr = Fr::zero(); + + // Resolve the size of all relative columns and compute the sum of all + // fractional tracks. + for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { + match col { + Sizing::Auto => {} + Sizing::Rel(v) => { + let resolved = + v.resolve(self.styles).relative_to(self.regions.base().x); + *rcol = resolved; + rel += resolved; + } + Sizing::Fr(v) => fr += v, + } + } + + // Size that is not used by fixed-size columns. + let available = self.regions.size.x - rel; + if available >= Abs::zero() { + // Determine size of auto columns. + let (auto, count) = self.measure_auto_columns(vt, available)?; + + // If there is remaining space, distribute it to fractional columns, + // otherwise shrink auto columns. + let remaining = available - auto; + if remaining >= Abs::zero() { + self.grow_fractional_columns(remaining, fr); + } else { + self.shrink_auto_columns(available, count); + } + } + + // Sum up the resolved column sizes once here. + self.width = self.rcols.iter().sum(); + + Ok(()) + } + + /// Measure the size that is available to auto columns. + fn measure_auto_columns( + &mut self, + vt: &mut Vt, + available: Abs, + ) -> SourceResult<(Abs, usize)> { + let mut auto = Abs::zero(); + let mut count = 0; + + // Determine size of auto columns by laying out all cells in those + // columns, measuring them and finding the largest one. + for (x, &col) in self.cols.iter().enumerate() { + if col != Sizing::Auto { + continue; + } + + let mut resolved = Abs::zero(); + for y in 0..self.rows.len() { + if let Some(cell) = self.cell(x, y) { + // For relative rows, we can already resolve the correct + // base and for auto and fr we could only guess anyway. + let height = match self.rows[y] { + Sizing::Rel(v) => { + v.resolve(self.styles).relative_to(self.regions.base().y) + } + _ => self.regions.base().y, + }; + + let size = Size::new(available, height); + let pod = Regions::one(size, Axes::splat(false)); + let frame = cell.measure(vt, self.styles, pod)?.into_frame(); + resolved.set_max(frame.width()); + } + } + + self.rcols[x] = resolved; + auto += resolved; + count += 1; + } + + Ok((auto, count)) + } + + /// Distribute remaining space to fractional columns. + fn grow_fractional_columns(&mut self, remaining: Abs, fr: Fr) { + if fr.is_zero() { + return; + } + + for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { + if let Sizing::Fr(v) = col { + *rcol = v.share(fr, remaining); + } + } + } + + /// Redistribute space to auto columns so that each gets a fair share. + fn shrink_auto_columns(&mut self, available: Abs, count: usize) { + let mut last; + let mut fair = -Abs::inf(); + let mut redistribute = available; + let mut overlarge = count; + let mut changed = true; + + // Iteratively remove columns that don't need to be shrunk. + while changed && overlarge > 0 { + changed = false; + last = fair; + fair = redistribute / (overlarge as f64); + + for (&col, &rcol) in self.cols.iter().zip(&self.rcols) { + // Remove an auto column if it is not overlarge (rcol <= fair), + // but also hasn't already been removed (rcol > last). + if col == Sizing::Auto && rcol <= fair && rcol > last { + redistribute -= rcol; + overlarge -= 1; + changed = true; + } + } + } + + // Redistribute space fairly among overlarge columns. + for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { + if col == Sizing::Auto && *rcol > fair { + *rcol = fair; + } + } + } + + /// Layout a row with automatic height. Such a row may break across multiple + /// regions. + fn layout_auto_row(&mut self, vt: &mut Vt, y: usize) -> SourceResult<()> { + // Determine the size for each region of the row. If the first region + // ends up empty for some column, skip the region and remeasure. + let mut resolved = match self.measure_auto_row(vt, y, true)? { + Some(resolved) => resolved, + None => { + self.finish_region(vt)?; + self.measure_auto_row(vt, y, false)?.unwrap() + } + }; + + // Nothing to layout. + if resolved.is_empty() { + return Ok(()); + } + + // Layout into a single region. + if let &[first] = resolved.as_slice() { + let frame = self.layout_single_row(vt, first, y)?; + self.push_row(frame, y); + return Ok(()); + } + + // Expand all but the last region. + // Skip the first region if the space is eaten up by an fr row. + let len = resolved.len(); + for (region, target) in self + .regions + .iter() + .zip(&mut resolved[..len - 1]) + .skip(self.lrows.iter().any(|row| matches!(row, Row::Fr(..))) as usize) + { + target.set_max(region.y); + } + + // Layout into multiple regions. + let fragment = self.layout_multi_row(vt, &resolved, y)?; + let len = fragment.len(); + for (i, frame) in fragment.into_iter().enumerate() { + self.push_row(frame, y); + if i + 1 < len { + self.finish_region(vt)?; + } + } + + Ok(()) + } + + /// Measure the regions sizes of an auto row. The option is always `Some(_)` + /// if `can_skip` is false. + fn measure_auto_row( + &mut self, + vt: &mut Vt, + y: usize, + can_skip: bool, + ) -> SourceResult<Option<Vec<Abs>>> { + let mut resolved: Vec<Abs> = vec![]; + + for (x, &rcol) in self.rcols.iter().enumerate() { + if let Some(cell) = self.cell(x, y) { + let mut pod = self.regions; + pod.size.x = rcol; + + let frames = cell.measure(vt, self.styles, pod)?.into_frames(); + + // Skip the first region if one cell in it is empty. Then, + // remeasure. + if let [first, rest @ ..] = frames.as_slice() { + if can_skip + && first.is_empty() + && rest.iter().any(|frame| !frame.is_empty()) + { + return Ok(None); + } + } + + let mut sizes = frames.iter().map(|frame| frame.height()); + for (target, size) in resolved.iter_mut().zip(&mut sizes) { + target.set_max(size); + } + + // New heights are maximal by virtue of being new. Note that + // this extend only uses the rest of the sizes iterator. + resolved.extend(sizes); + } + } + + Ok(Some(resolved)) + } + + /// Layout a row with relative height. Such a row cannot break across + /// multiple regions, but it may force a region break. + fn layout_relative_row( + &mut self, + vt: &mut Vt, + v: Rel<Length>, + y: usize, + ) -> SourceResult<()> { + let resolved = v.resolve(self.styles).relative_to(self.regions.base().y); + let frame = self.layout_single_row(vt, resolved, y)?; + + // Skip to fitting region. + let height = frame.height(); + while !self.regions.size.y.fits(height) && !self.regions.in_last() { + self.finish_region(vt)?; + + // Don't skip multiple regions for gutter and don't push a row. + if self.has_gutter && y % 2 == 1 { + return Ok(()); + } + } + + self.push_row(frame, y); + + Ok(()) + } + + /// Layout a row with fixed height and return its frame. + fn layout_single_row( + &mut self, + vt: &mut Vt, + height: Abs, + y: usize, + ) -> SourceResult<Frame> { + let mut output = Frame::new(Size::new(self.width, height)); + let mut pos = Point::zero(); + + for (x, &rcol) in self.rcols.iter().enumerate() { + if let Some(cell) = self.cell(x, y) { + let size = Size::new(rcol, height); + let mut pod = Regions::one(size, Axes::splat(true)); + if self.rows[y] == Sizing::Auto { + pod.full = self.regions.full; + } + let frame = cell.layout(vt, self.styles, pod)?.into_frame(); + output.push_frame(pos, frame); + } + + pos.x += rcol; + } + + Ok(output) + } + + /// Layout a row spanning multiple regions. + fn layout_multi_row( + &mut self, + vt: &mut Vt, + heights: &[Abs], + y: usize, + ) -> SourceResult<Fragment> { + // Prepare frames. + let mut outputs: Vec<_> = heights + .iter() + .map(|&h| Frame::new(Size::new(self.width, h))) + .collect(); + + // Prepare regions. + let size = Size::new(self.width, heights[0]); + let mut pod = Regions::one(size, Axes::splat(true)); + pod.full = self.regions.full; + pod.backlog = &heights[1..]; + + // Layout the row. + let mut pos = Point::zero(); + for (x, &rcol) in self.rcols.iter().enumerate() { + if let Some(cell) = self.cell(x, y) { + pod.size.x = rcol; + + // Push the layouted frames into the individual output frames. + let fragment = cell.layout(vt, self.styles, pod)?; + for (output, frame) in outputs.iter_mut().zip(fragment) { + output.push_frame(pos, frame); + } + } + + pos.x += rcol; + } + + Ok(Fragment::frames(outputs)) + } + + /// Push a row frame into the current region. + fn push_row(&mut self, frame: Frame, y: usize) { + self.regions.size.y -= frame.height(); + self.lrows.push(Row::Frame(frame, y)); + } + + /// Finish rows for one region. + fn finish_region(&mut self, vt: &mut Vt) -> SourceResult<()> { + // Determine the height of existing rows in the region. + let mut used = Abs::zero(); + let mut fr = Fr::zero(); + for row in &self.lrows { + match row { + Row::Frame(frame, _) => used += frame.height(), + Row::Fr(v, _) => fr += *v, + } + } + + // Determine the size of the grid in this region, expanding fully if + // there are fr rows. + let mut size = Size::new(self.width, used).min(self.initial); + if fr.get() > 0.0 && self.initial.y.is_finite() { + size.y = self.initial.y; + } + + // The frame for the region. + let mut output = Frame::new(size); + let mut pos = Point::zero(); + let mut rrows = vec![]; + + // Place finished rows and layout fractional rows. + for row in std::mem::take(&mut self.lrows) { + let (frame, y) = match row { + Row::Frame(frame, y) => (frame, y), + Row::Fr(v, y) => { + let remaining = self.regions.full - used; + let height = v.share(fr, remaining); + (self.layout_single_row(vt, height, y)?, y) + } + }; + + let height = frame.height(); + output.push_frame(pos, frame); + rrows.push(RowPiece { height, y }); + pos.y += height; + } + + self.finished.push(output); + self.rrows.push(rrows); + self.regions.next(); + self.initial = self.regions.size; + + Ok(()) + } + + /// Get the content of the cell in column `x` and row `y`. + /// + /// Returns `None` if it's a gutter cell. + #[track_caller] + fn cell(&self, mut x: usize, y: usize) -> Option<&'a Content> { + assert!(x < self.cols.len()); + assert!(y < self.rows.len()); + + // Columns are reorder, but the cell slice is not. + if self.is_rtl { + x = self.cols.len() - 1 - x; + } + + if self.has_gutter { + // Even columns and rows are children, odd ones are gutter. + if x % 2 == 0 && y % 2 == 0 { + let c = 1 + self.cols.len() / 2; + self.cells.get((y / 2) * c + x / 2) + } else { + None + } + } else { + let c = self.cols.len(); + self.cells.get(y * c + x) + } + } +} diff --git a/crates/typst-library/src/layout/hide.rs b/crates/typst-library/src/layout/hide.rs new file mode 100644 index 00000000..c6e83e0c --- /dev/null +++ b/crates/typst-library/src/layout/hide.rs @@ -0,0 +1,30 @@ +use crate::prelude::*; + +/// 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 } +/// ```example +/// Hello Jane \ +/// #hide[Hello] Joe +/// ``` +/// +/// Display: Hide +/// Category: layout +#[element(Show)] +pub struct HideElem { + /// The content to hide. + #[required] + pub body: Content, +} + +impl Show for HideElem { + #[tracing::instrument(name = "HideElem::show", skip(self))] + fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> { + Ok(self.body().styled(MetaElem::set_data(vec![Meta::Hide]))) + } +} diff --git a/crates/typst-library/src/layout/list.rs b/crates/typst-library/src/layout/list.rs new file mode 100644 index 00000000..e39ec3f5 --- /dev/null +++ b/crates/typst-library/src/layout/list.rs @@ -0,0 +1,239 @@ +use crate::layout::{BlockElem, ParElem, Sizing, Spacing}; +use crate::prelude::*; +use crate::text::TextElem; + +use super::GridLayouter; + +/// A bullet list. +/// +/// Displays a sequence of items vertically, with each item introduced by a +/// marker. +/// +/// ## Example { #example } +/// ```example +/// - *Content* +/// - Text +/// - Math +/// - Layout +/// - Visualize +/// - Meta +/// - Symbols +/// +/// - *Compute* +/// #list( +/// [Foundations], +/// [Calculate], +/// [Construct], +/// [Data Loading], +/// ) +/// ``` +/// +/// ## Syntax { #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 hyphen becomes part of that item. +/// +/// Display: Bullet List +/// Category: layout +#[element(Layout)] +#[scope( + scope.define("item", ListItem::func()); + scope +)] +pub struct ListElem { + /// If this is `{false}`, the items are spaced apart with [list + /// spacing]($func/list.spacing). If it is `{true}`, they use normal + /// [leading]($func/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}`. + /// + /// ```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 last one is repeated. 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 + /// ``` + #[default(ListMarker::Content(vec![TextElem::packed('•')]))] + 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 a wide (non-tight) list. + /// + /// If set to `{auto}`, uses the spacing [below blocks]($func/block.below). + pub spacing: Smart<Spacing>, + + /// 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<ListItem>, + + /// The nesting depth. + #[internal] + #[fold] + depth: Depth, +} + +impl Layout for ListElem { + #[tracing::instrument(name = "ListElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let indent = self.indent(styles); + let body_indent = self.body_indent(styles); + let gutter = if self.tight(styles) { + ParElem::leading_in(styles).into() + } else { + self.spacing(styles) + .unwrap_or_else(|| BlockElem::below_in(styles).amount()) + }; + + let depth = self.depth(styles); + let marker = self + .marker(styles) + .resolve(vt, depth)? + // avoid '#set align' interference with the list + .aligned(Align::LEFT_TOP.into()); + + let mut cells = vec![]; + for item in self.children() { + cells.push(Content::empty()); + cells.push(marker.clone()); + cells.push(Content::empty()); + cells.push(item.body().styled(Self::set_depth(Depth))); + } + + let layouter = GridLayouter::new( + Axes::with_x(&[ + Sizing::Rel(indent.into()), + Sizing::Auto, + Sizing::Rel(body_indent.into()), + Sizing::Auto, + ]), + Axes::with_y(&[gutter.into()]), + &cells, + regions, + styles, + ); + + Ok(layouter.layout(vt)?.fragment) + } +} + +/// A bullet list item. +/// +/// Display: Bullet List Item +/// Category: layout +#[element] +pub struct ListItem { + /// The item's body. + #[required] + pub body: Content, +} + +cast! { + ListItem, + v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::new(v.clone())), +} + +/// A list's marker. +#[derive(Debug, Clone, Hash)] +pub enum ListMarker { + Content(Vec<Content>), + Func(Func), +} + +impl ListMarker { + /// Resolve the marker for the given depth. + fn resolve(&self, vt: &mut Vt, depth: usize) -> SourceResult<Content> { + Ok(match self { + Self::Content(list) => { + list.get(depth).or(list.last()).cloned().unwrap_or_default() + } + Self::Func(func) => func.call_vt(vt, [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), +} + +struct Depth; + +cast! { + Depth, + self => Value::None, + _: Value => Self, +} + +impl Fold for Depth { + type Output = usize; + + fn fold(self, outer: Self::Output) -> Self::Output { + outer + 1 + } +} diff --git a/crates/typst-library/src/layout/measure.rs b/crates/typst-library/src/layout/measure.rs new file mode 100644 index 00000000..eb8e509e --- /dev/null +++ b/crates/typst-library/src/layout/measure.rs @@ -0,0 +1,56 @@ +use crate::prelude::*; + +/// Measures the layouted size of content. +/// +/// The `measure` function lets you determine the layouted size of content. +/// Note that an infinite space is assumed, therefore the measured height/width +/// may not necessarily match the final height/width of the measured content. +/// If you want to measure in the current layout dimensions, you can combined +/// `measure` and [`layout`]($func/layout). +/// +/// # Example { #example } +/// The same content can have a different size depending on the styles that +/// are active when it is layouted. For example, in the example below +/// `[#content]` is of course bigger when we increase the font size. +/// +/// ```example +/// #let content = [Hello!] +/// #content +/// #set text(14pt) +/// #content +/// ``` +/// +/// To do a meaningful measurement, you therefore first need to retrieve the +/// active styles with the [`style`]($func/style) function. You can then pass +/// them to the `measure` function. +/// +/// ```example +/// #let thing(body) = style(styles => { +/// let size = measure(body, styles) +/// [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`]($type/length). +/// +/// Display: Measure +/// Category: layout +#[func] +pub fn measure( + /// The content whose size to measure. + content: Content, + /// The styles with which to layout the content. + styles: Styles, + /// The virtual machine. + vm: &mut Vm, +) -> SourceResult<Dict> { + let pod = Regions::one(Axes::splat(Abs::inf()), Axes::splat(false)); + let styles = StyleChain::new(&styles); + let frame = content.measure(&mut vm.vt, styles, pod)?.into_frame(); + 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..41490eb8 --- /dev/null +++ b/crates/typst-library/src/layout/mod.rs @@ -0,0 +1,709 @@ +//! Composable layouts. + +mod align; +mod columns; +mod container; +#[path = "enum.rs"] +mod enum_; +mod flow; +mod fragment; +mod grid; +mod hide; +mod list; +mod measure; +mod pad; +mod page; +mod par; +mod place; +mod regions; +mod repeat; +mod spacing; +mod stack; +mod table; +mod terms; +mod transform; + +pub use self::align::*; +pub use self::columns::*; +pub use self::container::*; +pub use self::enum_::*; +pub use self::flow::*; +pub use self::fragment::*; +pub use self::grid::*; +pub use self::hide::*; +pub use self::list::*; +pub use self::measure::*; +pub use self::pad::*; +pub use self::page::*; +pub use self::par::*; +pub use self::place::*; +pub use self::regions::*; +pub use self::repeat::*; +pub use self::spacing::*; +pub use self::stack::*; +pub use self::table::*; +pub use self::terms::*; +pub use self::transform::*; + +use std::mem; + +use typed_arena::Arena; +use typst::diag::SourceResult; +use typst::eval::Tracer; +use typst::model::DelayedErrors; +use typst::model::{applicable, realize, StyleVecBuilder}; + +use crate::math::{EquationElem, LayoutMath}; +use crate::meta::DocumentElem; +use crate::prelude::*; +use crate::shared::BehavedBuilder; +use crate::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; +use crate::visualize::{ + CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem, + SquareElem, +}; + +/// Hook up all layout definitions. +pub(super) fn define(global: &mut Scope) { + global.define("page", PageElem::func()); + global.define("pagebreak", PagebreakElem::func()); + global.define("v", VElem::func()); + global.define("par", ParElem::func()); + global.define("parbreak", ParbreakElem::func()); + global.define("h", HElem::func()); + global.define("box", BoxElem::func()); + global.define("block", BlockElem::func()); + global.define("list", ListElem::func()); + global.define("enum", EnumElem::func()); + global.define("terms", TermsElem::func()); + global.define("table", TableElem::func()); + global.define("stack", StackElem::func()); + global.define("grid", GridElem::func()); + global.define("columns", ColumnsElem::func()); + global.define("colbreak", ColbreakElem::func()); + global.define("place", PlaceElem::func()); + global.define("align", AlignElem::func()); + global.define("pad", PadElem::func()); + global.define("repeat", RepeatElem::func()); + global.define("move", MoveElem::func()); + global.define("scale", ScaleElem::func()); + global.define("rotate", RotateElem::func()); + global.define("hide", HideElem::func()); + global.define("measure", measure_func()); + global.define("ltr", Dir::LTR); + global.define("rtl", Dir::RTL); + global.define("ttb", Dir::TTB); + global.define("btt", Dir::BTT); + global.define("start", GenAlign::Start); + global.define("end", GenAlign::End); + global.define("left", GenAlign::Specific(Align::Left)); + global.define("center", GenAlign::Specific(Align::Center)); + global.define("right", GenAlign::Specific(Align::Right)); + global.define("top", GenAlign::Specific(Align::Top)); + global.define("horizon", GenAlign::Specific(Align::Horizon)); + global.define("bottom", GenAlign::Specific(Align::Bottom)); +} + +/// Root-level layout. +pub trait LayoutRoot { + /// Layout into one frame per page. + fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document>; +} + +impl LayoutRoot for Content { + #[tracing::instrument(name = "Content::layout_root", skip_all)] + fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document> { + #[comemo::memoize] + fn cached( + content: &Content, + world: Tracked<dyn World + '_>, + introspector: Tracked<Introspector>, + locator: Tracked<Locator>, + delayed: TrackedMut<DelayedErrors>, + tracer: TrackedMut<Tracer>, + styles: StyleChain, + ) -> SourceResult<Document> { + let mut locator = Locator::chained(locator); + let mut vt = Vt { + world, + introspector, + locator: &mut locator, + delayed, + tracer, + }; + let scratch = Scratch::default(); + let (realized, styles) = realize_root(&mut vt, &scratch, content, styles)?; + realized + .with::<dyn LayoutRoot>() + .unwrap() + .layout_root(&mut vt, styles) + } + + tracing::info!("Starting layout"); + cached( + self, + vt.world, + vt.introspector, + vt.locator.track(), + TrackedMut::reborrow_mut(&mut vt.delayed), + TrackedMut::reborrow_mut(&mut vt.tracer), + styles, + ) + } +} + +/// Layout into regions. +pub trait Layout { + /// Layout into one frame per region. + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment>; + + /// Layout without side effects. + /// + /// This element must be layouted again in the same order for the results to + /// be valid. + #[tracing::instrument(name = "Layout::measure", skip_all)] + fn measure( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let mut locator = Locator::chained(vt.locator.track()); + let mut vt = Vt { + world: vt.world, + introspector: vt.introspector, + locator: &mut locator, + tracer: TrackedMut::reborrow_mut(&mut vt.tracer), + delayed: TrackedMut::reborrow_mut(&mut vt.delayed), + }; + self.layout(&mut vt, styles, regions) + } +} + +impl Layout for Content { + #[tracing::instrument(name = "Content::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + #[allow(clippy::too_many_arguments)] + #[comemo::memoize] + fn cached( + content: &Content, + world: Tracked<dyn World + '_>, + introspector: Tracked<Introspector>, + locator: Tracked<Locator>, + delayed: TrackedMut<DelayedErrors>, + tracer: TrackedMut<Tracer>, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let mut locator = Locator::chained(locator); + let mut vt = Vt { + world, + introspector, + locator: &mut locator, + delayed, + tracer, + }; + let scratch = Scratch::default(); + let (realized, styles) = realize_block(&mut vt, &scratch, content, styles)?; + realized + .with::<dyn Layout>() + .unwrap() + .layout(&mut vt, styles, regions) + } + + tracing::info!("Layouting `Content`"); + + let fragment = cached( + self, + vt.world, + vt.introspector, + vt.locator.track(), + TrackedMut::reborrow_mut(&mut vt.delayed), + TrackedMut::reborrow_mut(&mut vt.tracer), + styles, + regions, + )?; + + vt.locator.visit_frames(&fragment); + Ok(fragment) + } +} + +/// Realize into an element that is capable of root-level layout. +#[tracing::instrument(skip_all)] +fn realize_root<'a>( + vt: &mut Vt, + scratch: &'a Scratch<'a>, + content: &'a Content, + styles: StyleChain<'a>, +) -> SourceResult<(Content, StyleChain<'a>)> { + if content.can::<dyn LayoutRoot>() && !applicable(content, styles) { + return Ok((content.clone(), styles)); + } + + let mut builder = Builder::new(vt, scratch, true); + builder.accept(content, styles)?; + builder.interrupt_page(Some(styles))?; + let (pages, shared) = builder.doc.unwrap().pages.finish(); + Ok((DocumentElem::new(pages.to_vec()).pack(), shared)) +} + +/// Realize into an element that is capable of block-level layout. +#[tracing::instrument(skip_all)] +fn realize_block<'a>( + vt: &mut Vt, + scratch: &'a Scratch<'a>, + content: &'a Content, + styles: StyleChain<'a>, +) -> SourceResult<(Content, StyleChain<'a>)> { + if content.can::<dyn Layout>() + && !content.is::<LineElem>() + && !content.is::<RectElem>() + && !content.is::<SquareElem>() + && !content.is::<EllipseElem>() + && !content.is::<CircleElem>() + && !content.is::<ImageElem>() + && !content.is::<PolygonElem>() + && !content.is::<PathElem>() + && !applicable(content, styles) + { + return Ok((content.clone(), styles)); + } + + let mut builder = Builder::new(vt, scratch, false); + builder.accept(content, styles)?; + builder.interrupt_par()?; + let (children, shared) = builder.flow.0.finish(); + Ok((FlowElem::new(children.to_vec()).pack(), shared)) +} + +/// Builds a document or a flow element from content. +struct Builder<'a, 'v, 't> { + /// The virtual typesetter. + vt: &'v mut Vt<'t>, + /// Scratch arenas for building. + scratch: &'a Scratch<'a>, + /// The current document building state. + doc: Option<DocBuilder<'a>>, + /// The current flow building state. + flow: FlowBuilder<'a>, + /// The current paragraph building state. + par: ParBuilder<'a>, + /// The current list building state. + list: ListBuilder<'a>, +} + +/// Temporary storage arenas for building. +#[derive(Default)] +struct Scratch<'a> { + /// An arena where intermediate style chains are stored. + styles: Arena<StyleChain<'a>>, + /// An arena where intermediate content resulting from show rules is stored. + content: Arena<Content>, +} + +impl<'a, 'v, 't> Builder<'a, 'v, 't> { + fn new(vt: &'v mut Vt<'t>, scratch: &'a Scratch<'a>, top: bool) -> Self { + Self { + vt, + scratch, + doc: top.then(DocBuilder::default), + flow: FlowBuilder::default(), + par: ParBuilder::default(), + list: ListBuilder::default(), + } + } + + fn accept( + &mut self, + mut content: &'a Content, + styles: StyleChain<'a>, + ) -> SourceResult<()> { + if content.can::<dyn LayoutMath>() && !content.is::<EquationElem>() { + content = + self.scratch.content.alloc(EquationElem::new(content.clone()).pack()); + } + + if let Some(realized) = realize(self.vt, content, styles)? { + let stored = self.scratch.content.alloc(realized); + return self.accept(stored, styles); + } + + if let Some((elem, local)) = content.to_styled() { + return self.styled(elem, local, styles); + } + + if let Some(children) = content.to_sequence() { + for elem in children { + self.accept(elem, styles)?; + } + return Ok(()); + } + + if self.list.accept(content, styles) { + return Ok(()); + } + + self.interrupt_list()?; + + if self.list.accept(content, styles) { + return Ok(()); + } + + if self.par.accept(content, styles) { + return Ok(()); + } + + self.interrupt_par()?; + + if self.flow.accept(content, styles) { + return Ok(()); + } + + let keep = content + .to::<PagebreakElem>() + .map_or(false, |pagebreak| !pagebreak.weak(styles)); + + self.interrupt_page(keep.then_some(styles))?; + + if let Some(doc) = &mut self.doc { + if doc.accept(content, styles) { + return Ok(()); + } + } + + if content.is::<PagebreakElem>() { + bail!(content.span(), "pagebreaks are not allowed inside of containers"); + } else { + bail!(content.span(), "{} is not allowed here", content.func().name()); + } + } + + fn styled( + &mut self, + elem: &'a Content, + map: &'a Styles, + styles: StyleChain<'a>, + ) -> SourceResult<()> { + let stored = self.scratch.styles.alloc(styles); + let styles = stored.chain(map); + self.interrupt_style(map, None)?; + self.accept(elem, styles)?; + self.interrupt_style(map, Some(styles))?; + Ok(()) + } + + fn interrupt_style( + &mut self, + local: &Styles, + outer: Option<StyleChain<'a>>, + ) -> SourceResult<()> { + if let Some(Some(span)) = local.interruption::<DocumentElem>() { + if self.doc.is_none() { + bail!(span, "document set rules are not allowed inside of containers"); + } + if outer.is_none() + && (!self.flow.0.is_empty() + || !self.par.0.is_empty() + || !self.list.items.is_empty()) + { + bail!(span, "document set rules must appear before any content"); + } + } else if let Some(Some(span)) = local.interruption::<PageElem>() { + if self.doc.is_none() { + bail!(span, "page configuration is not allowed inside of containers"); + } + self.interrupt_page(outer)?; + } else if local.interruption::<ParElem>().is_some() + || local.interruption::<AlignElem>().is_some() + { + self.interrupt_par()?; + } else if local.interruption::<ListElem>().is_some() + || local.interruption::<EnumElem>().is_some() + || local.interruption::<TermsElem>().is_some() + { + self.interrupt_list()?; + } + Ok(()) + } + + fn interrupt_list(&mut self) -> SourceResult<()> { + if !self.list.items.is_empty() { + let staged = mem::take(&mut self.list.staged); + let (list, styles) = mem::take(&mut self.list).finish(); + let stored = self.scratch.content.alloc(list); + self.accept(stored, styles)?; + for (content, styles) in staged { + self.accept(content, styles)?; + } + } + Ok(()) + } + + fn interrupt_par(&mut self) -> SourceResult<()> { + self.interrupt_list()?; + if !self.par.0.is_empty() { + let (par, styles) = mem::take(&mut self.par).finish(); + let stored = self.scratch.content.alloc(par); + self.accept(stored, styles)?; + } + + Ok(()) + } + + fn interrupt_page(&mut self, styles: Option<StyleChain<'a>>) -> SourceResult<()> { + self.interrupt_par()?; + let Some(doc) = &mut self.doc else { return Ok(()) }; + if !self.flow.0.is_empty() || (doc.keep_next && styles.is_some()) { + let (flow, shared) = mem::take(&mut self.flow).0.finish(); + let styles = if shared == StyleChain::default() { + styles.unwrap_or_default() + } else { + shared + }; + let page = PageElem::new(FlowElem::new(flow.to_vec()).pack()); + let stored = self.scratch.content.alloc(page.pack()); + self.accept(stored, styles)?; + } + Ok(()) + } +} + +/// Accepts pagebreaks and pages. +struct DocBuilder<'a> { + /// The page runs built so far. + pages: StyleVecBuilder<'a, Content>, + /// Whether to keep a following page even if it is empty. + keep_next: bool, + /// Whether the next page should be cleared to an even or odd number. + clear_next: Option<Parity>, +} + +impl<'a> DocBuilder<'a> { + fn accept(&mut self, content: &Content, styles: StyleChain<'a>) -> bool { + if let Some(pagebreak) = content.to::<PagebreakElem>() { + self.keep_next = !pagebreak.weak(styles); + self.clear_next = pagebreak.to(styles); + return true; + } + + if let Some(page) = content.to::<PageElem>() { + let elem = if let Some(clear_to) = self.clear_next.take() { + let mut page = page.clone(); + page.push_clear_to(Some(clear_to)); + page.pack() + } else { + content.clone() + }; + + self.pages.push(elem, styles); + self.keep_next = false; + return true; + } + + false + } +} + +impl Default for DocBuilder<'_> { + fn default() -> Self { + Self { + pages: StyleVecBuilder::new(), + keep_next: true, + clear_next: None, + } + } +} + +/// Accepts flow content. +#[derive(Default)] +struct FlowBuilder<'a>(BehavedBuilder<'a>, bool); + +impl<'a> FlowBuilder<'a> { + fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { + if content.is::<ParbreakElem>() { + self.1 = true; + return true; + } + + let last_was_parbreak = self.1; + self.1 = false; + + if content.is::<VElem>() + || content.is::<ColbreakElem>() + || content.is::<MetaElem>() + { + self.0.push(content.clone(), styles); + return true; + } + + if content.can::<dyn Layout>() || content.is::<ParElem>() { + let is_tight_list = if let Some(elem) = content.to::<ListElem>() { + elem.tight(styles) + } else if let Some(elem) = content.to::<EnumElem>() { + elem.tight(styles) + } else if let Some(elem) = content.to::<TermsElem>() { + elem.tight(styles) + } else { + false + }; + + if !last_was_parbreak && is_tight_list { + let leading = ParElem::leading_in(styles); + let spacing = VElem::list_attach(leading.into()); + self.0.push(spacing.pack(), styles); + } + + let (above, below) = if let Some(block) = content.to::<BlockElem>() { + (block.above(styles), block.below(styles)) + } else { + (BlockElem::above_in(styles), BlockElem::below_in(styles)) + }; + + self.0.push(above.pack(), styles); + self.0.push(content.clone(), styles); + self.0.push(below.pack(), styles); + return true; + } + + false + } +} + +/// Accepts paragraph content. +#[derive(Default)] +struct ParBuilder<'a>(BehavedBuilder<'a>); + +impl<'a> ParBuilder<'a> { + fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { + if content.is::<MetaElem>() { + if !self.0.is_basically_empty() { + self.0.push(content.clone(), styles); + return true; + } + } else if content.is::<SpaceElem>() + || content.is::<TextElem>() + || content.is::<HElem>() + || content.is::<LinebreakElem>() + || content.is::<SmartQuoteElem>() + || content.to::<EquationElem>().map_or(false, |elem| !elem.block(styles)) + || content.is::<BoxElem>() + { + self.0.push(content.clone(), styles); + return true; + } + + false + } + + fn finish(self) -> (Content, StyleChain<'a>) { + let (children, shared) = self.0.finish(); + (ParElem::new(children.to_vec()).pack(), shared) + } +} + +/// Accepts list / enum items, spaces, paragraph breaks. +struct ListBuilder<'a> { + /// The list items collected so far. + items: StyleVecBuilder<'a, Content>, + /// Whether the list contains no paragraph breaks. + tight: bool, + /// Trailing content for which it is unclear whether it is part of the list. + staged: Vec<(&'a Content, StyleChain<'a>)>, +} + +impl<'a> ListBuilder<'a> { + fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { + if !self.items.is_empty() + && (content.is::<SpaceElem>() || content.is::<ParbreakElem>()) + { + self.staged.push((content, styles)); + return true; + } + + if (content.is::<ListItem>() + || content.is::<EnumItem>() + || content.is::<TermItem>()) + && self + .items + .elems() + .next() + .map_or(true, |first| first.func() == content.func()) + { + self.items.push(content.clone(), styles); + self.tight &= self.staged.drain(..).all(|(t, _)| !t.is::<ParbreakElem>()); + return true; + } + + false + } + + fn finish(self) -> (Content, StyleChain<'a>) { + let (items, shared) = self.items.finish(); + let item = items.items().next().unwrap(); + let output = if item.is::<ListItem>() { + ListElem::new( + items + .iter() + .map(|(item, local)| { + let item = item.to::<ListItem>().unwrap(); + item.clone().with_body(item.body().styled_with_map(local.clone())) + }) + .collect::<Vec<_>>(), + ) + .with_tight(self.tight) + .pack() + } else if item.is::<EnumItem>() { + EnumElem::new( + items + .iter() + .map(|(item, local)| { + let item = item.to::<EnumItem>().unwrap(); + item.clone().with_body(item.body().styled_with_map(local.clone())) + }) + .collect::<Vec<_>>(), + ) + .with_tight(self.tight) + .pack() + } else if item.is::<TermItem>() { + TermsElem::new( + items + .iter() + .map(|(item, local)| { + let item = item.to::<TermItem>().unwrap(); + item.clone() + .with_term(item.term().styled_with_map(local.clone())) + .with_description( + item.description().styled_with_map(local.clone()), + ) + }) + .collect::<Vec<_>>(), + ) + .with_tight(self.tight) + .pack() + } else { + unreachable!() + }; + (output, shared) + } +} + +impl Default for ListBuilder<'_> { + fn default() -> Self { + Self { + items: StyleVecBuilder::default(), + tight: true, + staged: vec![], + } + } +} diff --git a/crates/typst-library/src/layout/pad.rs b/crates/typst-library/src/layout/pad.rs new file mode 100644 index 00000000..a3d5646b --- /dev/null +++ b/crates/typst-library/src/layout/pad.rs @@ -0,0 +1,125 @@ +use crate::prelude::*; + +/// 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 } +/// ```example +/// #set align(center) +/// +/// #pad(x: 16pt, image("typing.jpg")) +/// _Typing speeds can be +/// measured in words per minute._ +/// ``` +/// +/// Display: Padding +/// Category: layout +#[element(Layout)] +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>, + + /// The horizontal padding. Both `left` and `right` take precedence over + /// this. + #[external] + pub x: Rel<Length>, + + /// The vertical padding. Both `top` and `bottom` take precedence over this. + #[external] + pub y: Rel<Length>, + + /// The padding for all sides. All other parameters take precedence over + /// this. + #[external] + pub rest: Rel<Length>, + + /// The content to pad at the sides. + #[required] + pub body: Content, +} + +impl Layout for PadElem { + #[tracing::instrument(name = "PadElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let sides = Sides::new( + self.left(styles), + self.top(styles), + self.right(styles), + self.bottom(styles), + ); + + // Layout child into padded regions. + let mut backlog = vec![]; + let padding = sides.resolve(styles); + let pod = regions.map(&mut backlog, |size| shrink(size, padding)); + let mut fragment = self.body().layout(vt, styles, pod)?; + + for frame in &mut fragment { + // Apply the padding inversely such that the grown size padded + // yields the frame's size. + let padded = grow(frame.size(), padding); + let padding = padding.relative_to(padded); + let offset = Point::new(padding.left, padding.top); + + // Grow the frame and translate everything in the frame inwards. + frame.set_size(padded); + frame.translate(offset); + } + + Ok(fragment) + } +} + +/// Shrink a size by padding relative to the size itself. +fn shrink(size: Size, padding: Sides<Rel<Abs>>) -> Size { + size - padding.relative_to(size).sum_by_axis() +} + +/// Grow a size by padding relative to the grown size. +/// This is the inverse operation to `shrink()`. +/// +/// For the horizontal axis the derivation looks as follows. +/// (Vertical axis is analogous.) +/// +/// Let w be the grown target width, +/// s be the given width, +/// l be the left padding, +/// r be the right padding, +/// p = l + r. +/// +/// We want that: w - l.resolve(w) - r.resolve(w) = s +/// +/// Thus: w - l.resolve(w) - r.resolve(w) = s +/// <=> w - p.resolve(w) = s +/// <=> w - p.rel * w - p.abs = s +/// <=> (1 - p.rel) * w = s + p.abs +/// <=> w = (s + p.abs) / (1 - p.rel) +fn grow(size: Size, padding: Sides<Rel<Abs>>) -> Size { + size.zip(padding.sum_by_axis()) + .map(|(s, p)| (s + p.abs).safe_div(1.0 - p.rel.get())) +} diff --git a/crates/typst-library/src/layout/page.rs b/crates/typst-library/src/layout/page.rs new file mode 100644 index 00000000..3b81f92c --- /dev/null +++ b/crates/typst-library/src/layout/page.rs @@ -0,0 +1,898 @@ +use std::ptr; +use std::str::FromStr; + +use super::{AlignElem, ColumnsElem}; +use crate::meta::{Counter, CounterKey, Numbering}; +use crate::prelude::*; +use crate::text::TextElem; + +/// 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. +/// +/// ## Example { #example } +/// ```example +/// >>> #set page(margin: auto) +/// #set page("us-letter") +/// +/// There you go, US friends! +/// ``` +/// +/// Display: Page +/// Category: layout +#[element] +pub struct PageElem { + /// A standard paper size to set width and height. + #[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()))] + 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]($func/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()))] + 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)] + pub flipped: bool, + + /// The page's margins. + /// + /// - A single length: The same margin on all sides. + /// - `{auto}`: The margin is set to the default value for the page's size. + /// - 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]($func/page.binding) is). + /// - `outside`: The margin at the outer side of the page (opposite to the + /// [binding]($func/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] + pub margin: Margin, + + /// On which side the pages will be bound. + /// + /// - `{auto}`: Equivalent to `left` if the [text direction]($func/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. + pub binding: Smart<Binding>, + + /// How many columns the page has. + /// + /// ```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)] + pub columns: NonZeroUsize, + + /// The page's background color. + /// + /// This instructs the printer to color the complete page with the given + /// color. 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. + /// + /// ```example + /// #set page(fill: rgb("444352")) + /// #set text(fill: rgb("fdfdfd")) + /// *Dark mode enabled.* + /// ``` + pub fill: Option<Paint>, + + /// How to [number]($func/numbering) the pages. + /// + /// If an explicit `footer` is given, the numbering is ignored. + /// + /// ```example + /// #set page( + /// height: 100pt, + /// margin: (top: 16pt, bottom: 24pt), + /// numbering: "1 / 1", + /// ) + /// + /// #lorem(48) + /// ``` + pub numbering: Option<Numbering>, + + /// The alignment of the page numbering. + /// + /// ```example + /// #set page( + /// margin: (top: 16pt, bottom: 24pt), + /// numbering: "1", + /// number-align: right, + /// ) + /// + /// #lorem(30) + /// ``` + #[default(Align::Center.into())] + pub number_align: Axes<Option<GenAlign>>, + + /// The page's header. Fills the top margin of each page. + /// + /// ```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) + /// ``` + pub header: Option<Content>, + + /// The amount the header is raised into the top margin. + #[resolve] + #[default(Ratio::new(0.3).into())] + pub header_ascent: Rel<Length>, + + /// The page's footer. Fills the bottom margin of each page. + /// + /// 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]($func/counter). + /// + /// ```example + /// #set par(justify: true) + /// #set page( + /// height: 100pt, + /// margin: 20pt, + /// footer: [ + /// #set align(right) + /// #set text(8pt) + /// #counter(page).display( + /// "1 of I", + /// both: true, + /// ) + /// ] + /// ) + /// + /// #lorem(48) + /// ``` + pub footer: Option<Content>, + + /// The amount the footer is lowered into the bottom margin. + #[resolve] + #[default(Ratio::new(0.3).into())] + 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). + /// ``` + 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... + /// ``` + 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. + #[required] + pub body: Content, + + /// Whether the page should be aligned to an even or odd page. + /// Not part of the public API for now. + #[internal] + pub clear_to: Option<Parity>, +} + +impl PageElem { + /// A document can consist of multiple `PageElem`s, one per run of pages + /// with equal properties (not one per actual output page!). The `number` is + /// the physical page number of the first page of this run. It is mutated + /// while we post-process the pages in this function. This function returns + /// a fragment consisting of multiple frames, one per output page of this + /// page run. + #[tracing::instrument(skip_all)] + pub fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + mut number: NonZeroUsize, + ) -> SourceResult<Fragment> { + tracing::info!("Page layout"); + + // When one of the lengths is infinite the page fits its content along + // that axis. + let width = self.width(styles).unwrap_or(Abs::inf()); + let height = self.height(styles).unwrap_or(Abs::inf()); + let mut size = Size::new(width, height); + if self.flipped(styles) { + std::mem::swap(&mut size.x, &mut size.y); + } + + let mut min = width.min(height); + if !min.is_finite() { + min = Paper::A4.width(); + } + + // Determine the margins. + let default = Rel::<Length>::from(0.1190 * min); + let margin = self.margin(styles); + let two_sided = margin.two_sided.unwrap_or(false); + let margin = margin + .sides + .map(|side| side.and_then(Smart::as_custom).unwrap_or(default)) + .resolve(styles) + .relative_to(size); + + // Determine the binding. + let binding = + self.binding(styles) + .unwrap_or_else(|| match TextElem::dir_in(styles) { + Dir::LTR => Binding::Left, + _ => Binding::Right, + }); + + // Realize columns. + let mut child = self.body(); + let columns = self.columns(styles); + if columns.get() > 1 { + child = ColumnsElem::new(child).with_count(columns).pack(); + } + + let area = size - margin.sum_by_axis(); + let mut regions = Regions::repeat(area, area.map(Abs::is_finite)); + regions.root = true; + + // Layout the child. + let mut frames = child.layout(vt, styles, regions)?.into_frames(); + + // Align the child to the pagebreak's parity. + if self.clear_to(styles).is_some_and(|p| !p.matches(number.get())) { + let size = area.map(Abs::is_finite).select(area, Size::zero()); + frames.insert(0, Frame::new(size)); + } + + let fill = self.fill(styles); + let foreground = self.foreground(styles); + let background = self.background(styles); + let header = self.header(styles); + let header_ascent = self.header_ascent(styles); + let footer = self.footer(styles).or_else(|| { + self.numbering(styles).map(|numbering| { + let both = match &numbering { + Numbering::Pattern(pattern) => pattern.pieces() >= 2, + Numbering::Func(_) => true, + }; + Counter::new(CounterKey::Page) + .display(Some(numbering), both) + .aligned(self.number_align(styles)) + }) + }); + let footer_descent = self.footer_descent(styles); + + let numbering_meta = FrameItem::Meta( + Meta::PageNumbering(self.numbering(styles).into_value()), + Size::zero(), + ); + + // Post-process pages. + for frame in frames.iter_mut() { + tracing::info!("Layouting page #{number}"); + + // The padded width of the page's content without margins. + let pw = frame.width(); + + // If two sided, left becomes inside and right becomes outside. + // Thus, for left-bound pages, we want to swap on even pages and + // for right-bound pages, we want to swap on odd pages. + let mut margin = margin; + if two_sided && binding.swap(number) { + std::mem::swap(&mut margin.left, &mut margin.right); + } + + // Realize margins. + frame.set_size(frame.size() + margin.sum_by_axis()); + frame.translate(Point::new(margin.left, margin.top)); + frame.push(Point::zero(), numbering_meta.clone()); + + // The page size with margins. + let size = frame.size(); + + // Realize overlays. + for (name, marginal) in [ + ("header", &header), + ("footer", &footer), + ("background", &background), + ("foreground", &foreground), + ] { + tracing::info!("Layouting {name}"); + + let Some(content) = marginal else { continue }; + + let (pos, area, align); + if ptr::eq(marginal, &header) { + let ascent = header_ascent.relative_to(margin.top); + pos = Point::with_x(margin.left); + area = Size::new(pw, margin.top - ascent); + align = Align::Bottom.into(); + } else if ptr::eq(marginal, &footer) { + let descent = footer_descent.relative_to(margin.bottom); + pos = Point::new(margin.left, size.y - margin.bottom + descent); + area = Size::new(pw, margin.bottom - descent); + align = Align::Top.into(); + } else { + pos = Point::zero(); + area = size; + align = Align::CENTER_HORIZON.into(); + }; + + let pod = Regions::one(area, Axes::splat(true)); + let sub = content + .clone() + .styled(AlignElem::set_alignment(align)) + .layout(vt, styles, pod)? + .into_frame(); + + if ptr::eq(marginal, &header) || ptr::eq(marginal, &background) { + frame.prepend_frame(pos, sub); + } else { + frame.push_frame(pos, sub); + } + } + + if let Some(fill) = &fill { + frame.fill(fill.clone()); + } + + number = number.saturating_add(1); + } + + Ok(Fragment::frames(frames)) + } +} + +/// Specification of the page's margins. +#[derive(Debug, Default, 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 Fold for Margin { + type Output = Margin; + + fn fold(self, outer: Self::Output) -> Self::Output { + let sides = + self.sides + .zip(outer.sides) + .map(|(inner, outer)| match (inner, outer) { + (Some(value), Some(outer)) => Some(value.fold(outer)), + _ => inner.or(outer), + }); + let two_sided = self.two_sided.or(outer.two_sided); + Margin { sides, two_sided } + } +} + +cast! { + Margin, + self => { + let mut dict = Dict::new(); + let mut handle = |key: &str, component: Value| { + let value = component.into_value(); + if value != Value::None { + dict.insert(key.into(), value); + } + }; + + handle("top", self.sides.top.into_value()); + handle("bottom", self.sides.bottom.into_value()); + if self.two_sided.unwrap_or(false) { + handle("inside", self.sides.left.into_value()); + handle("outside", self.sides.right.into_value()); + } else { + handle("left", self.sides.left.into_value()); + handle("right", self.sides.right.into_value()); + } + + 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. + 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 => GenAlign::Specific(Align::Left).into_value(), + Self::Right => GenAlign::Specific(Align::Right).into_value(), + }, + v: GenAlign => match v { + GenAlign::Specific(Align::Left) => Self::Left, + GenAlign::Specific(Align::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, vt: &mut Vt, page: usize) -> SourceResult<Content> { + Ok(match self { + Self::Content(content) => content.clone(), + Self::Func(func) => func.call_vt(vt, [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 manual page break. +/// +/// Must not be used inside any containers. +/// +/// ## Example { #example } +/// ```example +/// The next page contains +/// more details on compound theory. +/// #pagebreak() +/// +/// == Compound Theory +/// In 1984, the first ... +/// ``` +/// +/// Display: Page Break +/// Category: layout +#[element] +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 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. + 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.0) + } + + /// The height of the paper. + pub fn height(self) -> Abs { + Abs::mm(self.height.0) + } +} + +/// 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($width), + height: Scalar($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/par.rs b/crates/typst-library/src/layout/par.rs new file mode 100644 index 00000000..6b914e80 --- /dev/null +++ b/crates/typst-library/src/layout/par.rs @@ -0,0 +1,1566 @@ +use icu_properties::{maps::CodePointMapData, LineBreak}; +use icu_provider::AsDeserializingBufferProvider; +use icu_provider_adapters::fork::ForkByKeyProvider; +use icu_provider_blob::BlobDataProvider; +use icu_segmenter::{LineBreakIteratorUtf8, LineSegmenter}; +use once_cell::sync::Lazy; +use typst::eval::Tracer; +use typst::model::DelayedErrors; +use unicode_bidi::{BidiInfo, Level as BidiLevel}; +use unicode_script::{Script, UnicodeScript}; + +use super::{BoxElem, HElem, Sizing, Spacing}; +use crate::layout::AlignElem; +use crate::math::EquationElem; +use crate::prelude::*; +use crate::text::{ + is_gb_style, shape, LinebreakElem, Quoter, Quotes, ShapedText, SmartQuoteElem, + SpaceElem, TextElem, +}; + +/// 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 } +/// ```example +/// #show par: set block(spacing: 0.65em) +/// #set par( +/// first-line-indent: 1em, +/// 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 ... +/// ``` +/// +/// Display: Paragraph +/// Category: layout +#[element(Construct)] +pub struct ParElem { + /// The spacing between lines. + #[resolve] + #[default(Em::new(0.65).into())] + pub leading: Length, + + /// Whether to justify text in its line. + /// + /// Hyphenation will be enabled for justified paragraphs if the [text + /// property hyphenate]($func/text.hyphenate) is set to `{auto}` and the + /// current language is known. + /// + /// Note that the current [alignment]($func/align) still has an effect on + /// the placement of the last line except if it ends with a [justified line + /// break]($func/linebreak.justify). + #[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: 190pt) + /// #set par(linebreaks: "simple") + /// Some texts are frustratingly + /// challenging to break in a + /// visually pleasing way. This + /// very aesthetic example is one + /// of them. + /// + /// #set par(linebreaks: "optimized") + /// Some texts are frustratingly + /// challenging to break in a + /// visually pleasing way. This + /// very aesthetic example is one + /// of them. + /// ``` + #[default] + 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]($func/block.spacing) to the [`leading`] when + /// using this property (e.g. using + /// `[#show par: set block(spacing: 0.65em)]`). + pub first_line_indent: Length, + + /// The indent all but the first line of a paragraph should have. + #[resolve] + pub hanging_indent: Length, + + /// The contents of the paragraph. + #[external] + #[required] + pub body: Content, + + /// The paragraph's children. + #[internal] + #[variadic] + pub children: Vec<Content>, +} + +impl Construct for ParElem { + fn construct(_: &mut Vm, 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(args)?; + let body = args.expect::<Content>("body")?; + Ok(Content::sequence([ + ParbreakElem::new().pack(), + body.styled_with_map(styles), + ParbreakElem::new().pack(), + ])) + } +} + +impl ParElem { + /// Layout the paragraph into a collection of lines. + #[tracing::instrument(name = "ParElement::layout", skip_all)] + pub fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + consecutive: bool, + region: Size, + expand: bool, + ) -> SourceResult<Fragment> { + #[comemo::memoize] + #[allow(clippy::too_many_arguments)] + fn cached( + par: &ParElem, + world: Tracked<dyn World + '_>, + introspector: Tracked<Introspector>, + locator: Tracked<Locator>, + delayed: TrackedMut<DelayedErrors>, + tracer: TrackedMut<Tracer>, + styles: StyleChain, + consecutive: bool, + region: Size, + expand: bool, + ) -> SourceResult<Fragment> { + let mut locator = Locator::chained(locator); + let mut vt = Vt { + world, + introspector, + locator: &mut locator, + delayed, + tracer, + }; + let children = par.children(); + + // Collect all text into one string for BiDi analysis. + let (text, segments, spans) = collect(&children, &styles, consecutive)?; + + // Perform BiDi analysis and then prepare paragraph layout by building a + // representation on which we can do line breaking without layouting + // each and every line from scratch. + let p = prepare(&mut vt, &children, &text, segments, spans, styles, region)?; + + // Break the paragraph into lines. + let lines = linebreak(&vt, &p, region.x - p.hang); + + // Stack the lines into one frame per region. + finalize(&mut vt, &p, &lines, region, expand) + } + + let fragment = cached( + self, + vt.world, + vt.introspector, + vt.locator.track(), + TrackedMut::reborrow_mut(&mut vt.delayed), + TrackedMut::reborrow_mut(&mut vt.tracer), + styles, + consecutive, + region, + expand, + )?; + + vt.locator.visit_frames(&fragment); + Ok(fragment) + } +} + +/// 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 } +/// ```example +/// #for i in range(3) { +/// [Blind text #i: ] +/// lorem(5) +/// parbreak() +/// } +/// ``` +/// +/// ## Syntax { #syntax } +/// Instead of calling this function, you can insert a blank line into your +/// markup to create a paragraph break. +/// +/// Display: Paragraph Break +/// Category: layout +#[element(Unlabellable)] +pub struct ParbreakElem {} + +impl Unlabellable for ParbreakElem {} + +/// Range of a substring of text. +type Range = std::ops::Range<usize>; + +// The characters by which spacing, inline content and pins are replaced in the +// paragraph's full text. +const SPACING_REPLACE: char = ' '; // Space +const OBJ_REPLACE: char = '\u{FFFC}'; // Object Replacement Character + +/// A paragraph representation in which children are already layouted and text +/// is already preshaped. +/// +/// In many cases, we can directly reuse these results when constructing a line. +/// Only when a line break falls onto a text index that is not safe-to-break per +/// rustybuzz, we have to reshape that portion. +struct Preparation<'a> { + /// Bidirectional text embedding levels for the paragraph. + bidi: BidiInfo<'a>, + /// Text runs, spacing and layouted elements. + items: Vec<Item<'a>>, + /// The span mapper. + spans: SpanMapper, + /// The styles shared by all children. + styles: StyleChain<'a>, + /// Whether to hyphenate if it's the same for all children. + hyphenate: Option<bool>, + /// The text language if it's the same for all children. + lang: Option<Lang>, + /// The paragraph's resolved alignment. + align: Align, + /// Whether to justify the paragraph. + justify: bool, + /// The paragraph's hanging indent. + hang: Abs, +} + +impl<'a> Preparation<'a> { + /// Find the item that contains the given `text_offset`. + fn find(&self, text_offset: usize) -> Option<&Item<'a>> { + let mut cursor = 0; + for item in &self.items { + let end = cursor + item.len(); + if (cursor..end).contains(&text_offset) { + return Some(item); + } + cursor = end; + } + None + } + + /// Return the items that intersect the given `text_range`. + /// + /// Returns the expanded range around the items and the items. + fn slice(&self, text_range: Range) -> (Range, &[Item<'a>]) { + let mut cursor = 0; + let mut start = 0; + let mut end = 0; + let mut expanded = text_range.clone(); + + for (i, item) in self.items.iter().enumerate() { + if cursor <= text_range.start { + start = i; + expanded.start = cursor; + } + + let len = item.len(); + if cursor < text_range.end || cursor + len <= text_range.end { + end = i + 1; + expanded.end = cursor + len; + } else { + break; + } + + cursor += len; + } + + (expanded, &self.items[start..end]) + } +} + +/// A segment of one or multiple collapsed children. +#[derive(Debug, Copy, Clone)] +enum Segment<'a> { + /// One or multiple collapsed text or text-equivalent children. Stores how + /// long the segment is (in bytes of the full text string). + Text(usize), + /// Horizontal spacing between other segments. + Spacing(Spacing), + /// A mathematical equation. + Equation(&'a EquationElem), + /// A box with arbitrary content. + Box(&'a BoxElem, bool), + /// Metadata. + Meta, +} + +impl Segment<'_> { + /// The text length of the item. + fn len(&self) -> usize { + match *self { + Self::Text(len) => len, + Self::Spacing(_) => SPACING_REPLACE.len_utf8(), + Self::Box(_, true) => SPACING_REPLACE.len_utf8(), + Self::Equation(_) | Self::Box(_, _) => OBJ_REPLACE.len_utf8(), + Self::Meta => 0, + } + } +} + +/// A prepared item in a paragraph layout. +#[derive(Debug)] +enum Item<'a> { + /// A shaped text run with consistent style and direction. + Text(ShapedText<'a>), + /// Absolute spacing between other items. + Absolute(Abs), + /// Fractional spacing between other items. + Fractional(Fr, Option<(&'a BoxElem, StyleChain<'a>)>), + /// Layouted inline-level content. + Frame(Frame), + /// Metadata. + Meta(Frame), +} + +impl<'a> Item<'a> { + /// If this a text item, return it. + fn text(&self) -> Option<&ShapedText<'a>> { + match self { + Self::Text(shaped) => Some(shaped), + _ => None, + } + } + + fn text_mut(&mut self) -> Option<&mut ShapedText<'a>> { + match self { + Self::Text(shaped) => Some(shaped), + _ => None, + } + } + + /// The text length of the item. + fn len(&self) -> usize { + match self { + Self::Text(shaped) => shaped.text.len(), + Self::Absolute(_) | Self::Fractional(_, _) => SPACING_REPLACE.len_utf8(), + Self::Frame(_) => OBJ_REPLACE.len_utf8(), + Self::Meta(_) => 0, + } + } + + /// The natural layouted width of the item. + fn width(&self) -> Abs { + match self { + Self::Text(shaped) => shaped.width, + Self::Absolute(v) => *v, + Self::Frame(frame) => frame.width(), + Self::Fractional(_, _) | Self::Meta(_) => Abs::zero(), + } + } +} + +/// Maps byte offsets back to spans. +#[derive(Default)] +pub struct SpanMapper(Vec<(usize, Span)>); + +impl SpanMapper { + /// Create a new span mapper. + pub fn new() -> Self { + Self::default() + } + + /// Push a span for a segment with the given length. + pub fn push(&mut self, len: usize, span: Span) { + self.0.push((len, span)); + } + + /// Determine the span at the given byte offset. + /// + /// May return a detached span. + pub fn span_at(&self, offset: usize) -> (Span, u16) { + let mut cursor = 0; + for &(len, span) in &self.0 { + if (cursor..=cursor + len).contains(&offset) { + return (span, u16::try_from(offset - cursor).unwrap_or(0)); + } + cursor += len; + } + (Span::detached(), 0) + } +} + +/// A layouted line, consisting of a sequence of layouted paragraph items that +/// are mostly borrowed from the preparation phase. This type enables you to +/// measure the size of a line in a range before committing to building the +/// line's frame. +/// +/// At most two paragraph items must be created individually for this line: The +/// first and last one since they may be broken apart by the start or end of the +/// line, respectively. But even those can partially reuse previous results when +/// the break index is safe-to-break per rustybuzz. +struct Line<'a> { + /// Bidi information about the paragraph. + bidi: &'a BidiInfo<'a>, + /// The trimmed range the line spans in the paragraph. + trimmed: Range, + /// The untrimmed end where the line ends. + end: usize, + /// A reshaped text item if the line sliced up a text item at the start. + first: Option<Item<'a>>, + /// Inner items which don't need to be reprocessed. + inner: &'a [Item<'a>], + /// A reshaped text item if the line sliced up a text item at the end. If + /// there is only one text item, this takes precedence over `first`. + last: Option<Item<'a>>, + /// The width of the line. + width: Abs, + /// Whether the line should be justified. + justify: bool, + /// Whether the line ends with a hyphen or dash, either naturally or through + /// hyphenation. + dash: bool, +} + +impl<'a> Line<'a> { + /// Iterate over the line's items. + fn items(&self) -> impl Iterator<Item = &Item<'a>> { + self.first.iter().chain(self.inner).chain(&self.last) + } + + /// Return items that intersect the given `text_range`. + fn slice(&self, text_range: Range) -> impl Iterator<Item = &Item<'a>> { + let mut cursor = self.trimmed.start; + let mut start = 0; + let mut end = 0; + + for (i, item) in self.items().enumerate() { + if cursor <= text_range.start { + start = i; + } + + let len = item.len(); + if cursor < text_range.end || cursor + len <= text_range.end { + end = i + 1; + } else { + break; + } + + cursor += len; + } + + self.items().skip(start).take(end - start) + } + + /// How many glyphs are in the text where we can insert additional + /// space when encountering underfull lines. + fn justifiables(&self) -> usize { + let mut count = 0; + for shaped in self.items().filter_map(Item::text) { + count += shaped.justifiables(); + } + // CJK character at line end should not be adjusted. + if self + .items() + .last() + .and_then(Item::text) + .map(|s| s.cjk_justifiable_at_last()) + .unwrap_or(false) + { + count -= 1; + } + + count + } + + /// How much can the line stretch + fn stretchability(&self) -> Abs { + self.items().filter_map(Item::text).map(|s| s.stretchability()).sum() + } + + /// How much can the line shrink + fn shrinkability(&self) -> Abs { + self.items().filter_map(Item::text).map(|s| s.shrinkability()).sum() + } + + /// The sum of fractions in the line. + fn fr(&self) -> Fr { + self.items() + .filter_map(|item| match item { + Item::Fractional(fr, _) => Some(*fr), + _ => None, + }) + .sum() + } +} + +/// Collect all text of the paragraph into one string. This also performs +/// string-level preprocessing like case transformations. +#[allow(clippy::type_complexity)] +fn collect<'a>( + children: &'a [Content], + styles: &'a StyleChain<'a>, + consecutive: bool, +) -> SourceResult<(String, Vec<(Segment<'a>, StyleChain<'a>)>, SpanMapper)> { + let mut full = String::new(); + let mut quoter = Quoter::new(); + let mut segments = vec![]; + let mut spans = SpanMapper::new(); + let mut iter = children.iter().peekable(); + + let first_line_indent = ParElem::first_line_indent_in(*styles); + if !first_line_indent.is_zero() + && consecutive + && AlignElem::alignment_in(*styles).x.resolve(*styles) + == TextElem::dir_in(*styles).start().into() + { + full.push(SPACING_REPLACE); + segments.push((Segment::Spacing(first_line_indent.into()), *styles)); + } + + let hang = ParElem::hanging_indent_in(*styles); + if !hang.is_zero() { + full.push(SPACING_REPLACE); + segments.push((Segment::Spacing((-hang).into()), *styles)); + } + + while let Some(mut child) = iter.next() { + let outer = styles; + let mut styles = *styles; + if let Some((elem, local)) = child.to_styled() { + child = elem; + styles = outer.chain(local); + } + + let segment = if child.is::<SpaceElem>() { + full.push(' '); + Segment::Text(1) + } else if let Some(elem) = child.to::<TextElem>() { + let prev = full.len(); + if let Some(case) = TextElem::case_in(styles) { + full.push_str(&case.apply(&elem.text())); + } else { + full.push_str(&elem.text()); + } + Segment::Text(full.len() - prev) + } else if let Some(elem) = child.to::<HElem>() { + if elem.amount().is_zero() { + continue; + } + + full.push(SPACING_REPLACE); + Segment::Spacing(elem.amount()) + } else if let Some(elem) = child.to::<LinebreakElem>() { + let c = if elem.justify(styles) { '\u{2028}' } else { '\n' }; + full.push(c); + Segment::Text(c.len_utf8()) + } else if let Some(elem) = child.to::<SmartQuoteElem>() { + let prev = full.len(); + if SmartQuoteElem::enabled_in(styles) { + let lang = TextElem::lang_in(styles); + let region = TextElem::region_in(styles); + let quotes = Quotes::from_lang(lang, region); + let peeked = iter.peek().and_then(|child| { + let child = if let Some((child, _)) = child.to_styled() { + child + } else { + child + }; + if let Some(elem) = child.to::<TextElem>() { + elem.text().chars().next() + } else if child.is::<SmartQuoteElem>() { + Some('"') + } else if child.is::<SpaceElem>() + || child.is::<HElem>() + || child.is::<LinebreakElem>() + { + Some(SPACING_REPLACE) + } else { + Some(OBJ_REPLACE) + } + }); + + full.push_str(quoter.quote("es, elem.double(styles), peeked)); + } else { + full.push(if elem.double(styles) { '"' } else { '\'' }); + } + Segment::Text(full.len() - prev) + } else if let Some(elem) = child.to::<EquationElem>() { + full.push(OBJ_REPLACE); + Segment::Equation(elem) + } else if let Some(elem) = child.to::<BoxElem>() { + let frac = elem.width(styles).is_fractional(); + full.push(if frac { SPACING_REPLACE } else { OBJ_REPLACE }); + Segment::Box(elem, frac) + } else if child.is::<MetaElem>() { + Segment::Meta + } else { + bail!(child.span(), "unexpected paragraph child"); + }; + + if let Some(last) = full.chars().last() { + quoter.last(last); + } + + spans.push(segment.len(), child.span()); + + if let (Some((Segment::Text(last_len), last_styles)), Segment::Text(len)) = + (segments.last_mut(), segment) + { + if *last_styles == styles { + *last_len += len; + continue; + } + } + + segments.push((segment, styles)); + } + + Ok((full, segments, spans)) +} + +/// Prepare paragraph layout by shaping the whole paragraph and layouting all +/// contained inline-level content. +fn prepare<'a>( + vt: &mut Vt, + children: &'a [Content], + text: &'a str, + segments: Vec<(Segment<'a>, StyleChain<'a>)>, + spans: SpanMapper, + styles: StyleChain<'a>, + region: Size, +) -> SourceResult<Preparation<'a>> { + let bidi = BidiInfo::new( + text, + match TextElem::dir_in(styles) { + Dir::LTR => Some(BidiLevel::ltr()), + Dir::RTL => Some(BidiLevel::rtl()), + _ => None, + }, + ); + + let mut cursor = 0; + let mut items = vec![]; + + // Shape / layout the children and collect them into items. + for (segment, styles) in segments { + let end = cursor + segment.len(); + match segment { + Segment::Text(_) => { + shape_range(&mut items, vt, &bidi, cursor..end, &spans, styles); + } + Segment::Spacing(spacing) => match spacing { + Spacing::Rel(v) => { + let resolved = v.resolve(styles).relative_to(region.x); + items.push(Item::Absolute(resolved)); + } + Spacing::Fr(v) => { + items.push(Item::Fractional(v, None)); + } + }, + Segment::Equation(equation) => { + let pod = Regions::one(region, Axes::splat(false)); + let mut frame = equation.layout(vt, styles, pod)?.into_frame(); + frame.translate(Point::with_y(TextElem::baseline_in(styles))); + items.push(Item::Frame(frame)); + } + Segment::Box(elem, _) => { + if let Sizing::Fr(v) = elem.width(styles) { + items.push(Item::Fractional(v, Some((elem, styles)))); + } else { + let pod = Regions::one(region, Axes::splat(false)); + let mut frame = elem.layout(vt, styles, pod)?.into_frame(); + frame.translate(Point::with_y(TextElem::baseline_in(styles))); + items.push(Item::Frame(frame)); + } + } + Segment::Meta => { + let mut frame = Frame::new(Size::zero()); + frame.meta(styles, true); + items.push(Item::Meta(frame)); + } + } + + cursor = end; + } + + Ok(Preparation { + bidi, + items, + spans, + styles, + hyphenate: shared_get(styles, children, TextElem::hyphenate_in), + lang: shared_get(styles, children, TextElem::lang_in), + align: AlignElem::alignment_in(styles).x.resolve(styles), + justify: ParElem::justify_in(styles), + hang: ParElem::hanging_indent_in(styles), + }) +} + +/// Group a range of text by BiDi level and script, shape the runs and generate +/// items for them. +fn shape_range<'a>( + items: &mut Vec<Item<'a>>, + vt: &Vt, + bidi: &BidiInfo<'a>, + range: Range, + spans: &SpanMapper, + styles: StyleChain<'a>, +) { + let lang = TextElem::lang_in(styles); + let region = TextElem::region_in(styles); + let mut process = |range: Range, level: BidiLevel| { + let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL }; + let shaped = + shape(vt, range.start, &bidi.text[range], spans, styles, dir, lang, region); + items.push(Item::Text(shaped)); + }; + + let mut prev_level = BidiLevel::ltr(); + let mut prev_script = Script::Unknown; + let mut cursor = range.start; + + // Group by embedding level and script. + for i in range.clone() { + if !bidi.text.is_char_boundary(i) { + continue; + } + + let level = bidi.levels[i]; + let script = + bidi.text[i..].chars().next().map_or(Script::Unknown, |c| c.script()); + + if level != prev_level || !is_compatible(script, prev_script) { + if cursor < i { + process(cursor..i, prev_level); + } + cursor = i; + prev_level = level; + prev_script = script; + } else if is_generic_script(prev_script) { + prev_script = script; + } + } + + process(cursor..range.end, prev_level); +} + +/// Whether this is not a specific script. +fn is_generic_script(script: Script) -> bool { + matches!(script, Script::Unknown | Script::Common | Script::Inherited) +} + +/// Whether these script can be part of the same shape run. +fn is_compatible(a: Script, b: Script) -> bool { + is_generic_script(a) || is_generic_script(b) || a == b +} + +/// Get a style property, but only if it is the same for all children of the +/// paragraph. +fn shared_get<T: PartialEq>( + styles: StyleChain<'_>, + children: &[Content], + getter: fn(StyleChain) -> T, +) -> Option<T> { + let value = getter(styles); + children + .iter() + .filter_map(|child| child.to_styled()) + .all(|(_, local)| getter(styles.chain(local)) == value) + .then_some(value) +} + +/// Find suitable linebreaks. +fn linebreak<'a>(vt: &Vt, p: &'a Preparation<'a>, width: Abs) -> Vec<Line<'a>> { + let linebreaks = ParElem::linebreaks_in(p.styles).unwrap_or_else(|| { + if ParElem::justify_in(p.styles) { + Linebreaks::Optimized + } else { + Linebreaks::Simple + } + }); + + match linebreaks { + Linebreaks::Simple => linebreak_simple(vt, p, width), + Linebreaks::Optimized => linebreak_optimized(vt, p, width), + } +} + +/// Perform line breaking in simple first-fit style. This means that we build +/// lines greedily, always taking the longest possible line. This may lead to +/// very unbalanced line, but is fast and simple. +fn linebreak_simple<'a>(vt: &Vt, p: &'a Preparation<'a>, width: Abs) -> Vec<Line<'a>> { + let mut lines = vec![]; + let mut start = 0; + let mut last = None; + + for (end, mandatory, hyphen) in breakpoints(p) { + // Compute the line and its size. + let mut attempt = line(vt, p, start..end, mandatory, hyphen); + + // If the line doesn't fit anymore, we push the last fitting attempt + // into the stack and rebuild the line from the attempt's end. The + // resulting line cannot be broken up further. + if !width.fits(attempt.width) { + if let Some((last_attempt, last_end)) = last.take() { + lines.push(last_attempt); + start = last_end; + attempt = line(vt, p, start..end, mandatory, hyphen); + } + } + + // Finish the current line if there is a mandatory line break (i.e. + // due to "\n") or if the line doesn't fit horizontally already + // since then no shorter line will be possible. + if mandatory || !width.fits(attempt.width) { + lines.push(attempt); + start = end; + last = None; + } else { + last = Some((attempt, end)); + } + } + + if let Some((line, _)) = last { + lines.push(line); + } + + lines +} + +/// Perform line breaking in optimized Knuth-Plass style. Here, we use more +/// context to determine the line breaks than in the simple first-fit style. For +/// example, we might choose to cut a line short even though there is still a +/// bit of space to improve the fit of one of the following lines. The +/// Knuth-Plass algorithm is based on the idea of "cost". A line which has a +/// very tight or very loose fit has a higher cost than one that is just right. +/// Ending a line with a hyphen incurs extra cost and endings two successive +/// lines with hyphens even more. +/// +/// To find the layout with the minimal total cost the algorithm uses dynamic +/// programming: For each possible breakpoint it determines the optimal +/// paragraph layout _up to that point_. It walks over all possible start points +/// for a line ending at that point and finds the one for which the cost of the +/// line plus the cost of the optimal paragraph up to the start point (already +/// computed and stored in dynamic programming table) is minimal. The final +/// result is simply the layout determined for the last breakpoint at the end of +/// text. +fn linebreak_optimized<'a>(vt: &Vt, p: &'a Preparation<'a>, width: Abs) -> Vec<Line<'a>> { + /// The cost of a line or paragraph layout. + type Cost = f64; + + /// An entry in the dynamic programming table. + struct Entry<'a> { + pred: usize, + total: Cost, + line: Line<'a>, + } + + // Cost parameters. + const HYPH_COST: Cost = 0.5; + const CONSECUTIVE_DASH_COST: Cost = 300.0; + const MAX_COST: Cost = 1_000_000.0; + const MIN_RATIO: f64 = -1.0; + + // Dynamic programming table. + let mut active = 0; + let mut table = vec![Entry { + pred: 0, + total: 0.0, + line: line(vt, p, 0..0, false, false), + }]; + + let em = TextElem::size_in(p.styles); + + for (end, mandatory, hyphen) in breakpoints(p) { + let k = table.len(); + let eof = end == p.bidi.text.len(); + let mut best: Option<Entry> = None; + + // Find the optimal predecessor. + for (i, pred) in table.iter_mut().enumerate().skip(active) { + // Layout the line. + let start = pred.line.end; + let attempt = line(vt, p, start..end, mandatory, hyphen); + + // Determine how much the line's spaces would need to be stretched + // to make it the desired width. + let delta = width - attempt.width; + // Determine how much stretch are permitted. + let adjust = if delta >= Abs::zero() { + attempt.stretchability() + } else { + attempt.shrinkability() + }; + // Ideally, the ratio should between -1.0 and 1.0, but sometimes a value above 1.0 + // is possible, in which case the line is underfull. + let mut ratio = delta / adjust; + if ratio.is_nan() { + // The line is not stretchable, but it just fits. + // This often happens with monospace fonts and CJK texts. + ratio = 0.0; + } + if ratio > 1.0 { + // We should stretch the line above its stretchability. Now calculate the extra amount. + let extra_stretch = (delta - adjust) / attempt.justifiables() as f64; + // Normalize the amount by half Em size. + ratio = 1.0 + extra_stretch / (em / 2.0); + } + + // Determine the cost of the line. + let min_ratio = if p.justify { MIN_RATIO } else { 0.0 }; + let mut cost = if ratio < min_ratio { + // The line is overfull. This is the case if + // - justification is on, but we'd need to shrink too much + // - justification is off and the line just doesn't fit + // Since any longer line will also be overfull, we can deactivate + // this breakpoint. + active = i + 1; + MAX_COST + } else if mandatory || eof { + // This is a mandatory break and the line is not overfull, so + // all breakpoints before this one become inactive since no line + // can span above the mandatory break. + active = k; + // If ratio > 0, we need to stretch the line only when justify is needed. + // If ratio < 0, we always need to shrink the line. + if (ratio > 0.0 && attempt.justify) || ratio < 0.0 { + ratio.powi(3).abs() + } else { + 0.0 + } + } else { + // Normal line with cost of |ratio^3|. + ratio.powi(3).abs() + }; + + // Penalize hyphens. + if hyphen { + cost += HYPH_COST; + } + + // In Knuth paper, cost = (1 + 100|r|^3 + p)^2 + a, + // where r is the ratio, p=50 is penaty, and a=3000 is consecutive penaty. + // We divide the whole formula by 10, resulting (0.01 + |r|^3 + p)^2 + a, + // where p=0.5 and a=300 + cost = (0.01 + cost).powi(2); + + // Penalize two consecutive dashes (not necessarily hyphens) extra. + if attempt.dash && pred.line.dash { + cost += CONSECUTIVE_DASH_COST; + } + + // The total cost of this line and its chain of predecessors. + let total = pred.total + cost; + + // If this attempt is better than what we had before, take it! + if best.as_ref().map_or(true, |best| best.total >= total) { + best = Some(Entry { pred: i, total, line: attempt }); + } + } + + table.push(best.unwrap()); + } + + // Retrace the best path. + let mut lines = vec![]; + let mut idx = table.len() - 1; + while idx != 0 { + table.truncate(idx + 1); + let entry = table.pop().unwrap(); + lines.push(entry.line); + idx = entry.pred; + } + + lines.reverse(); + lines +} + +/// Generated by the following command: +/// +/// ```sh +/// icu4x-datagen --locales full --keys-for-bin target/debug/typst \ +/// --format blob --out library/assets/icudata.postcard --overwrite +/// ``` +/// +/// Install icu4x-datagen with `cargo install icu4x-datagen`. +static ICU_DATA: &[u8] = include_bytes!("../../assets/icudata.postcard"); + +/// Generated by the following command: +/// +/// ```sh +/// icu4x-datagen --locales zh ja --keys segmenter/line@1 --format blob \ +/// --out library/assets/cj_linebreak_data.postcard --overwrite +/// ``` +/// +/// The used icu4x-datagen should be patched by +/// https://github.com/peng1999/icu4x/commit/b9beb6cbf633d61fc3d7983e5baf7f4449fbfae5 +static CJ_LINEBREAK_DATA: &[u8] = + include_bytes!("../../assets/cj_linebreak_data.postcard"); + +/// The general line break segmenter. +static SEGMENTER: Lazy<LineSegmenter> = Lazy::new(|| { + let provider = BlobDataProvider::try_new_from_static_blob(ICU_DATA).unwrap(); + LineSegmenter::try_new_lstm_with_buffer_provider(&provider).unwrap() +}); + +/// The Unicode line break properties for each code point. +static CJ_SEGMENTER: Lazy<LineSegmenter> = Lazy::new(|| { + let provider = BlobDataProvider::try_new_from_static_blob(ICU_DATA).unwrap(); + let cj_blob = BlobDataProvider::try_new_from_static_blob(CJ_LINEBREAK_DATA).unwrap(); + let cj_provider = ForkByKeyProvider::new(cj_blob, provider); + LineSegmenter::try_new_lstm_with_buffer_provider(&cj_provider).unwrap() +}); + +/// The line break segmenter for Chinese/Jpanese text. +static LINEBREAK_DATA: Lazy<CodePointMapData<LineBreak>> = Lazy::new(|| { + let provider = BlobDataProvider::try_new_from_static_blob(ICU_DATA).unwrap(); + let deser_provider = provider.as_deserializing(); + icu_properties::maps::load_line_break(&deser_provider).unwrap() +}); + +/// Determine all possible points in the text where lines can broken. +/// +/// Returns for each breakpoint the text index, whether the break is mandatory +/// (after `\n`) and whether a hyphen is required (when breaking inside of a +/// word). +fn breakpoints<'a>(p: &'a Preparation<'a>) -> Breakpoints<'a> { + let mut linebreaks = if matches!(p.lang, Some(Lang::CHINESE | Lang::JAPANESE)) { + CJ_SEGMENTER.segment_str(p.bidi.text) + } else { + SEGMENTER.segment_str(p.bidi.text) + }; + // The iterator always yields a breakpoint at index 0, we want to ignore it + linebreaks.next(); + Breakpoints { + p, + linebreaks, + syllables: None, + offset: 0, + suffix: 0, + end: 0, + mandatory: false, + } +} + +/// An iterator over the line break opportunities in a text. +struct Breakpoints<'a> { + /// The paragraph's items. + p: &'a Preparation<'a>, + /// The inner iterator over the unicode line break opportunities. + linebreaks: LineBreakIteratorUtf8<'a, 'a>, + /// Iterator over syllables of the current word. + syllables: Option<hypher::Syllables<'a>>, + /// The current text offset. + offset: usize, + /// The trimmed end of the current word. + suffix: usize, + /// The untrimmed end of the current word. + end: usize, + /// Whether the break after the current word is mandatory. + mandatory: bool, +} + +impl Iterator for Breakpoints<'_> { + type Item = (usize, bool, bool); + + fn next(&mut self) -> Option<Self::Item> { + // If we're currently in a hyphenated "word", process the next syllable. + if let Some(syllable) = self.syllables.as_mut().and_then(Iterator::next) { + self.offset += syllable.len(); + if self.offset == self.suffix { + self.offset = self.end; + } + + // Filter out hyphenation opportunities where hyphenation was + // actually disabled. + let hyphen = self.offset < self.end; + if hyphen && !self.hyphenate(self.offset) { + return self.next(); + } + + return Some((self.offset, self.mandatory && !hyphen, hyphen)); + } + + let lb = LINEBREAK_DATA.as_borrowed(); + + // Get the next "word". + self.end = self.linebreaks.next()?; + self.mandatory = + self.p.bidi.text[..self.end].chars().next_back().map_or(false, |c| { + matches!( + lb.get(c), + LineBreak::MandatoryBreak + | LineBreak::CarriageReturn + | LineBreak::LineFeed + | LineBreak::NextLine + ) || self.end == self.p.bidi.text.len() + }); + + // Hyphenate the next word. + if self.p.hyphenate != Some(false) { + if let Some(lang) = self.lang(self.offset) { + let word = &self.p.bidi.text[self.offset..self.end]; + let trimmed = word.trim_end_matches(|c: char| !c.is_alphabetic()); + if !trimmed.is_empty() { + self.suffix = self.offset + trimmed.len(); + self.syllables = Some(hypher::hyphenate(trimmed, lang)); + return self.next(); + } + } + } + + self.offset = self.end; + Some((self.end, self.mandatory, false)) + } +} + +impl Breakpoints<'_> { + /// Whether hyphenation is enabled at the given offset. + fn hyphenate(&self, offset: usize) -> bool { + self.p + .hyphenate + .or_else(|| { + let shaped = self.p.find(offset)?.text()?; + Some(TextElem::hyphenate_in(shaped.styles)) + }) + .unwrap_or(false) + } + + /// The text language at the given offset. + fn lang(&self, offset: usize) -> Option<hypher::Lang> { + let lang = self.p.lang.or_else(|| { + let shaped = self.p.find(offset)?.text()?; + Some(TextElem::lang_in(shaped.styles)) + })?; + + let bytes = lang.as_str().as_bytes().try_into().ok()?; + hypher::Lang::from_iso(bytes) + } +} + +/// Create a line which spans the given range. +fn line<'a>( + vt: &Vt, + p: &'a Preparation, + mut range: Range, + mandatory: bool, + hyphen: bool, +) -> Line<'a> { + let end = range.end; + let mut justify = p.justify && end < p.bidi.text.len() && !mandatory; + + if range.is_empty() { + return Line { + bidi: &p.bidi, + end, + trimmed: range, + first: None, + inner: &[], + last: None, + width: Abs::zero(), + justify, + dash: false, + }; + } + + // Slice out the relevant items. + let (expanded, mut inner) = p.slice(range.clone()); + let mut width = Abs::zero(); + + // Reshape the last item if it's split in half or hyphenated. + let mut last = None; + let mut dash = false; + if let Some((Item::Text(shaped), before)) = inner.split_last() { + // Compute the range we want to shape, trimming whitespace at the + // end of the line. + let base = expanded.end - shaped.text.len(); + let start = range.start.max(base); + let text = &p.bidi.text[start..range.end]; + // U+200B ZERO WIDTH SPACE is used to provide a line break opportunity, + // we want to trim it too. + let trimmed = text.trim_end().trim_end_matches('\u{200B}'); + range.end = start + trimmed.len(); + + // Deal with hyphens, dashes and justification. + let shy = trimmed.ends_with('\u{ad}'); + dash = hyphen || shy || trimmed.ends_with(['-', '–', '—']); + justify |= text.ends_with('\u{2028}'); + + // Deal with CJK punctuation at line ends. + let gb_style = is_gb_style(shaped.lang, shaped.region); + let end_cjk_punct = trimmed + .ends_with(['”', '’', ',', '。', '、', ':', ';', '》', ')', '』', '」']); + + // Usually, we don't want to shape an empty string because: + // - We don't want the height of trimmed whitespace in a different + // font to be considered for the line height. + // - Even if it's in the same font, its unnecessary. + // + // There is one exception though. When the whole line is empty, we + // need the shaped empty string to make the line the appropriate + // height. That is the case exactly if the string is empty and there + // are no other items in the line. + if hyphen || start + shaped.text.len() > range.end || end_cjk_punct { + if hyphen || start < range.end || before.is_empty() { + let mut reshaped = shaped.reshape(vt, &p.spans, start..range.end); + if hyphen || shy { + reshaped.push_hyphen(vt); + } + let punct = reshaped.glyphs.last(); + if let Some(punct) = punct { + if punct.is_cjk_left_aligned_punctuation(gb_style) { + let shrink_amount = punct.shrinkability().1; + let punct = reshaped.glyphs.to_mut().last_mut().unwrap(); + punct.shrink_right(shrink_amount); + reshaped.width -= shrink_amount.at(reshaped.size); + } + } + width += reshaped.width; + last = Some(Item::Text(reshaped)); + } + + inner = before; + } + } + + // Deal with CJK punctuation at line starts. + let text = &p.bidi.text[range.start..end]; + let start_cjk_punct = text.starts_with(['“', '‘', '《', '(', '『', '「']); + + // Reshape the start item if it's split in half. + let mut first = None; + if let Some((Item::Text(shaped), after)) = inner.split_first() { + // Compute the range we want to shape. + let base = expanded.start; + let end = range.end.min(base + shaped.text.len()); + + // Reshape if necessary. + if range.start + shaped.text.len() > end || start_cjk_punct { + if range.start < end || start_cjk_punct { + let reshaped = shaped.reshape(vt, &p.spans, range.start..end); + width += reshaped.width; + first = Some(Item::Text(reshaped)); + } + + inner = after; + } + } + + if start_cjk_punct { + let reshaped = first.as_mut().or(last.as_mut()).and_then(Item::text_mut); + if let Some(reshaped) = reshaped { + if let Some(punct) = reshaped.glyphs.first() { + if punct.is_cjk_right_aligned_punctuation() { + let shrink_amount = punct.shrinkability().0; + let punct = reshaped.glyphs.to_mut().first_mut().unwrap(); + punct.shrink_left(shrink_amount); + let amount_abs = shrink_amount.at(reshaped.size); + reshaped.width -= amount_abs; + width -= amount_abs; + } + } + } + } + + // Measure the inner items. + for item in inner { + width += item.width(); + } + + Line { + bidi: &p.bidi, + trimmed: range, + end, + first, + inner, + last, + width, + justify, + dash, + } +} + +/// Combine layouted lines into one frame per region. +fn finalize( + vt: &mut Vt, + p: &Preparation, + lines: &[Line], + region: Size, + expand: bool, +) -> SourceResult<Fragment> { + // Determine the paragraph's width: Full width of the region if we + // should expand or there's fractional spacing, fit-to-width otherwise. + let width = if !region.x.is_finite() + || (!expand && lines.iter().all(|line| line.fr().is_zero())) + { + p.hang + lines.iter().map(|line| line.width).max().unwrap_or_default() + } else { + region.x + }; + + // Stack the lines into one frame per region. + let mut frames: Vec<Frame> = lines + .iter() + .map(|line| commit(vt, p, line, width, region.y)) + .collect::<SourceResult<_>>()?; + + // Prevent orphans. + let leading = ParElem::leading_in(p.styles); + if frames.len() >= 2 && !frames[1].is_empty() { + let second = frames.remove(1); + let first = &mut frames[0]; + merge(first, second, leading); + } + + // Prevent widows. + let len = frames.len(); + if len >= 2 && !frames[len - 2].is_empty() { + let second = frames.pop().unwrap(); + let first = frames.last_mut().unwrap(); + merge(first, second, leading); + } + + Ok(Fragment::frames(frames)) +} + +/// Merge two line frames +fn merge(first: &mut Frame, second: Frame, leading: Abs) { + let offset = first.height() + leading; + let total = offset + second.height(); + first.push_frame(Point::with_y(offset), second); + first.size_mut().y = total; +} + +/// Commit to a line and build its frame. +fn commit( + vt: &mut Vt, + p: &Preparation, + line: &Line, + width: Abs, + full: Abs, +) -> SourceResult<Frame> { + let mut remaining = width - line.width - p.hang; + let mut offset = Abs::zero(); + + // Reorder the line from logical to visual order. + let (reordered, starts_rtl) = reorder(line); + if !starts_rtl { + offset += p.hang; + } + + // Handle hanging punctuation to the left. + if let Some(Item::Text(text)) = reordered.first() { + if let Some(glyph) = text.glyphs.first() { + if !text.dir.is_positive() + && TextElem::overhang_in(text.styles) + && (reordered.len() > 1 || text.glyphs.len() > 1) + { + let amount = overhang(glyph.c) * glyph.x_advance.at(text.size); + offset -= amount; + remaining += amount; + } + } + } + + // Handle hanging punctuation to the right. + if let Some(Item::Text(text)) = reordered.last() { + if let Some(glyph) = text.glyphs.last() { + if text.dir.is_positive() + && TextElem::overhang_in(text.styles) + && (reordered.len() > 1 || text.glyphs.len() > 1) + { + let amount = overhang(glyph.c) * glyph.x_advance.at(text.size); + remaining += amount; + } + } + } + + // Determine how much additional space is needed. + // The justicication_ratio is for the first step justification, + // extra_justification is for the last step. + // For more info on multi-step justification, see Procedures for Inter- + // Character Space Expansion in W3C document Chinese Layout Requirements. + let fr = line.fr(); + let mut justification_ratio = 0.0; + let mut extra_justification = Abs::zero(); + + let shrink = line.shrinkability(); + let stretch = line.stretchability(); + if remaining < Abs::zero() && shrink > Abs::zero() { + // Attempt to reduce the length of the line, using shrinkability. + justification_ratio = (remaining / shrink).max(-1.0); + remaining = (remaining + shrink).min(Abs::zero()); + } else if line.justify && fr.is_zero() { + // Attempt to increase the length of the line, using stretchability. + if stretch > Abs::zero() { + justification_ratio = (remaining / stretch).min(1.0); + remaining = (remaining - stretch).max(Abs::zero()); + } + + let justifiables = line.justifiables(); + if justifiables > 0 && remaining > Abs::zero() { + // Underfull line, distribute the extra space. + extra_justification = remaining / justifiables as f64; + remaining = Abs::zero(); + } + } + + let mut top = Abs::zero(); + let mut bottom = Abs::zero(); + + // Build the frames and determine the height and baseline. + let mut frames = vec![]; + for item in reordered { + let mut push = |offset: &mut Abs, frame: Frame| { + let width = frame.width(); + top.set_max(frame.baseline()); + bottom.set_max(frame.size().y - frame.baseline()); + frames.push((*offset, frame)); + *offset += width; + }; + + match item { + Item::Absolute(v) => { + offset += *v; + } + Item::Fractional(v, elem) => { + let amount = v.share(fr, remaining); + if let Some((elem, styles)) = elem { + let region = Size::new(amount, full); + let pod = Regions::one(region, Axes::new(true, false)); + let mut frame = elem.layout(vt, *styles, pod)?.into_frame(); + frame.translate(Point::with_y(TextElem::baseline_in(*styles))); + push(&mut offset, frame); + } else { + offset += amount; + } + } + Item::Text(shaped) => { + let frame = shaped.build(vt, justification_ratio, extra_justification); + push(&mut offset, frame); + } + Item::Frame(frame) | Item::Meta(frame) => { + push(&mut offset, frame.clone()); + } + } + } + + // Remaining space is distributed now. + if !fr.is_zero() { + remaining = Abs::zero(); + } + + let size = Size::new(width, top + bottom); + let mut output = Frame::new(size); + output.set_baseline(top); + + // Construct the line's frame. + for (offset, frame) in frames { + let x = offset + p.align.position(remaining); + let y = top - frame.baseline(); + output.push_frame(Point::new(x, y), frame); + } + + Ok(output) +} + +/// Return a line's items in visual order. +fn reorder<'a>(line: &'a Line<'a>) -> (Vec<&Item<'a>>, bool) { + let mut reordered = vec![]; + + // The bidi crate doesn't like empty lines. + if line.trimmed.is_empty() { + return (line.slice(line.trimmed.clone()).collect(), false); + } + + // Find the paragraph that contains the line. + let para = line + .bidi + .paragraphs + .iter() + .find(|para| para.range.contains(&line.trimmed.start)) + .unwrap(); + + // Compute the reordered ranges in visual order (left to right). + let (levels, runs) = line.bidi.visual_runs(para, line.trimmed.clone()); + let starts_rtl = levels.first().map_or(false, |level| level.is_rtl()); + + // Collect the reordered items. + for run in runs { + // Skip reset L1 runs because handling them would require reshaping + // again in some cases. + if line.bidi.levels[run.start] != levels[run.start] { + continue; + } + + let prev = reordered.len(); + reordered.extend(line.slice(run.clone())); + + if levels[run.start].is_rtl() { + reordered[prev..].reverse(); + } + } + + (reordered, starts_rtl) +} + +/// How much a character should hang into the end margin. +/// +/// For more discussion, see: +/// https://recoveringphysicist.com/21/ +fn overhang(c: char) -> f64 { + match c { + // Dashes. + '–' | '—' => 0.2, + '-' => 0.55, + + // Punctuation. + '.' | ',' => 0.8, + ':' | ';' => 0.3, + + // Arabic + '\u{60C}' | '\u{6D4}' => 0.4, + + _ => 0.0, + } +} diff --git a/crates/typst-library/src/layout/place.rs b/crates/typst-library/src/layout/place.rs new file mode 100644 index 00000000..6602948c --- /dev/null +++ b/crates/typst-library/src/layout/place.rs @@ -0,0 +1,103 @@ +use crate::prelude::*; + +/// Places content at an absolute position. +/// +/// Placed content will not affect the position of other content. Place is +/// always relative to its parent container and will be in the foreground of all +/// other content in the container. Page margins will be respected. +/// +/// +/// ## Example { #example } +/// ```example +/// #set page(height: 60pt) +/// Hello, world! +/// +/// #place( +/// top + right, +/// square( +/// width: 20pt, +/// stroke: 2pt + blue +/// ), +/// ) +/// ``` +/// +/// Display: Place +/// Category: layout +#[element(Layout, Behave)] +pub struct PlaceElem { + /// Relative to which position in the parent container to place the content. + /// + /// When an axis of the page is `{auto}` sized, all alignments relative to that + /// axis will be ignored, instead, the item will be placed in the origin of the + /// axis. + #[positional] + #[default(Axes::with_x(Some(GenAlign::Start)))] + pub alignment: Axes<Option<GenAlign>>, + + /// 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] + /// } + /// ``` + pub dx: Rel<Length>, + + /// The vertical displacement of the placed content. + pub dy: Rel<Length>, + + /// The content to place. + #[required] + pub body: Content, +} + +impl Layout for PlaceElem { + #[tracing::instrument(name = "PlaceElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let out_of_flow = self.out_of_flow(styles); + + // The pod is the base area of the region because for absolute + // placement we don't really care about the already used area. + let pod = { + let finite = regions.base().map(Abs::is_finite); + let expand = finite & (regions.expand | out_of_flow); + Regions::one(regions.base(), expand) + }; + + let child = self + .body() + .moved(Axes::new(self.dx(styles), self.dy(styles))) + .aligned(self.alignment(styles)); + + let mut frame = child.layout(vt, styles, pod)?.into_frame(); + + // If expansion is off, zero all sizes so that we don't take up any + // space in our parent. Otherwise, respect the expand settings. + let target = regions.expand.select(regions.size, Size::zero()); + frame.resize(target, Align::LEFT_TOP); + + Ok(Fragment::frame(frame)) + } +} + +impl PlaceElem { + /// Whether this element wants to be placed relative to its its parent's + /// base origin. Instead of relative to the parent's current flow/cursor + /// position. + pub fn out_of_flow(&self, styles: StyleChain) -> bool { + self.alignment(styles).y.is_some() + } +} + +impl Behave for PlaceElem { + fn behaviour(&self) -> Behaviour { + Behaviour::Ignorant + } +} diff --git a/crates/typst-library/src/layout/regions.rs b/crates/typst-library/src/layout/regions.rs new file mode 100644 index 00000000..6dd549b0 --- /dev/null +++ b/crates/typst-library/src/layout/regions.rs @@ -0,0 +1,144 @@ +use std::fmt::{self, Debug, Formatter}; + +use typst::geom::{Abs, Axes, Size}; + +/// A sequence of regions to layout into. +#[derive(Copy, Clone, Hash)] +pub struct Regions<'a> { + /// The remaining size of the first region. + pub size: Size, + /// 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>, + /// Whether elements should expand to fill the regions instead of shrinking + /// to fit the content. + pub expand: Axes<bool>, + /// Whether these are the root regions or direct descendants. + /// + /// True for the padded page regions and columns directly in the page, + /// false otherwise. + pub root: bool, +} + +impl Regions<'_> { + /// Create a new region sequence with exactly one region. + pub fn one(size: Size, expand: Axes<bool>) -> Self { + Self { + size, + full: size.y, + backlog: &[], + last: None, + expand, + root: false, + } + } + + /// 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, + root: false, + } + } + + /// 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, + root: false, + } + } + + /// 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.in_last() + } + + /// Whether the first region is the last usable region. + /// + /// If this is true, calling `next()` will have no effect. + pub fn in_last(&self) -> bool { + self.backlog.is_empty() && self.last.map_or(true, |height| self.size.y == height) + } + + /// The same regions, but with different `root` configuration. + pub fn with_root(self, root: bool) -> Self { + Self { root, ..self } + } + + /// 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/repeat.rs b/crates/typst-library/src/layout/repeat.rs new file mode 100644 index 00000000..646eb991 --- /dev/null +++ b/crates/typst-library/src/layout/repeat.rs @@ -0,0 +1,79 @@ +use crate::prelude::*; + +use super::AlignElem; + +/// 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 include negative space if you need the instances to overlap. +/// +/// Errors if there no bounds on the available space, as it would create +/// infinite content. +/// +/// ## Example { #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 +/// ] +/// ``` +/// +/// Display: Repeat +/// Category: layout +#[element(Layout)] +pub struct RepeatElem { + /// The content to repeat. + #[required] + pub body: Content, +} + +impl Layout for RepeatElem { + #[tracing::instrument(name = "RepeatElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let pod = Regions::one(regions.size, Axes::new(false, false)); + let piece = self.body().layout(vt, styles, pod)?.into_frame(); + let align = AlignElem::alignment_in(styles).x.resolve(styles); + + let fill = regions.size.x; + let width = piece.width(); + let count = (fill / width).floor(); + let remaining = fill % width; + let apart = remaining / (count - 1.0); + + let size = Size::new(regions.size.x, piece.height()); + + if !size.is_finite() { + bail!(self.span(), "repeat with no size restrictions"); + } + + let mut frame = Frame::new(size); + if piece.has_baseline() { + frame.set_baseline(piece.baseline()); + } + + let mut offset = Abs::zero(); + if count == 1.0 { + offset += align.position(remaining); + } + + if width > Abs::zero() { + for _ in 0..(count as usize).min(1000) { + frame.push_frame(Point::with_x(offset), piece.clone()); + offset += piece.width() + apart; + } + } + + Ok(Fragment::frame(frame)) + } +} diff --git a/crates/typst-library/src/layout/spacing.rs b/crates/typst-library/src/layout/spacing.rs new file mode 100644 index 00000000..e7dc24fb --- /dev/null +++ b/crates/typst-library/src/layout/spacing.rs @@ -0,0 +1,240 @@ +use std::cmp::Ordering; + +use crate::prelude::*; + +/// 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 } +/// ```example +/// First #h(1cm) Second \ +/// First #h(30%) Second \ +/// 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`, `med`, `thick`, `quad`. +/// +/// Display: Spacing (H) +/// Category: layout +#[element(Behave)] +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. + /// + /// ```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. + /// ``` + #[default(false)] + pub weak: bool, +} + +impl Behave for HElem { + fn behaviour(&self) -> Behaviour { + if self.amount().is_fractional() { + Behaviour::Destructive + } else if self.weak(StyleChain::default()) { + Behaviour::Weak(1) + } else { + Behaviour::Ignorant + } + } + + fn larger(&self, prev: &Content) -> bool { + let Some(prev) = prev.to::<Self>() else { return false }; + self.amount() > prev.amount() + } +} + +/// 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 } +/// ```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], +/// ) +/// ``` +/// +/// Display: Spacing (V) +/// Category: layout +#[element(Behave)] +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: + /// ``` + #[external] + pub weak: bool, + + /// The element's weakness level, see also [`Behaviour`]. + #[internal] + #[parse(args.named("weak")?.map(|v: bool| v as usize))] + pub weakness: usize, +} + +impl VElem { + /// Normal strong spacing. + pub fn strong(amount: Spacing) -> Self { + Self::new(amount).with_weakness(0) + } + + /// User-created weak spacing. + pub fn weak(amount: Spacing) -> Self { + Self::new(amount).with_weakness(1) + } + + /// Weak spacing with list attach weakness. + pub fn list_attach(amount: Spacing) -> Self { + Self::new(amount).with_weakness(2) + } + + /// Weak spacing with BlockElem::ABOVE/BELOW weakness. + pub fn block_around(amount: Spacing) -> Self { + Self::new(amount).with_weakness(3) + } + + /// Weak spacing with BlockElem::SPACING weakness. + pub fn block_spacing(amount: Spacing) -> Self { + Self::new(amount).with_weakness(4) + } +} + +impl Behave for VElem { + fn behaviour(&self) -> Behaviour { + if self.amount().is_fractional() { + Behaviour::Destructive + } else if self.weakness(StyleChain::default()) > 0 { + Behaviour::Weak(self.weakness(StyleChain::default())) + } else { + Behaviour::Ignorant + } + } + + fn larger(&self, prev: &Content) -> bool { + let Some(prev) = prev.to::<Self>() else { return false }; + self.amount() > prev.amount() + } +} + +cast! { + VElem, + v: Content => v.to::<Self>().cloned().ok_or("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) + } +} + +impl PartialOrd for Spacing { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + match (self, other) { + (Self::Rel(a), Self::Rel(b)) => a.partial_cmp(b), + (Self::Fr(a), Self::Fr(b)) => a.partial_cmp(b), + _ => None, + } + } +} + +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..97305ddf --- /dev/null +++ b/crates/typst-library/src/layout/stack.rs @@ -0,0 +1,339 @@ +use super::{AlignElem, Spacing}; +use crate::prelude::*; + +/// 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 } +/// ```example +/// #stack( +/// dir: ttb, +/// rect(width: 40pt), +/// rect(width: 120pt), +/// rect(width: 90pt), +/// ) +/// ``` +/// +/// Display: Stack +/// Category: layout +#[element(Layout)] +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. + #[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 Layout for StackElem { + #[tracing::instrument(name = "StackElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let mut layouter = StackLayouter::new(self.dir(styles), regions, styles); + + // Spacing to insert before the next block. + let spacing = self.spacing(styles); + let mut deferred = None; + + for child in self.children() { + match child { + StackChild::Spacing(kind) => { + layouter.layout_spacing(kind); + deferred = None; + } + StackChild::Block(block) => { + if let Some(kind) = deferred { + layouter.layout_spacing(kind); + } + + layouter.layout_block(vt, &block, styles)?; + deferred = spacing; + } + } + } + + Ok(layouter.finish()) + } +} + +/// A child of a stack element. +#[derive(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), +} + +/// Performs stack layout. +struct StackLayouter<'a> { + /// The stacking direction. + dir: Dir, + /// The axis of the stacking direction. + axis: Axis, + /// The regions to layout children into. + regions: Regions<'a>, + /// The inherited styles. + styles: StyleChain<'a>, + /// Whether the stack itself should expand to fill the region. + expand: Axes<bool>, + /// The initial size of the current region before we started subtracting. + initial: Size, + /// The generic size used by the frames for the current region. + used: Gen<Abs>, + /// The sum of fractions in the current region. + fr: Fr, + /// Already layouted items whose exact positions are not yet known due to + /// fractional spacing. + items: Vec<StackItem>, + /// Finished frames for previous regions. + finished: Vec<Frame>, +} + +/// A prepared item in a stack layout. +enum StackItem { + /// Absolute spacing between other items. + Absolute(Abs), + /// Fractional spacing between other items. + Fractional(Fr), + /// A frame for a layouted block. + Frame(Frame, Axes<Align>), +} + +impl<'a> StackLayouter<'a> { + /// Create a new stack layouter. + fn new(dir: Dir, mut regions: Regions<'a>, styles: StyleChain<'a>) -> Self { + let axis = dir.axis(); + let expand = regions.expand; + + // Disable expansion along the block axis for children. + regions.expand.set(axis, false); + + Self { + dir, + axis, + regions, + styles, + expand, + initial: regions.size, + used: Gen::zero(), + fr: Fr::zero(), + items: vec![], + finished: vec![], + } + } + + /// Add spacing along the spacing direction. + #[tracing::instrument(name = "StackLayouter::layout_spacing", skip_all)] + fn layout_spacing(&mut self, spacing: Spacing) { + match spacing { + Spacing::Rel(v) => { + // Resolve the spacing and limit it to the remaining space. + let resolved = v + .resolve(self.styles) + .relative_to(self.regions.base().get(self.axis)); + let remaining = self.regions.size.get_mut(self.axis); + let limited = resolved.min(*remaining); + if self.dir.axis() == Axis::Y { + *remaining -= limited; + } + self.used.main += limited; + self.items.push(StackItem::Absolute(resolved)); + } + Spacing::Fr(v) => { + self.fr += v; + self.items.push(StackItem::Fractional(v)); + } + } + } + + /// Layout an arbitrary block. + #[tracing::instrument(name = "StackLayouter::layout_block", skip_all)] + fn layout_block( + &mut self, + vt: &mut Vt, + block: &Content, + styles: StyleChain, + ) -> SourceResult<()> { + if self.regions.is_full() { + self.finish_region(); + } + + // Block-axis alignment of the `AlignElement` is respected by stacks. + let aligns = if let Some(align) = block.to::<AlignElem>() { + align.alignment(styles) + } else if let Some((_, local)) = block.to_styled() { + AlignElem::alignment_in(styles.chain(local)) + } else { + AlignElem::alignment_in(styles) + } + .resolve(styles); + + let fragment = block.layout(vt, styles, self.regions)?; + let len = fragment.len(); + for (i, frame) in fragment.into_iter().enumerate() { + // Grow our size, shrink the region and save the frame for later. + let size = frame.size(); + if self.dir.axis() == Axis::Y { + self.regions.size.y -= size.y; + } + + let gen = match self.axis { + Axis::X => Gen::new(size.y, size.x), + Axis::Y => Gen::new(size.x, size.y), + }; + + self.used.main += gen.main; + self.used.cross.set_max(gen.cross); + + self.items.push(StackItem::Frame(frame, aligns)); + + if i + 1 < len { + self.finish_region(); + } + } + + Ok(()) + } + + /// Advance to the next region. + fn finish_region(&mut self) { + // Determine the size of the stack in this region depending on whether + // the region expands. + let mut size = self + .expand + .select(self.initial, self.used.into_axes(self.axis)) + .min(self.initial); + + // Expand fully if there are fr spacings. + let full = self.initial.get(self.axis); + let remaining = full - self.used.main; + if self.fr.get() > 0.0 && full.is_finite() { + self.used.main = full; + size.set(self.axis, full); + } + + let mut output = Frame::new(size); + let mut cursor = Abs::zero(); + let mut ruler: Align = self.dir.start().into(); + + // Place all frames. + for item in self.items.drain(..) { + match item { + StackItem::Absolute(v) => cursor += v, + StackItem::Fractional(v) => cursor += v.share(self.fr, remaining), + StackItem::Frame(frame, aligns) => { + if self.dir.is_positive() { + ruler = ruler.max(aligns.get(self.axis)); + } else { + ruler = ruler.min(aligns.get(self.axis)); + } + + // Align along the main axis. + let parent = size.get(self.axis); + let child = frame.size().get(self.axis); + let main = ruler.position(parent - self.used.main) + + if self.dir.is_positive() { + cursor + } else { + self.used.main - child - cursor + }; + + // Align along the cross axis. + let other = self.axis.other(); + let cross = aligns + .get(other) + .position(size.get(other) - frame.size().get(other)); + + let pos = Gen::new(cross, main).to_point(self.axis); + cursor += child; + output.push_frame(pos, frame); + } + } + } + + // Advance to the next region. + self.regions.next(); + self.initial = self.regions.size; + self.used = Gen::zero(); + self.fr = Fr::zero(); + self.finished.push(output); + } + + /// Finish layouting and return the resulting frames. + fn finish(mut self) -> Fragment { + self.finish_region(); + Fragment::frames(self.finished) + } +} + +/// A container with a main and cross component. +#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] +struct Gen<T> { + /// The main component. + pub cross: T, + /// The cross component. + pub main: T, +} + +impl<T> Gen<T> { + /// Create a new instance from the two components. + const fn new(cross: T, main: T) -> Self { + Self { cross, main } + } + + /// Convert to the specific representation, given the current main axis. + fn into_axes(self, main: Axis) -> Axes<T> { + match main { + Axis::X => Axes::new(self.main, self.cross), + Axis::Y => Axes::new(self.cross, self.main), + } + } +} + +impl Gen<Abs> { + /// The zero value. + fn zero() -> Self { + Self { cross: Abs::zero(), main: Abs::zero() } + } + + /// Convert to a point. + fn to_point(self, main: Axis) -> Point { + self.into_axes(main).to_point() + } +} diff --git a/crates/typst-library/src/layout/table.rs b/crates/typst-library/src/layout/table.rs new file mode 100644 index 00000000..c2faedba --- /dev/null +++ b/crates/typst-library/src/layout/table.rs @@ -0,0 +1,334 @@ +use typst::eval::{CastInfo, Reflect}; + +use crate::layout::{AlignElem, GridLayouter, TrackSizings}; +use crate::meta::{Figurable, LocalName}; +use crate::prelude::*; + +/// 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. +/// Because tables are just grids with configurable cell properties, refer to +/// the [grid documentation]($func/grid) for more information on how to size the +/// table tracks. +/// +/// To give a table a caption and make it [referenceable]($func/ref), put it +/// into a [figure]($func/figure). +/// +/// ## Example { #example } +/// ```example +/// #table( +/// columns: (1fr, auto, auto), +/// inset: 10pt, +/// align: horizon, +/// [], [*Area*], [*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] +/// ) +/// ``` +/// +/// Display: Table +/// Category: layout +#[element(Layout, LocalName, Figurable)] +pub struct TableElem { + /// The column sizes. See the [grid documentation]($func/grid) for more + /// information on track sizing. + pub columns: TrackSizings, + + /// The row sizes. See the [grid documentation]($func/grid) for more + /// information on track sizing. + pub rows: TrackSizings, + + /// The gaps between rows & columns. See the [grid + /// documentation]($func/grid) for more information on gutters. + #[external] + pub gutter: TrackSizings, + + /// The gaps between columns. Takes precedence over `gutter`. See the [grid + /// documentation]($func/grid) for more information on gutters. + #[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]($func/grid) for more information on gutters. + #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))] + pub row_gutter: TrackSizings, + + /// How to fill the cells. + /// + /// This can be a color or a function that returns a color. The function is + /// passed the cells' column and row index, starting at zero. This can be + /// used to implement striped tables. + /// + /// ```example + /// #table( + /// fill: (col, _) => if calc.odd(col) { luma(240) } else { white }, + /// align: (col, row) => + /// if row == 0 { center } + /// else if col == 0 { left } + /// else { right }, + /// columns: 4, + /// [], [*Q1*], [*Q2*], [*Q3*], + /// [Revenue:], [1000 €], [2000 €], [3000 €], + /// [Expenses:], [500 €], [1000 €], [1500 €], + /// [Profit:], [500 €], [1000 €], [1500 €], + /// ) + /// ``` + 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 is passed the cells' column and row index, starting at zero. + /// If set to `{auto}`, the outer alignment is used. + /// + /// ```example + /// #table( + /// columns: 3, + /// align: (x, y) => (left, center, right).at(x), + /// [Hello], [Hello], [Hello], + /// [A], [B], [C], + /// ) + /// ``` + pub align: Celled<Smart<Axes<Option<GenAlign>>>>, + + /// How to stroke the cells. + /// + /// See the [line's documentation]($func/line.stroke) for more details. + /// Strokes can be disabled by setting this to `{none}`. + /// + /// _Note:_ Richer stroke customization for individual cells is not yet + /// implemented, but will be in the future. In the meantime, you can use + /// the third-party [tablex library](https://github.com/PgBiel/typst-tablex/). + #[resolve] + #[fold] + #[default(Some(PartialStroke::default()))] + pub stroke: Option<PartialStroke>, + + /// How much to pad the cells' content. + #[default(Abs::pt(5.0).into())] + pub inset: Rel<Length>, + + /// The contents of the table cells. + #[variadic] + pub children: Vec<Content>, +} + +impl Layout for TableElem { + #[tracing::instrument(name = "TableElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let inset = self.inset(styles); + let align = self.align(styles); + + let tracks = Axes::new(self.columns(styles).0, self.rows(styles).0); + let gutter = Axes::new(self.column_gutter(styles).0, self.row_gutter(styles).0); + let cols = tracks.x.len().max(1); + let cells: Vec<_> = self + .children() + .into_iter() + .enumerate() + .map(|(i, child)| { + let mut child = child.padded(Sides::splat(inset)); + + let x = i % cols; + let y = i / cols; + if let Smart::Custom(alignment) = align.resolve(vt, x, y)? { + child = child.styled(AlignElem::set_alignment(alignment)); + } + + Ok(child) + }) + .collect::<SourceResult<_>>()?; + + let fill = self.fill(styles); + let stroke = self.stroke(styles).map(PartialStroke::unwrap_or_default); + + // Prepare grid layout by unifying content and gutter tracks. + let layouter = GridLayouter::new( + tracks.as_deref(), + gutter.as_deref(), + &cells, + regions, + styles, + ); + + // Measure the columns and layout the grid row-by-row. + let mut layout = layouter.layout(vt)?; + + // Add lines and backgrounds. + for (frame, rows) in layout.fragment.iter_mut().zip(&layout.rows) { + if layout.cols.is_empty() || rows.is_empty() { + continue; + } + + // Render table lines. + if let Some(stroke) = &stroke { + let thickness = stroke.thickness; + let half = thickness / 2.0; + + // Render horizontal lines. + for offset in points(rows.iter().map(|piece| piece.height)) { + let target = Point::with_x(frame.width() + thickness); + let hline = Geometry::Line(target).stroked(stroke.clone()); + frame.prepend( + Point::new(-half, offset), + FrameItem::Shape(hline, self.span()), + ); + } + + // Render vertical lines. + for offset in points(layout.cols.iter().copied()) { + let target = Point::with_y(frame.height() + thickness); + let vline = Geometry::Line(target).stroked(stroke.clone()); + frame.prepend( + Point::new(offset, -half), + FrameItem::Shape(vline, self.span()), + ); + } + } + + // Render cell backgrounds. + let mut dx = Abs::zero(); + for (x, &col) in layout.cols.iter().enumerate() { + let mut dy = Abs::zero(); + for row in rows { + if let Some(fill) = fill.resolve(vt, x, row.y)? { + let pos = Point::new(dx, dy); + let size = Size::new(col, row.height); + let rect = Geometry::Rect(size).filled(fill); + frame.prepend(pos, FrameItem::Shape(rect, self.span())); + } + dy += row.height; + } + dx += col; + } + } + + Ok(layout.fragment) + } +} + +/// Turn an iterator of extents into an iterator of offsets before, in between, +/// and after the extents, e.g. [10mm, 5mm] -> [0mm, 10mm, 15mm]. +fn points(extents: impl IntoIterator<Item = Abs>) -> impl Iterator<Item = Abs> { + let mut offset = Abs::zero(); + std::iter::once(Abs::zero()) + .chain(extents.into_iter()) + .map(move |extent| { + offset += extent; + offset + }) +} + +/// 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, vt: &mut Vt, x: usize, y: usize) -> SourceResult<T> { + Ok(match self { + Self::Value(value) => value.clone(), + Self::Func(func) => func.call_vt(vt, [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 describe() -> CastInfo { + T::describe() + Array::describe() + Func::describe() + } + + 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) -> StrResult<Self> { + match value { + Value::Func(v) => Ok(Self::Func(v)), + Value::Array(array) => Ok(Self::Array( + array.into_iter().map(T::from_value).collect::<StrResult<_>>()?, + )), + v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)), + v => Err(Self::error(&v)), + } + } +} + +impl LocalName for TableElem { + fn local_name(&self, lang: Lang, _: Option<Region>) -> &'static str { + match lang { + Lang::ALBANIAN => "Tabel", + Lang::ARABIC => "جدول", + Lang::BOKMÅL => "Tabell", + Lang::CHINESE => "表", + Lang::CZECH => "Tabulka", + Lang::DANISH => "Tabel", + Lang::DUTCH => "Tabel", + Lang::FILIPINO => "Talaan", + Lang::FRENCH => "Tableau", + Lang::GERMAN => "Tabelle", + Lang::ITALIAN => "Tabella", + Lang::NYNORSK => "Tabell", + Lang::POLISH => "Tabela", + Lang::PORTUGUESE => "Tabela", + Lang::RUSSIAN => "Таблица", + Lang::SLOVENIAN => "Tabela", + Lang::SPANISH => "Tabla", + Lang::SWEDISH => "Tabell", + Lang::TURKISH => "Tablo", + Lang::UKRAINIAN => "Таблиця", + Lang::VIETNAMESE => "Bảng", + Lang::ENGLISH | _ => "Table", + } + } +} + +impl Figurable for TableElem {} diff --git a/crates/typst-library/src/layout/terms.rs b/crates/typst-library/src/layout/terms.rs new file mode 100644 index 00000000..d693f100 --- /dev/null +++ b/crates/typst-library/src/layout/terms.rs @@ -0,0 +1,166 @@ +use super::{HElem, VElem}; +use crate::layout::{BlockElem, ParElem, Spacing}; +use crate::prelude::*; + +/// 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 } +/// ```example +/// / Ligature: A merged glyph. +/// / Kerning: A spacing adjustment +/// between two adjacent letters. +/// ``` +/// +/// ## Syntax { #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. +/// +/// Display: Term List +/// Category: layout +#[element(Layout)] +#[scope( + scope.define("item", TermItem::func()); + scope +)] +pub struct TermsElem { + /// If this is `{false}`, the items are spaced apart with [term list + /// spacing]($func/terms.spacing). If it is `{true}`, they use normal + /// [leading]($func/par.leading) instead. This makes the term 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}`. + /// + /// ```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())] + 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 a wide (non-tight) term list. + /// + /// If set to `{auto}`, uses the spacing [below blocks]($func/block.below). + pub spacing: Smart<Spacing>, + + /// 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<TermItem>, +} + +impl Layout for TermsElem { + #[tracing::instrument(name = "TermsElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let separator = self.separator(styles); + let indent = self.indent(styles); + let hanging_indent = self.hanging_indent(styles); + let gutter = if self.tight(styles) { + ParElem::leading_in(styles).into() + } else { + self.spacing(styles) + .unwrap_or_else(|| BlockElem::below_in(styles).amount()) + }; + + let mut seq = vec![]; + for (i, child) in self.children().into_iter().enumerate() { + if i > 0 { + seq.push(VElem::new(gutter).with_weakness(1).pack()); + } + if !indent.is_zero() { + seq.push(HElem::new(indent.into()).pack()); + } + seq.push(child.term().strong()); + seq.push(separator.clone()); + seq.push(child.description()); + } + + Content::sequence(seq) + .styled(ParElem::set_hanging_indent(hanging_indent + indent)) + .layout(vt, styles, regions) + } +} + +/// A term list item. +/// +/// Display: Term List Item +/// Category: layout +#[element] +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.to::<Self>().cloned().ok_or("expected term item or array")?, +} diff --git a/crates/typst-library/src/layout/transform.rs b/crates/typst-library/src/layout/transform.rs new file mode 100644 index 00000000..a57a5edc --- /dev/null +++ b/crates/typst-library/src/layout/transform.rs @@ -0,0 +1,194 @@ +use typst::geom::Transform; + +use crate::prelude::*; + +/// 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 } +/// ```example +/// #rect(inset: 0pt, move( +/// dx: 6pt, dy: 6pt, +/// rect( +/// inset: 8pt, +/// fill: white, +/// stroke: black, +/// [Abra cadabra] +/// ) +/// )) +/// ``` +/// +/// Display: Move +/// Category: layout +#[element(Layout)] +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 Layout for MoveElem { + #[tracing::instrument(name = "MoveElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let pod = Regions::one(regions.base(), Axes::splat(false)); + let mut frame = self.body().layout(vt, styles, pod)?.into_frame(); + let delta = Axes::new(self.dx(styles), self.dy(styles)).resolve(styles); + let delta = delta.zip(regions.base()).map(|(d, s)| d.relative_to(s)); + frame.translate(delta.to_point()); + Ok(Fragment::frame(frame)) + } +} + +/// Rotates content without affecting layout. +/// +/// Rotates an element by a given angle. The layout will act as if the element +/// was not rotated. +/// +/// ## Example { #example } +/// ```example +/// #stack( +/// dir: ltr, +/// spacing: 1fr, +/// ..range(16) +/// .map(i => rotate(24deg * i)[X]), +/// ) +/// ``` +/// +/// Display: Rotate +/// Category: layout +#[element(Layout)] +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())) + /// ``` + #[resolve] + #[fold] + #[default(Align::CENTER_HORIZON)] + pub origin: Axes<Option<GenAlign>>, + + /// The content to rotate. + #[required] + pub body: Content, +} + +impl Layout for RotateElem { + #[tracing::instrument(name = "RotateElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let pod = Regions::one(regions.base(), Axes::splat(false)); + let mut frame = self.body().layout(vt, styles, pod)?.into_frame(); + let Axes { x, y } = + self.origin(styles).zip(frame.size()).map(|(o, s)| o.position(s)); + let ts = Transform::translate(x, y) + .pre_concat(Transform::rotate(self.angle(styles))) + .pre_concat(Transform::translate(-x, -y)); + frame.transform(ts); + Ok(Fragment::frame(frame)) + } +} + +/// Scales content without affecting layout. +/// +/// Lets you mirror content by specifying a negative scale on a single axis. +/// +/// ## Example { #example } +/// ```example +/// #set align(center) +/// #scale(x: -100%)[This is mirrored.] +/// ``` +/// +/// Display: Scale +/// Category: layout +#[element(Layout)] +pub struct ScaleElem { + /// 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(Ratio::one())] + pub x: Ratio, + + /// The vertical scaling factor. + /// + /// The body will be mirrored vertically if the parameter is negative. + #[parse(args.named("y")?.or(all))] + #[default(Ratio::one())] + pub y: Ratio, + + /// The origin of the transformation. + /// + /// ```example + /// A#box(scale(75%)[A])A \ + /// B#box(scale(75%, origin: bottom + left)[B])B + /// ``` + #[resolve] + #[fold] + #[default(Align::CENTER_HORIZON)] + pub origin: Axes<Option<GenAlign>>, + + /// The content to scale. + #[required] + pub body: Content, +} + +impl Layout for ScaleElem { + #[tracing::instrument(name = "ScaleElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let pod = Regions::one(regions.base(), Axes::splat(false)); + let mut frame = self.body().layout(vt, styles, pod)?.into_frame(); + let Axes { x, y } = + self.origin(styles).zip(frame.size()).map(|(o, s)| o.position(s)); + let transform = Transform::translate(x, y) + .pre_concat(Transform::scale(self.x(styles), self.y(styles))) + .pre_concat(Transform::translate(-x, -y)); + frame.transform(transform); + Ok(Fragment::frame(frame)) + } +} diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs new file mode 100644 index 00000000..0bd88501 --- /dev/null +++ b/crates/typst-library/src/lib.rs @@ -0,0 +1,141 @@ +//! Typst's standard library. + +#![allow(clippy::wildcard_in_or_patterns)] +#![allow(clippy::manual_range_contains)] +#![allow(clippy::comparison_chain)] + +pub mod compute; +pub mod layout; +pub mod math; +pub mod meta; +pub mod prelude; +pub mod shared; +pub mod symbols; +pub mod text; +pub mod visualize; + +use typst::diag::At; +use typst::eval::{LangItems, Library, Module, Scope}; +use typst::geom::Smart; +use typst::model::{Element, Styles}; + +use self::layout::LayoutRoot; + +/// Construct the standard library. +pub fn build() -> Library { + let math = math::module(); + let global = global(math.clone()); + Library { global, math, styles: styles(), items: items() } +} + +/// Construct the module with global definitions. +#[tracing::instrument(skip_all)] +fn global(math: Module) -> Module { + let mut global = Scope::deduplicating(); + + // Categories. + text::define(&mut global); + layout::define(&mut global); + visualize::define(&mut global); + meta::define(&mut global); + compute::define(&mut global); + symbols::define(&mut global); + global.define("math", math); + + Module::new("global").with_scope(global) +} + +/// Construct the standard style map. +fn styles() -> Styles { + Styles::new() +} + +/// Construct the standard lang item mapping. +fn items() -> LangItems { + LangItems { + layout: |world, content, styles| content.layout_root(world, styles), + em: text::TextElem::size_in, + dir: text::TextElem::dir_in, + space: || text::SpaceElem::new().pack(), + linebreak: || text::LinebreakElem::new().pack(), + text: |text| text::TextElem::new(text).pack(), + text_func: text::TextElem::func(), + text_str: |content| Some(content.to::<text::TextElem>()?.text()), + smart_quote: |double| text::SmartQuoteElem::new().with_double(double).pack(), + parbreak: || layout::ParbreakElem::new().pack(), + strong: |body| text::StrongElem::new(body).pack(), + emph: |body| text::EmphElem::new(body).pack(), + raw: |text, lang, block| { + let mut elem = text::RawElem::new(text).with_block(block); + if let Some(lang) = lang { + elem.push_lang(Some(lang)); + } + elem.pack() + }, + raw_languages: text::RawElem::languages, + link: |url| meta::LinkElem::from_url(url).pack(), + reference: |target, supplement| { + let mut elem = meta::RefElem::new(target); + if let Some(supplement) = supplement { + elem.push_supplement(Smart::Custom(Some(meta::Supplement::Content( + supplement, + )))); + } + elem.pack() + }, + bibliography_keys: meta::BibliographyElem::keys, + heading: |level, title| meta::HeadingElem::new(title).with_level(level).pack(), + heading_func: meta::HeadingElem::func(), + list_item: |body| layout::ListItem::new(body).pack(), + enum_item: |number, body| { + let mut elem = layout::EnumItem::new(body); + if let Some(number) = number { + elem.push_number(Some(number)); + } + elem.pack() + }, + term_item: |term, description| layout::TermItem::new(term, description).pack(), + equation: |body, block| math::EquationElem::new(body).with_block(block).pack(), + math_align_point: || math::AlignPointElem::new().pack(), + math_delimited: |open, body, close| math::LrElem::new(open + body + close).pack(), + math_attach: |base, t, b, tl, bl, tr, br| { + let mut elem = math::AttachElem::new(base); + if let Some(t) = t { + elem.push_t(Some(t)); + } + if let Some(b) = b { + elem.push_b(Some(b)); + } + if let Some(tl) = tl { + elem.push_tl(Some(tl)); + } + if let Some(bl) = bl { + elem.push_bl(Some(bl)); + } + if let Some(tr) = tr { + elem.push_tr(Some(tr)); + } + if let Some(br) = br { + elem.push_br(Some(br)); + } + elem.pack() + }, + math_accent: |base, accent| { + math::AccentElem::new(base, math::Accent::new(accent)).pack() + }, + math_frac: |num, denom| math::FracElem::new(num, denom).pack(), + math_root: |index, radicand| { + math::RootElem::new(radicand).with_index(index).pack() + }, + library_method: |vm, dynamic, method, args, span| { + if let Some(counter) = dynamic.downcast::<meta::Counter>().cloned() { + counter.call_method(vm, method, args, span) + } else if let Some(state) = dynamic.downcast::<meta::State>().cloned() { + state.call_method(vm, method, args, span) + } else { + Err(format!("type {} has no method `{method}`", dynamic.type_name())) + .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..d1bee198 --- /dev/null +++ b/crates/typst-library/src/math/accent.rs @@ -0,0 +1,139 @@ +use super::*; + +/// How much the accent can be shorter than the base. +const ACCENT_SHORT_FALL: Em = Em::new(0.5); + +/// Attaches an accent to a base. +/// +/// ## Example { #example } +/// ```example +/// $grave(a) = accent(a, `)$ \ +/// $arrow(a) = accent(a, arrow)$ \ +/// $tilde(a) = accent(a, \u{0303})$ +/// ``` +/// +/// Display: Accent +/// Category: math +#[element(LayoutMath)] +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` | `¯` | + /// | Breve | `breve` | `˘` | + /// | Dot | `dot` | `.` | + /// | Double dot | `dot.double` | `¨` | + /// | Triple dot | `dot.triple` | <code>⃛</code> | + /// | Quadruple dot | `dot.quad` | <code>⃜</code> | + /// | Diaeresis | `diaer` | `¨` | + /// | Circle | `circle` | `∘` | + /// | Double acute | `acute.double` | `˝` | + /// | Caron | `caron` | `ˇ` | + /// | Right arrow | `arrow`, `->` | `→` | + /// | Left arrow | `arrow.l`, `<-` | `←` | + #[required] + pub accent: Accent, +} + +impl LayoutMath for AccentElem { + #[tracing::instrument(skip(ctx))] + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + ctx.style(ctx.style.with_cramped(true)); + let base = ctx.layout_fragment(&self.base())?; + ctx.unstyle(); + + // Preserve class to preserve automatic spacing. + let base_class = base.class().unwrap_or(MathClass::Normal); + let base_attach = match &base { + MathFragment::Glyph(base) => { + attachment(ctx, base.id, base.italics_correction) + } + _ => (base.width() + base.italics_correction()) / 2.0, + }; + + // Forcing the accent to be at least as large as the base makes it too + // wide in many case. + let Accent(c) = self.accent(); + let glyph = GlyphFragment::new(ctx, c, self.span()); + let short_fall = ACCENT_SHORT_FALL.scaled(ctx); + let variant = glyph.stretch_horizontal(ctx, base.width(), short_fall); + let accent = variant.frame; + let accent_attach = match variant.id { + Some(id) => attachment(ctx, id, variant.italics_correction), + None => accent.width() / 2.0, + }; + + // Descent is negative because the accent's ink bottom is above the + // baseline. Therefore, the default gap is the accent's negated descent + // minus the accent base height. Only if the base is very small, we need + // a larger gap so that the accent doesn't move too low. + let accent_base_height = scaled!(ctx, accent_base_height); + let gap = -accent.descent() - base.height().min(accent_base_height); + let size = Size::new(base.width(), accent.height() + gap + base.height()); + let accent_pos = Point::with_x(base_attach - accent_attach); + let base_pos = Point::with_y(accent.height() + gap); + let base_ascent = base.ascent(); + let baseline = base_pos.y + base.ascent(); + + let mut frame = Frame::new(size); + frame.set_baseline(baseline); + frame.push_frame(accent_pos, accent); + frame.push_frame(base_pos, base.into_frame()); + ctx.push( + FrameFragment::new(ctx, frame) + .with_class(base_class) + .with_base_ascent(base_ascent), + ); + + Ok(()) + } +} + +/// The horizontal attachment position for the given glyph. +fn attachment(ctx: &MathContext, id: GlyphId, italics_correction: Abs) -> Abs { + ctx.table + .glyph_info + .and_then(|info| info.top_accent_attachments) + .and_then(|attachments| attachments.get(id)) + .map(|record| record.value.scaled(ctx)) + .unwrap_or_else(|| { + let advance = ctx.ttf.glyph_hor_advance(id).unwrap_or_default(); + (advance.scaled(ctx) + italics_correction) / 2.0 + }) +} + +/// An accent character. +pub struct Accent(char); + +impl Accent { + /// Normalize a character into an accent. + pub fn new(c: char) -> Self { + Self(Symbol::combining_accent(c).unwrap_or(c)) + } +} + +cast! { + Accent, + self => self.0.into_value(), + v: char => Self::new(v), + v: Content => match v.to::<TextElem>() { + Some(elem) => Value::Str(elem.text().into()).cast()?, + None => bail!("expected text"), + }, +} diff --git a/crates/typst-library/src/math/align.rs b/crates/typst-library/src/math/align.rs new file mode 100644 index 00000000..aee89a89 --- /dev/null +++ b/crates/typst-library/src/math/align.rs @@ -0,0 +1,63 @@ +use super::*; + +/// A math alignment point: `&`, `&&`. +/// +/// Display: Alignment Point +/// Category: math +#[element(LayoutMath)] +pub struct AlignPointElem {} + +impl LayoutMath for AlignPointElem { + #[tracing::instrument(skip(ctx))] + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + ctx.push(MathFragment::Align); + Ok(()) + } +} + +pub(super) struct AlignmentResult { + pub points: Vec<Abs>, + pub width: Abs, +} + +/// Determine the position of the alignment points. +pub(super) fn alignments(rows: &[MathRow]) -> AlignmentResult { + let mut widths = Vec::<Abs>::new(); + + let mut pending_width = Abs::zero(); + for row in rows { + let mut width = Abs::zero(); + let mut alignment_index = 0; + + for fragment in row.iter() { + if matches!(fragment, MathFragment::Align) { + if alignment_index < widths.len() { + widths[alignment_index].set_max(width); + } else { + widths.push(width.max(pending_width)); + } + width = Abs::zero(); + alignment_index += 1; + } else { + width += fragment.width(); + } + } + if widths.is_empty() { + pending_width.set_max(width); + } else if alignment_index < widths.len() { + widths[alignment_index].set_max(width); + } else { + widths.push(width.max(pending_width)); + } + } + + let mut points = widths; + for i in 1..points.len() { + let prev = points[i - 1]; + points[i] += prev; + } + AlignmentResult { + width: points.last().copied().unwrap_or(pending_width), + points, + } +} diff --git a/crates/typst-library/src/math/attach.rs b/crates/typst-library/src/math/attach.rs new file mode 100644 index 00000000..fedeb908 --- /dev/null +++ b/crates/typst-library/src/math/attach.rs @@ -0,0 +1,411 @@ +use super::*; + +/// A base with optional attachments. +/// +/// ## Example { #example } +/// ```example +/// // With syntax. +/// $ sum_(i=0)^n a_i = 2^(1+i) $ +/// +/// // With function call. +/// $ attach( +/// Pi, t: alpha, b: beta, +/// tl: 1, tr: 2+3, bl: 4+5, br: 6, +/// ) $ +/// ``` +/// +/// ## Syntax { #syntax } +/// This function also has dedicated syntax for attachments after the base: Use +/// the underscore (`_`) to indicate a subscript i.e. bottom attachment and the +/// hat (`^`) to indicate a superscript i.e. top attachment. +/// +/// Display: Attachment +/// Category: math +#[element(LayoutMath)] +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 LayoutMath for AttachElem { + #[tracing::instrument(skip(ctx))] + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + type GetAttachment = fn(&AttachElem, styles: StyleChain) -> Option<Content>; + let layout_attachment = |ctx: &mut MathContext, getter: GetAttachment| { + getter(self, ctx.styles()) + .map(|elem| ctx.layout_fragment(&elem)) + .transpose() + }; + + let base = ctx.layout_fragment(&self.base())?; + + ctx.style(ctx.style.for_superscript()); + let tl = layout_attachment(ctx, Self::tl)?; + let tr = layout_attachment(ctx, Self::tr)?; + let t = layout_attachment(ctx, Self::t)?; + ctx.unstyle(); + + ctx.style(ctx.style.for_subscript()); + let bl = layout_attachment(ctx, Self::bl)?; + let br = layout_attachment(ctx, Self::br)?; + let b = layout_attachment(ctx, Self::b)?; + ctx.unstyle(); + + let limits = base.limits().active(ctx); + let (t, tr) = if limits || tr.is_some() { (t, tr) } else { (None, t) }; + let (b, br) = if limits || br.is_some() { (b, br) } else { (None, b) }; + layout_attachments(ctx, base, [tl, t, tr, bl, b, br]) + } +} + +/// Forces a base to display attachments as scripts. +/// +/// ## Example { #example } +/// ```example +/// $ scripts(sum)_1^2 != sum_1^2 $ +/// ``` +/// +/// Display: Scripts +/// Category: math +#[element(LayoutMath)] +pub struct ScriptsElem { + /// The base to attach the scripts to. + #[required] + pub body: Content, +} + +impl LayoutMath for ScriptsElem { + #[tracing::instrument(skip(ctx))] + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + let mut fragment = ctx.layout_fragment(&self.body())?; + fragment.set_limits(Limits::Never); + ctx.push(fragment); + Ok(()) + } +} + +/// Forces a base to display attachments as limits. +/// +/// ## Example { #example } +/// ```example +/// $ limits(A)_1^2 != A_1^2 $ +/// ``` +/// +/// Display: Limits +/// Category: math +#[element(LayoutMath)] +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, +} + +impl LayoutMath for LimitsElem { + #[tracing::instrument(skip(ctx))] + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + let mut fragment = ctx.layout_fragment(&self.body())?; + fragment.set_limits(if self.inline(ctx.styles()) { + Limits::Always + } else { + Limits::Display + }); + ctx.push(fragment); + Ok(()) + } +} + +/// Describes in which situation a frame should use limits for attachments. +#[derive(Debug, Copy, Clone)] +pub enum Limits { + /// Always scripts. + Never, + /// Display limits only in `display` math. + Display, + /// Always limits. + Always, +} + +impl Limits { + /// The default limit configuration if the given character is the base. + pub fn for_char(c: char) -> Self { + if Self::DEFAULT_TO_LIMITS.contains(&c) { + Limits::Display + } else { + Limits::Never + } + } + + /// Whether limits should be displayed in this context + pub fn active(&self, ctx: &MathContext) -> bool { + match self { + Self::Always => true, + Self::Display => ctx.style.size == MathSize::Display, + Self::Never => false, + } + } + + /// Unicode codepoints that should show attachments as limits in display + /// mode. + #[rustfmt::skip] + const DEFAULT_TO_LIMITS: &[char] = &[ + /* ∏ */ '\u{220F}', /* ∐ */ '\u{2210}', /* ∑ */ '\u{2211}', + /* ⋀ */ '\u{22C0}', /* ⋁ */ '\u{22C1}', + /* ⋂ */ '\u{22C2}', /* ⋃ */ '\u{22C3}', + /* ⨀ */ '\u{2A00}', /* ⨁ */ '\u{2A01}', /* ⨂ */ '\u{2A02}', + /* ⨃ */ '\u{2A03}', /* ⨄ */ '\u{2A04}', + /* ⨅ */ '\u{2A05}', /* ⨆ */ '\u{2A06}', + ]; +} + +macro_rules! measure { + ($e: ident, $attr: ident) => { + $e.as_ref().map(|e| e.$attr()).unwrap_or_default() + }; +} + +/// Layout the attachments. +fn layout_attachments( + ctx: &mut MathContext, + base: MathFragment, + [tl, t, tr, bl, b, br]: [Option<MathFragment>; 6], +) -> SourceResult<()> { + let (shift_up, shift_down) = + compute_shifts_up_and_down(ctx, &base, [&tl, &tr, &bl, &br]); + + let sup_delta = Abs::zero(); + let sub_delta = -base.italics_correction(); + let (base_width, base_ascent, base_descent) = + (base.width(), base.ascent(), base.descent()); + let base_class = base.class().unwrap_or(MathClass::Normal); + + let ascent = base_ascent + .max(shift_up + measure!(tr, ascent)) + .max(shift_up + measure!(tl, ascent)) + .max(shift_up + measure!(t, height)); + + let descent = base_descent + .max(shift_down + measure!(br, descent)) + .max(shift_down + measure!(bl, descent)) + .max(shift_down + measure!(b, height)); + + let pre_sup_width = measure!(tl, width); + let pre_sub_width = measure!(bl, width); + let pre_width_dif = pre_sup_width - pre_sub_width; // Could be negative. + let pre_width_max = pre_sup_width.max(pre_sub_width); + let post_max_width = + (sup_delta + measure!(tr, width)).max(sub_delta + measure!(br, width)); + + let (center_frame, base_offset) = attach_top_and_bottom(ctx, base, t, b); + let base_pos = + Point::new(sup_delta + pre_width_max, ascent - base_ascent - base_offset); + if [&tl, &bl, &tr, &br].iter().all(|&e| e.is_none()) { + ctx.push(FrameFragment::new(ctx, center_frame).with_class(base_class)); + return Ok(()); + } + + let mut frame = Frame::new(Size::new( + pre_width_max + base_width + post_max_width + scaled!(ctx, space_after_script), + ascent + descent, + )); + frame.set_baseline(ascent); + frame.push_frame(base_pos, center_frame); + + if let Some(tl) = tl { + let pos = + Point::new(-pre_width_dif.min(Abs::zero()), ascent - shift_up - tl.ascent()); + frame.push_frame(pos, tl.into_frame()); + } + + if let Some(bl) = bl { + let pos = + Point::new(pre_width_dif.max(Abs::zero()), ascent + shift_down - bl.ascent()); + frame.push_frame(pos, bl.into_frame()); + } + + if let Some(tr) = tr { + let pos = Point::new( + sup_delta + pre_width_max + base_width, + ascent - shift_up - tr.ascent(), + ); + frame.push_frame(pos, tr.into_frame()); + } + + if let Some(br) = br { + let pos = Point::new( + sub_delta + pre_width_max + base_width, + ascent + shift_down - br.ascent(), + ); + frame.push_frame(pos, br.into_frame()); + } + + ctx.push(FrameFragment::new(ctx, frame).with_class(base_class)); + + Ok(()) +} + +fn attach_top_and_bottom( + ctx: &mut MathContext, + base: MathFragment, + t: Option<MathFragment>, + b: Option<MathFragment>, +) -> (Frame, Abs) { + let upper_gap_min = scaled!(ctx, upper_limit_gap_min); + let upper_rise_min = scaled!(ctx, upper_limit_baseline_rise_min); + let lower_gap_min = scaled!(ctx, lower_limit_gap_min); + let lower_drop_min = scaled!(ctx, lower_limit_baseline_drop_min); + + let mut base_offset = Abs::zero(); + let mut width = base.width(); + let mut height = base.height(); + + if let Some(t) = &t { + let top_gap = upper_gap_min.max(upper_rise_min - t.descent()); + width.set_max(t.width()); + height += t.height() + top_gap; + base_offset = top_gap + t.height(); + } + + if let Some(b) = &b { + let bottom_gap = lower_gap_min.max(lower_drop_min - b.ascent()); + width.set_max(b.width()); + height += b.height() + bottom_gap; + } + + let base_pos = Point::new((width - base.width()) / 2.0, base_offset); + let delta = base.italics_correction() / 2.0; + + let mut frame = Frame::new(Size::new(width, height)); + frame.set_baseline(base_pos.y + base.ascent()); + frame.push_frame(base_pos, base.into_frame()); + + if let Some(t) = t { + let top_pos = Point::with_x((width - t.width()) / 2.0 + delta); + frame.push_frame(top_pos, t.into_frame()); + } + + if let Some(b) = b { + let bottom_pos = + Point::new((width - b.width()) / 2.0 - delta, height - b.height()); + frame.push_frame(bottom_pos, b.into_frame()); + } + + (frame, base_offset) +} + +fn compute_shifts_up_and_down( + ctx: &MathContext, + base: &MathFragment, + [tl, tr, bl, br]: [&Option<MathFragment>; 4], +) -> (Abs, Abs) { + let sup_shift_up = if ctx.style.cramped { + scaled!(ctx, superscript_shift_up_cramped) + } else { + scaled!(ctx, superscript_shift_up) + }; + + let sup_bottom_min = scaled!(ctx, superscript_bottom_min); + let sup_bottom_max_with_sub = scaled!(ctx, superscript_bottom_max_with_subscript); + let sup_drop_max = scaled!(ctx, superscript_baseline_drop_max); + let gap_min = scaled!(ctx, sub_superscript_gap_min); + let sub_shift_down = scaled!(ctx, subscript_shift_down); + let sub_top_max = scaled!(ctx, subscript_top_max); + let sub_drop_min = scaled!(ctx, subscript_baseline_drop_min); + + let mut shift_up = Abs::zero(); + let mut shift_down = Abs::zero(); + let is_char_box = is_character_box(base); + + if tl.is_some() || tr.is_some() { + let ascent = match &base { + MathFragment::Frame(frame) => frame.base_ascent, + _ => base.ascent(), + }; + shift_up = shift_up + .max(sup_shift_up) + .max(if is_char_box { Abs::zero() } else { ascent - sup_drop_max }) + .max(sup_bottom_min + measure!(tl, descent)) + .max(sup_bottom_min + measure!(tr, descent)); + } + + if bl.is_some() || br.is_some() { + shift_down = shift_down + .max(sub_shift_down) + .max(if is_char_box { Abs::zero() } else { base.descent() + sub_drop_min }) + .max(measure!(bl, ascent) - sub_top_max) + .max(measure!(br, ascent) - sub_top_max); + } + + for (sup, sub) in [(tl, bl), (tr, br)] { + if let (Some(sup), Some(sub)) = (&sup, &sub) { + let sup_bottom = shift_up - sup.descent(); + let sub_top = sub.ascent() - shift_down; + let gap = sup_bottom - sub_top; + if gap >= gap_min { + continue; + } + + let increase = gap_min - gap; + let sup_only = + (sup_bottom_max_with_sub - sup_bottom).clamp(Abs::zero(), increase); + let rest = (increase - sup_only) / 2.0; + shift_up += sup_only + rest; + shift_down += rest; + } + } + + (shift_up, shift_down) +} + +/// Whether the fragment consists of a single character or atomic piece of text. +fn is_character_box(fragment: &MathFragment) -> bool { + match fragment { + MathFragment::Glyph(_) | MathFragment::Variant(_) => { + fragment.class() != Some(MathClass::Large) + } + MathFragment::Frame(fragment) => is_atomic_text_frame(&fragment.frame), + _ => false, + } +} + +/// Handles e.g. "sin", "log", "exp", "CustomOperator". +fn is_atomic_text_frame(frame: &Frame) -> bool { + // Meta information isn't visible or renderable, so we exclude it. + let mut iter = frame + .items() + .map(|(_, item)| item) + .filter(|item| !matches!(item, FrameItem::Meta(_, _))); + matches!(iter.next(), Some(FrameItem::Text(_))) && iter.next().is_none() +} diff --git a/crates/typst-library/src/math/cancel.rs b/crates/typst-library/src/math/cancel.rs new file mode 100644 index 00000000..f576a727 --- /dev/null +++ b/crates/typst-library/src/math/cancel.rs @@ -0,0 +1,187 @@ +use super::*; + +/// Displays a diagonal line over a part of an equation. +/// +/// This is commonly used to show the elimination of a term. +/// +/// ## Example { #example } +/// ```example +/// >>> #set page(width: 140pt) +/// Here, we can simplify: +/// $ (a dot b dot cancel(x)) / +/// cancel(x) $ +/// ``` +/// +/// Display: Cancel +/// Category: math +#[element(LayoutMath)] +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>, + + /// If the cancel line should be inverted (pointing 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, + + /// If 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 to rotate the cancel line. See the [line's + /// documentation]($func/line.angle) for more details. + /// + /// ```example + /// >>> #set page(width: 140pt) + /// $ cancel(Pi, rotation: #30deg) $ + /// ``` + #[default(Angle::zero())] + pub rotation: Angle, + + /// How to stroke the cancel line. See the + /// [line's documentation]($func/line.stroke) for more details. + /// + /// ```example + /// >>> #set page(width: 140pt) + /// $ cancel( + /// sum x, + /// stroke: #( + /// paint: red, + /// thickness: 1.5pt, + /// dash: "dashed", + /// ), + /// ) $ + /// ``` + #[resolve] + #[fold] + #[default(PartialStroke { + // Default stroke has 0.5pt for better visuals. + thickness: Smart::Custom(Abs::pt(0.5)), + ..Default::default() + })] + pub stroke: PartialStroke, +} + +impl LayoutMath for CancelElem { + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + let body = ctx.layout_fragment(&self.body())?; + // Use the same math class as the body, in order to preserve automatic spacing around it. + let body_class = body.class().unwrap_or(MathClass::Special); + let mut body = body.into_frame(); + + let styles = ctx.styles(); + let body_size = body.size(); + let span = self.span(); + let length = self.length(styles).resolve(styles); + + let stroke = self.stroke(styles).unwrap_or(Stroke { + paint: TextElem::fill_in(styles), + ..Default::default() + }); + + let invert = self.inverted(styles); + let cross = self.cross(styles); + let angle = self.rotation(styles); + + let invert_first_line = !cross && invert; + let first_line = draw_cancel_line( + length, + stroke.clone(), + invert_first_line, + angle, + body_size, + span, + ); + + // The origin of our line is the very middle of the element. + let center = body_size.to_point() / 2.0; + body.push_frame(center, first_line); + + if cross { + // Draw the second line. + let second_line = + draw_cancel_line(length, stroke, true, angle, body_size, span); + + body.push_frame(center, second_line); + } + + ctx.push(FrameFragment::new(ctx, body).with_class(body_class)); + + Ok(()) + } +} + +/// Draws a cancel line. +fn draw_cancel_line( + length: Rel<Abs>, + stroke: Stroke, + invert: bool, + angle: Angle, + body_size: Size, + span: Span, +) -> Frame { + // B + // /| + // diagonal / | height + // / | + // / | + // O ---- + // width + let diagonal = body_size.to_point().hypot(); + let length = length.relative_to(diagonal); + let (width, height) = (body_size.x, body_size.y); + let mid = body_size / 2.0; + + // Scale the amount needed such that the cancel line has the given 'length' + // (reference length, or 100%, is the whole diagonal). + // Scales from the center. + let scale = length.to_raw() / diagonal.to_raw(); + + // invert horizontally if 'invert' was given + let scale_x = scale * if invert { -1.0 } else { 1.0 }; + let scale_y = scale; + let scales = Axes::new(scale_x, scale_y); + + // Draw a line from bottom left to top right of the given element, where the + // origin represents the very middle of that element, that is, a line from + // (-width / 2, height / 2) with length components (width, -height) (sign is + // inverted in the y-axis). After applying the scale, the line will have the + // correct length and orientation (inverted if needed). + let start = Axes::new(-mid.x, mid.y).zip(scales).map(|(l, s)| l * s); + let delta = Axes::new(width, -height).zip(scales).map(|(l, s)| l * s); + + let mut frame = Frame::new(body_size); + frame.push( + start.to_point(), + FrameItem::Shape(Geometry::Line(delta.to_point()).stroked(stroke), span), + ); + + // Having the middle of the line at the origin is convenient here. + frame.transform(Transform::rotate(angle)); + frame +} diff --git a/crates/typst-library/src/math/ctx.rs b/crates/typst-library/src/math/ctx.rs new file mode 100644 index 00000000..a1dc6cf4 --- /dev/null +++ b/crates/typst-library/src/math/ctx.rs @@ -0,0 +1,268 @@ +use ttf_parser::math::MathValue; +use typst::font::{FontStyle, FontWeight}; +use typst::model::realize; +use unicode_segmentation::UnicodeSegmentation; + +use super::*; + +macro_rules! scaled { + ($ctx:expr, text: $text:ident, display: $display:ident $(,)?) => { + match $ctx.style.size { + MathSize::Display => scaled!($ctx, $display), + _ => scaled!($ctx, $text), + } + }; + ($ctx:expr, $name:ident) => { + $ctx.constants.$name().scaled($ctx) + }; +} + +macro_rules! percent { + ($ctx:expr, $name:ident) => { + $ctx.constants.$name() as f64 / 100.0 + }; +} + +/// The context for math layout. +pub struct MathContext<'a, 'b, 'v> { + pub vt: &'v mut Vt<'b>, + pub regions: Regions<'static>, + pub font: &'a Font, + pub ttf: &'a ttf_parser::Face<'a>, + pub table: ttf_parser::math::Table<'a>, + pub constants: ttf_parser::math::Constants<'a>, + pub ssty_table: Option<ttf_parser::gsub::AlternateSubstitution<'a>>, + pub space_width: Em, + pub fragments: Vec<MathFragment>, + pub local: Styles, + pub style: MathStyle, + pub size: Abs, + outer: StyleChain<'a>, + style_stack: Vec<(MathStyle, Abs)>, +} + +impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { + pub fn new( + vt: &'v mut Vt<'b>, + styles: StyleChain<'a>, + regions: Regions, + font: &'a Font, + block: bool, + ) -> Self { + let table = font.ttf().tables().math.unwrap(); + let constants = table.constants.unwrap(); + + let ssty_table = font + .ttf() + .tables() + .gsub + .and_then(|gsub| { + gsub.features + .find(ttf_parser::Tag::from_bytes(b"ssty")) + .and_then(|feature| feature.lookup_indices.get(0)) + .and_then(|index| gsub.lookups.get(index)) + }) + .and_then(|ssty| { + ssty.subtables.get::<ttf_parser::gsub::SubstitutionSubtable>(0) + }) + .and_then(|ssty| match ssty { + ttf_parser::gsub::SubstitutionSubtable::Alternate(alt_glyphs) => { + Some(alt_glyphs) + } + _ => None, + }); + + let size = TextElem::size_in(styles); + let ttf = font.ttf(); + let space_width = ttf + .glyph_index(' ') + .and_then(|id| ttf.glyph_hor_advance(id)) + .map(|advance| font.to_em(advance)) + .unwrap_or(THICK); + + let variant = variant(styles); + Self { + vt, + regions: Regions::one(regions.base(), Axes::splat(false)), + font, + ttf: font.ttf(), + table, + constants, + ssty_table, + space_width, + fragments: vec![], + local: Styles::new(), + style: MathStyle { + variant: MathVariant::Serif, + size: if block { MathSize::Display } else { MathSize::Text }, + cramped: false, + bold: variant.weight >= FontWeight::BOLD, + italic: match variant.style { + FontStyle::Normal => Smart::Auto, + FontStyle::Italic | FontStyle::Oblique => Smart::Custom(true), + }, + }, + size, + outer: styles, + style_stack: vec![], + } + } + + pub fn push(&mut self, fragment: impl Into<MathFragment>) { + self.fragments.push(fragment.into()); + } + + pub fn extend(&mut self, fragments: Vec<MathFragment>) { + self.fragments.extend(fragments); + } + + pub fn layout_fragment( + &mut self, + elem: &dyn LayoutMath, + ) -> SourceResult<MathFragment> { + let row = self.layout_fragments(elem)?; + Ok(MathRow::new(row).into_fragment(self)) + } + + pub fn layout_fragments( + &mut self, + elem: &dyn LayoutMath, + ) -> SourceResult<Vec<MathFragment>> { + let prev = std::mem::take(&mut self.fragments); + elem.layout_math(self)?; + Ok(std::mem::replace(&mut self.fragments, prev)) + } + + pub fn layout_row(&mut self, elem: &dyn LayoutMath) -> SourceResult<MathRow> { + let fragments = self.layout_fragments(elem)?; + Ok(MathRow::new(fragments)) + } + + pub fn layout_frame(&mut self, elem: &dyn LayoutMath) -> SourceResult<Frame> { + Ok(self.layout_fragment(elem)?.into_frame()) + } + + pub fn layout_content(&mut self, content: &Content) -> SourceResult<Frame> { + Ok(content + .layout(self.vt, self.outer.chain(&self.local), self.regions)? + .into_frame()) + } + + pub fn layout_text(&mut self, elem: &TextElem) -> SourceResult<MathFragment> { + let text = elem.text(); + let span = elem.span(); + let mut chars = text.chars(); + let fragment = if let Some(mut glyph) = chars + .next() + .filter(|_| chars.next().is_none()) + .map(|c| self.style.styled_char(c)) + .and_then(|c| GlyphFragment::try_new(self, c, span)) + { + // A single letter that is available in the math font. + match self.style.size { + MathSize::Display => { + if glyph.class == Some(MathClass::Large) { + let height = scaled!(self, display_operator_min_height); + glyph.stretch_vertical(self, height, Abs::zero()).into() + } else { + glyph.into() + } + } + MathSize::Script => { + glyph.make_scriptsize(self); + glyph.into() + } + MathSize::ScriptScript => { + glyph.make_scriptscriptsize(self); + glyph.into() + } + _ => glyph.into(), + } + } else if text.chars().all(|c| c.is_ascii_digit()) { + // Numbers aren't that difficult. + let mut fragments = vec![]; + for c in text.chars() { + let c = self.style.styled_char(c); + fragments.push(GlyphFragment::new(self, c, span).into()); + } + let frame = MathRow::new(fragments).into_frame(self); + FrameFragment::new(self, frame).into() + } else { + // Anything else is handled by Typst's standard text layout. + let spaced = text.graphemes(true).nth(1).is_some(); + let mut style = self.style; + if self.style.italic == Smart::Auto { + style = style.with_italic(false); + } + let text: EcoString = text.chars().map(|c| style.styled_char(c)).collect(); + let frame = self.layout_content(&TextElem::packed(text).spanned(span))?; + FrameFragment::new(self, frame) + .with_class(MathClass::Alphabetic) + .with_spaced(spaced) + .into() + }; + Ok(fragment) + } + + pub fn styles(&self) -> StyleChain { + self.outer.chain(&self.local) + } + + pub fn realize(&mut self, content: &Content) -> SourceResult<Option<Content>> { + realize(self.vt, content, self.outer.chain(&self.local)) + } + + pub fn style(&mut self, style: MathStyle) { + self.style_stack.push((self.style, self.size)); + let base_size = TextElem::size_in(self.styles()) / self.style.size.factor(self); + self.size = base_size * style.size.factor(self); + self.local.set(TextElem::set_size(TextSize(self.size.into()))); + self.local + .set(TextElem::set_style(if style.italic == Smart::Custom(true) { + FontStyle::Italic + } else { + FontStyle::Normal + })); + self.local.set(TextElem::set_weight(if style.bold { + FontWeight::BOLD + } else { + FontWeight::REGULAR + })); + self.style = style; + } + + pub fn unstyle(&mut self) { + (self.style, self.size) = self.style_stack.pop().unwrap(); + self.local.unset(); + self.local.unset(); + self.local.unset(); + } +} + +pub(super) trait Scaled { + fn scaled(self, ctx: &MathContext) -> Abs; +} + +impl Scaled for i16 { + fn scaled(self, ctx: &MathContext) -> Abs { + ctx.font.to_em(self).scaled(ctx) + } +} + +impl Scaled for u16 { + fn scaled(self, ctx: &MathContext) -> Abs { + ctx.font.to_em(self).scaled(ctx) + } +} + +impl Scaled for Em { + fn scaled(self, ctx: &MathContext) -> Abs { + self.at(ctx.size) + } +} + +impl Scaled for MathValue<'_> { + fn scaled(self, ctx: &MathContext) -> Abs { + self.value.scaled(ctx) + } +} diff --git a/crates/typst-library/src/math/delimited.rs b/crates/typst-library/src/math/delimited.rs new file mode 100644 index 00000000..99cd6c33 --- /dev/null +++ b/crates/typst-library/src/math/delimited.rs @@ -0,0 +1,200 @@ +use super::*; + +/// How much less high scaled delimiters can be than what they wrap. +pub(super) const DELIM_SHORT_FALL: Em = Em::new(0.1); + +/// Scales delimiters. +/// +/// While matched delimiters scale by default, this can be used to scale +/// unmatched delimiters and to control the delimiter scaling more precisely. +/// +/// ## Example { #example } +/// ```example +/// $ lr(]a, b/2]) $ +/// $ lr(]sum_(x=1)^n] x, size: #50%) $ +/// ``` +/// +/// Display: Left/Right +/// Category: math +#[element(LayoutMath)] +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 body = Content::empty(); + for (i, arg) in args.all::<Content>()?.into_iter().enumerate() { + if i > 0 { + body += TextElem::packed(','); + } + body += arg; + } + body + )] + pub body: Content, +} + +impl LayoutMath for LrElem { + #[tracing::instrument(skip(ctx))] + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + let mut body = self.body(); + if let Some(elem) = body.to::<LrElem>() { + if elem.size(ctx.styles()).is_auto() { + body = elem.body(); + } + } + + let mut fragments = ctx.layout_fragments(&body)?; + let axis = scaled!(ctx, axis_height); + let max_extent = fragments + .iter() + .map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis)) + .max() + .unwrap_or_default(); + + let height = self + .size(ctx.styles()) + .unwrap_or(Rel::one()) + .resolve(ctx.styles()) + .relative_to(2.0 * max_extent); + + match fragments.as_mut_slice() { + [one] => scale(ctx, one, height, None), + [first, .., last] => { + scale(ctx, first, height, Some(MathClass::Opening)); + scale(ctx, last, height, Some(MathClass::Closing)); + } + _ => {} + } + + ctx.extend(fragments); + + Ok(()) + } +} + +/// Scale a math fragment to a height. +fn scale( + ctx: &mut MathContext, + fragment: &mut MathFragment, + height: Abs, + apply: Option<MathClass>, +) { + if matches!( + fragment.class(), + Some(MathClass::Opening | MathClass::Closing | MathClass::Fence) + ) { + let glyph = match fragment { + MathFragment::Glyph(glyph) => glyph.clone(), + MathFragment::Variant(variant) => { + GlyphFragment::new(ctx, variant.c, variant.span) + } + _ => return, + }; + + let short_fall = DELIM_SHORT_FALL.scaled(ctx); + *fragment = + MathFragment::Variant(glyph.stretch_vertical(ctx, height, short_fall)); + + if let Some(class) = apply { + fragment.set_class(class); + } + } +} + +/// Floors an expression. +/// +/// ## Example { #example } +/// ```example +/// $ floor(x/2) $ +/// ``` +/// +/// Display: Floor +/// Category: math +#[func] +pub fn floor( + /// The expression to floor. + body: Content, +) -> Content { + delimited(body, '⌊', '⌋') +} + +/// Ceils an expression. +/// +/// ## Example { #example } +/// ```example +/// $ ceil(x/2) $ +/// ``` +/// +/// Display: Ceil +/// Category: math +#[func] +pub fn ceil( + /// The expression to ceil. + body: Content, +) -> Content { + delimited(body, '⌈', '⌉') +} + +/// Rounds an expression. +/// +/// ## Example { #example } +/// ```example +/// $ round(x/2) $ +/// ``` +/// +/// Display: Round +/// Category: math +#[func] +pub fn round( + /// The expression to round. + body: Content, +) -> Content { + delimited(body, '⌊', '⌉') +} + +/// Takes the absolute value of an expression. +/// +/// ## Example { #example } +/// ```example +/// $ abs(x/2) $ +/// ``` +/// +/// +/// Display: Abs +/// Category: math +#[func] +pub fn abs( + /// The expression to take the absolute value of. + body: Content, +) -> Content { + delimited(body, '|', '|') +} + +/// Takes the norm of an expression. +/// +/// ## Example { #example } +/// ```example +/// $ norm(x/2) $ +/// ``` +/// +/// Display: Norm +/// Category: math +#[func] +pub fn norm( + /// The expression to take the norm of. + body: Content, +) -> Content { + delimited(body, '‖', '‖') +} + +fn delimited(body: Content, left: char, right: char) -> Content { + LrElem::new(Content::sequence([ + TextElem::packed(left), + body, + TextElem::packed(right), + ])) + .pack() +} diff --git a/crates/typst-library/src/math/frac.rs b/crates/typst-library/src/math/frac.rs new file mode 100644 index 00000000..0e1f78cc --- /dev/null +++ b/crates/typst-library/src/math/frac.rs @@ -0,0 +1,148 @@ +use super::*; + +const FRAC_AROUND: Em = Em::new(0.1); + +/// A mathematical fraction. +/// +/// ## Example { #example } +/// ```example +/// $ 1/2 < (x+1)/2 $ +/// $ ((x+1)) / 2 = frac(a, b) $ +/// ``` +/// +/// ## Syntax { #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. +/// +/// Display: Fraction +/// Category: math +#[element(LayoutMath)] +pub struct FracElem { + /// The fraction's numerator. + #[required] + pub num: Content, + + /// The fraction's denominator. + #[required] + pub denom: Content, +} + +impl LayoutMath for FracElem { + #[tracing::instrument(skip(ctx))] + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + layout(ctx, &self.num(), &self.denom(), false, self.span()) + } +} + +/// A binomial expression. +/// +/// ## Example { #example } +/// ```example +/// $ binom(n, k) $ +/// ``` +/// +/// Display: Binomial +/// Category: math +#[element(LayoutMath)] +pub struct BinomElem { + /// The binomial's upper index. + #[required] + pub upper: Content, + + /// The binomial's lower index. + #[required] + pub lower: Content, +} + +impl LayoutMath for BinomElem { + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + layout(ctx, &self.upper(), &self.lower(), true, self.span()) + } +} + +/// Layout a fraction or binomial. +fn layout( + ctx: &mut MathContext, + num: &Content, + denom: &Content, + binom: bool, + span: Span, +) -> SourceResult<()> { + let short_fall = DELIM_SHORT_FALL.scaled(ctx); + let axis = scaled!(ctx, axis_height); + let thickness = scaled!(ctx, fraction_rule_thickness); + let shift_up = scaled!( + ctx, + text: fraction_numerator_shift_up, + display: fraction_numerator_display_style_shift_up, + ); + let shift_down = scaled!( + ctx, + text: fraction_denominator_shift_down, + display: fraction_denominator_display_style_shift_down, + ); + let num_min = scaled!( + ctx, + text: fraction_numerator_gap_min, + display: fraction_num_display_style_gap_min, + ); + let denom_min = scaled!( + ctx, + text: fraction_denominator_gap_min, + display: fraction_denom_display_style_gap_min, + ); + + ctx.style(ctx.style.for_numerator()); + let num = ctx.layout_frame(num)?; + ctx.unstyle(); + + ctx.style(ctx.style.for_denominator()); + let denom = ctx.layout_frame(denom)?; + ctx.unstyle(); + + let around = FRAC_AROUND.scaled(ctx); + let num_gap = (shift_up - axis - num.descent()).max(num_min + thickness / 2.0); + let denom_gap = (shift_down + axis - denom.ascent()).max(denom_min + thickness / 2.0); + + let line_width = num.width().max(denom.width()); + let width = line_width + 2.0 * around; + let height = num.height() + num_gap + thickness + denom_gap + denom.height(); + let size = Size::new(width, height); + let num_pos = Point::with_x((width - num.width()) / 2.0); + let line_pos = + Point::new((width - line_width) / 2.0, num.height() + num_gap + thickness / 2.0); + let denom_pos = Point::new((width - denom.width()) / 2.0, height - denom.height()); + let baseline = line_pos.y + axis; + + let mut frame = Frame::new(size); + frame.set_baseline(baseline); + frame.push_frame(num_pos, num); + frame.push_frame(denom_pos, denom); + + if binom { + ctx.push( + GlyphFragment::new(ctx, '(', span).stretch_vertical(ctx, height, short_fall), + ); + ctx.push(FrameFragment::new(ctx, frame)); + ctx.push( + GlyphFragment::new(ctx, ')', span).stretch_vertical(ctx, height, short_fall), + ); + } else { + frame.push( + line_pos, + FrameItem::Shape( + Geometry::Line(Point::with_x(line_width)).stroked(Stroke { + paint: TextElem::fill_in(ctx.styles()), + thickness, + ..Stroke::default() + }), + span, + ), + ); + ctx.push(FrameFragment::new(ctx, frame)); + } + + Ok(()) +} diff --git a/crates/typst-library/src/math/fragment.rs b/crates/typst-library/src/math/fragment.rs new file mode 100644 index 00000000..139ce07b --- /dev/null +++ b/crates/typst-library/src/math/fragment.rs @@ -0,0 +1,414 @@ +use super::*; +use ttf_parser::gsub::AlternateSet; + +#[derive(Debug, Clone)] +pub enum MathFragment { + Glyph(GlyphFragment), + Variant(VariantFragment), + Frame(FrameFragment), + Spacing(Abs), + Space(Abs), + Linebreak, + Align, +} + +impl MathFragment { + pub fn size(&self) -> Size { + Size::new(self.width(), self.height()) + } + + pub fn width(&self) -> Abs { + match self { + Self::Glyph(glyph) => glyph.width, + Self::Variant(variant) => variant.frame.width(), + Self::Frame(fragment) => fragment.frame.width(), + Self::Spacing(amount) => *amount, + Self::Space(amount) => *amount, + _ => Abs::zero(), + } + } + + pub fn height(&self) -> Abs { + match self { + Self::Glyph(glyph) => glyph.height(), + Self::Variant(variant) => variant.frame.height(), + Self::Frame(fragment) => fragment.frame.height(), + _ => Abs::zero(), + } + } + + pub fn ascent(&self) -> Abs { + match self { + Self::Glyph(glyph) => glyph.ascent, + Self::Variant(variant) => variant.frame.ascent(), + Self::Frame(fragment) => fragment.frame.baseline(), + _ => Abs::zero(), + } + } + + pub fn descent(&self) -> Abs { + match self { + Self::Glyph(glyph) => glyph.descent, + Self::Variant(variant) => variant.frame.descent(), + Self::Frame(fragment) => fragment.frame.descent(), + _ => Abs::zero(), + } + } + + pub fn class(&self) -> Option<MathClass> { + match self { + Self::Glyph(glyph) => glyph.class, + Self::Variant(variant) => variant.class, + Self::Frame(fragment) => Some(fragment.class), + _ => None, + } + } + + pub fn style(&self) -> Option<MathStyle> { + match self { + Self::Glyph(glyph) => Some(glyph.style), + Self::Variant(variant) => Some(variant.style), + Self::Frame(fragment) => Some(fragment.style), + _ => None, + } + } + + pub fn font_size(&self) -> Option<Abs> { + match self { + Self::Glyph(glyph) => Some(glyph.font_size), + Self::Variant(variant) => Some(variant.font_size), + Self::Frame(fragment) => Some(fragment.font_size), + _ => None, + } + } + + pub fn set_class(&mut self, class: MathClass) { + match self { + Self::Glyph(glyph) => glyph.class = Some(class), + Self::Variant(variant) => variant.class = Some(class), + Self::Frame(fragment) => fragment.class = class, + _ => {} + } + } + + pub fn set_limits(&mut self, limits: Limits) { + match self { + Self::Glyph(glyph) => glyph.limits = limits, + Self::Variant(variant) => variant.limits = limits, + Self::Frame(fragment) => fragment.limits = limits, + _ => {} + } + } + + pub fn is_spaced(&self) -> bool { + match self { + MathFragment::Frame(frame) => frame.spaced, + _ => self.class() == Some(MathClass::Fence), + } + } + + pub fn italics_correction(&self) -> Abs { + match self { + Self::Glyph(glyph) => glyph.italics_correction, + Self::Variant(variant) => variant.italics_correction, + _ => Abs::zero(), + } + } + + pub fn into_frame(self) -> Frame { + match self { + Self::Glyph(glyph) => glyph.into_frame(), + Self::Variant(variant) => variant.frame, + Self::Frame(fragment) => fragment.frame, + _ => Frame::new(self.size()), + } + } + + pub fn limits(&self) -> Limits { + match self { + MathFragment::Glyph(glyph) => glyph.limits, + MathFragment::Variant(variant) => variant.limits, + MathFragment::Frame(fragment) => fragment.limits, + _ => Limits::Never, + } + } +} + +impl From<GlyphFragment> for MathFragment { + fn from(glyph: GlyphFragment) -> Self { + Self::Glyph(glyph) + } +} + +impl From<VariantFragment> for MathFragment { + fn from(variant: VariantFragment) -> Self { + Self::Variant(variant) + } +} + +impl From<FrameFragment> for MathFragment { + fn from(fragment: FrameFragment) -> Self { + Self::Frame(fragment) + } +} + +#[derive(Clone)] +pub struct GlyphFragment { + pub id: GlyphId, + pub c: char, + pub font: Font, + pub lang: Lang, + pub fill: Paint, + pub width: Abs, + pub ascent: Abs, + pub descent: Abs, + pub italics_correction: Abs, + pub style: MathStyle, + pub font_size: Abs, + pub class: Option<MathClass>, + pub span: Span, + pub meta: Vec<Meta>, + pub limits: Limits, +} + +impl GlyphFragment { + pub fn new(ctx: &MathContext, c: char, span: Span) -> Self { + let id = ctx.ttf.glyph_index(c).unwrap_or_default(); + Self::with_id(ctx, c, id, span) + } + + pub fn try_new(ctx: &MathContext, c: char, span: Span) -> Option<Self> { + let c = ctx.style.styled_char(c); + let id = ctx.ttf.glyph_index(c)?; + Some(Self::with_id(ctx, c, id, span)) + } + + pub fn with_id(ctx: &MathContext, c: char, id: GlyphId, span: Span) -> Self { + let class = match c { + ':' => Some(MathClass::Relation), + _ => unicode_math_class::class(c), + }; + let mut fragment = Self { + id, + c, + font: ctx.font.clone(), + lang: TextElem::lang_in(ctx.styles()), + fill: TextElem::fill_in(ctx.styles()), + style: ctx.style, + font_size: ctx.size, + width: Abs::zero(), + ascent: Abs::zero(), + descent: Abs::zero(), + limits: Limits::for_char(c), + italics_correction: Abs::zero(), + class, + span, + meta: MetaElem::data_in(ctx.styles()), + }; + fragment.set_id(ctx, id); + fragment + } + + /// Sets element id and boxes in appropriate way without changing other + /// styles. This is used to replace the glyph with a stretch variant. + pub fn set_id(&mut self, ctx: &MathContext, id: GlyphId) { + let advance = ctx.ttf.glyph_hor_advance(id).unwrap_or_default(); + let italics = italics_correction(ctx, id).unwrap_or_default(); + let bbox = ctx.ttf.glyph_bounding_box(id).unwrap_or(Rect { + x_min: 0, + y_min: 0, + x_max: 0, + y_max: 0, + }); + + let mut width = advance.scaled(ctx); + if !is_extended_shape(ctx, id) { + width += italics; + } + + self.id = id; + self.width = width; + self.ascent = bbox.y_max.scaled(ctx); + self.descent = -bbox.y_min.scaled(ctx); + self.italics_correction = italics; + } + + pub fn height(&self) -> Abs { + self.ascent + self.descent + } + + pub fn into_variant(self) -> VariantFragment { + VariantFragment { + c: self.c, + id: Some(self.id), + style: self.style, + font_size: self.font_size, + italics_correction: self.italics_correction, + class: self.class, + span: self.span, + limits: self.limits, + frame: self.into_frame(), + } + } + + pub fn into_frame(self) -> Frame { + let item = TextItem { + font: self.font.clone(), + size: self.font_size, + fill: self.fill, + lang: self.lang, + text: self.c.into(), + glyphs: vec![Glyph { + id: self.id.0, + x_advance: Em::from_length(self.width, self.font_size), + x_offset: Em::zero(), + range: 0..self.c.len_utf8() as u16, + span: (self.span, 0), + }], + }; + let size = Size::new(self.width, self.ascent + self.descent); + let mut frame = Frame::new(size); + frame.set_baseline(self.ascent); + frame.push(Point::with_y(self.ascent), FrameItem::Text(item)); + frame.meta_iter(self.meta); + frame + } + + pub fn make_scriptsize(&mut self, ctx: &MathContext) { + let alt_id = + script_alternatives(ctx, self.id).and_then(|alts| alts.alternates.get(0)); + + if let Some(alt_id) = alt_id { + self.set_id(ctx, alt_id); + } + } + + pub fn make_scriptscriptsize(&mut self, ctx: &MathContext) { + let alts = script_alternatives(ctx, self.id); + let alt_id = alts + .and_then(|alts| alts.alternates.get(1).or_else(|| alts.alternates.get(0))); + + if let Some(alt_id) = alt_id { + self.set_id(ctx, alt_id); + } + } +} + +impl Debug for GlyphFragment { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "GlyphFragment({:?})", self.c) + } +} + +#[derive(Clone)] +pub struct VariantFragment { + pub c: char, + pub id: Option<GlyphId>, + pub italics_correction: Abs, + pub frame: Frame, + pub style: MathStyle, + pub font_size: Abs, + pub class: Option<MathClass>, + pub span: Span, + pub limits: Limits, +} + +impl Debug for VariantFragment { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "VariantFragment({:?})", self.c) + } +} + +#[derive(Debug, Clone)] +pub struct FrameFragment { + pub frame: Frame, + pub style: MathStyle, + pub font_size: Abs, + pub class: MathClass, + pub limits: Limits, + pub spaced: bool, + pub base_ascent: Abs, +} + +impl FrameFragment { + pub fn new(ctx: &MathContext, mut frame: Frame) -> Self { + let base_ascent = frame.ascent(); + frame.meta(ctx.styles(), false); + Self { + frame, + font_size: ctx.size, + style: ctx.style, + class: MathClass::Normal, + limits: Limits::Never, + spaced: false, + base_ascent, + } + } + + pub fn with_class(self, class: MathClass) -> Self { + Self { class, ..self } + } + + pub fn with_limits(self, limits: Limits) -> Self { + Self { limits, ..self } + } + + pub fn with_spaced(self, spaced: bool) -> Self { + Self { spaced, ..self } + } + + pub fn with_base_ascent(self, base_ascent: Abs) -> Self { + Self { base_ascent, ..self } + } +} + +/// Look up the italics correction for a glyph. +fn italics_correction(ctx: &MathContext, id: GlyphId) -> Option<Abs> { + Some(ctx.table.glyph_info?.italic_corrections?.get(id)?.scaled(ctx)) +} + +/// Look up the script/scriptscript alternates for a glyph +fn script_alternatives<'a>( + ctx: &MathContext<'a, '_, '_>, + id: GlyphId, +) -> Option<AlternateSet<'a>> { + ctx.ssty_table.and_then(|ssty| { + ssty.coverage.get(id).and_then(|index| ssty.alternate_sets.get(index)) + }) +} + +/// Look up the italics correction for a glyph. +fn is_extended_shape(ctx: &MathContext, id: GlyphId) -> bool { + ctx.table + .glyph_info + .and_then(|info| info.extended_shapes) + .and_then(|info| info.get(id)) + .is_some() +} + +/// Look up a kerning value at a specific corner and height. +/// +/// This can be integrated once we've found a font that actually provides this +/// data. +#[allow(unused)] +fn kern_at_height( + ctx: &MathContext, + id: GlyphId, + corner: Corner, + height: Abs, +) -> Option<Abs> { + let kerns = ctx.table.glyph_info?.kern_infos?.get(id)?; + let kern = match corner { + Corner::TopLeft => kerns.top_left, + Corner::TopRight => kerns.top_right, + Corner::BottomRight => kerns.bottom_right, + Corner::BottomLeft => kerns.bottom_left, + }?; + + let mut i = 0; + while i < kern.count() && height > kern.height(i)?.scaled(ctx) { + i += 1; + } + + Some(kern.kern(i)?.scaled(ctx)) +} diff --git a/crates/typst-library/src/math/matrix.rs b/crates/typst-library/src/math/matrix.rs new file mode 100644 index 00000000..aaccc332 --- /dev/null +++ b/crates/typst-library/src/math/matrix.rs @@ -0,0 +1,313 @@ +use super::*; + +const ROW_GAP: Em = Em::new(0.5); +const COL_GAP: Em = Em::new(0.5); +const VERTICAL_PADDING: Ratio = Ratio::new(0.1); + +/// A column vector. +/// +/// Content in the vector's elements can be aligned with the `&` symbol. +/// +/// ## Example { #example } +/// ```example +/// $ vec(a, b, c) dot vec(1, 2, 3) +/// = a + 2b + 3c $ +/// ``` +/// +/// Display: Vector +/// Category: math +#[element(LayoutMath)] +pub struct VecElem { + /// The delimiter to use. + /// + /// ```example + /// #set math.vec(delim: "[") + /// $ vec(1, 2) $ + /// ``` + #[default(Some(Delimiter::Paren))] + pub delim: Option<Delimiter>, + + /// The elements of the vector. + #[variadic] + pub children: Vec<Content>, +} + +impl LayoutMath for VecElem { + #[tracing::instrument(skip(ctx))] + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + let delim = self.delim(ctx.styles()); + let frame = layout_vec_body(ctx, &self.children(), Align::Center)?; + layout_delimiters( + ctx, + frame, + delim.map(Delimiter::open), + delim.map(Delimiter::close), + self.span(), + ) + } +} + +/// 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 that are in the same row can be aligned with the `&` symbol. +/// +/// ## Example { #example } +/// ```example +/// $ mat( +/// 1, 2, ..., 10; +/// 2, 2, ..., 10; +/// dots.v, dots.v, dots.down, dots.v; +/// 10, 10, ..., 10; +/// ) $ +/// ``` +/// +/// Display: Matrix +/// Category: math +#[element(LayoutMath)] +pub struct MatElem { + /// The delimiter to use. + /// + /// ```example + /// #set math.mat(delim: "[") + /// $ mat(1, 2; 3, 4) $ + /// ``` + #[default(Some(Delimiter::Paren))] + pub delim: Option<Delimiter>, + + /// 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>>, +} + +impl LayoutMath for MatElem { + #[tracing::instrument(skip(ctx))] + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + let delim = self.delim(ctx.styles()); + let frame = layout_mat_body(ctx, &self.rows())?; + layout_delimiters( + ctx, + frame, + delim.map(Delimiter::open), + delim.map(Delimiter::close), + self.span(), + ) + } +} + +/// A case distinction. +/// +/// Content across different branches can be aligned with the `&` symbol. +/// +/// ## Example { #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", +/// ) $ +/// ``` +/// +/// Display: Cases +/// Category: math +#[element(LayoutMath)] +pub struct CasesElem { + /// The delimiter to use. + /// + /// ```example + /// #set math.cases(delim: "[") + /// $ x = cases(1, 2) $ + /// ``` + #[default(Delimiter::Brace)] + pub delim: Delimiter, + + /// The branches of the case distinction. + #[variadic] + pub children: Vec<Content>, +} + +impl LayoutMath for CasesElem { + #[tracing::instrument(skip(ctx))] + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + let delim = self.delim(ctx.styles()); + let frame = layout_vec_body(ctx, &self.children(), Align::Left)?; + layout_delimiters(ctx, frame, Some(delim.open()), None, self.span()) + } +} + +/// A vector / matrix delimiter. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum Delimiter { + /// Delimit with parentheses. + #[string("(")] + Paren, + /// Delimit with brackets. + #[string("[")] + Bracket, + /// Delimit with curly braces. + #[string("{")] + Brace, + /// Delimit with vertical bars. + #[string("|")] + Bar, + /// Delimit with double vertical bars. + #[string("||")] + DoubleBar, +} + +impl Delimiter { + /// The delimiter's opening character. + fn open(self) -> char { + match self { + Self::Paren => '(', + Self::Bracket => '[', + Self::Brace => '{', + Self::Bar => '|', + Self::DoubleBar => '‖', + } + } + + /// The delimiter's closing character. + fn close(self) -> char { + match self { + Self::Paren => ')', + Self::Bracket => ']', + Self::Brace => '}', + Self::Bar => '|', + Self::DoubleBar => '‖', + } + } +} + +/// Layout the inner contents of a vector. +fn layout_vec_body( + ctx: &mut MathContext, + column: &[Content], + align: Align, +) -> SourceResult<Frame> { + let gap = ROW_GAP.scaled(ctx); + ctx.style(ctx.style.for_denominator()); + let mut flat = vec![]; + for child in column { + flat.push(ctx.layout_row(child)?); + } + ctx.unstyle(); + Ok(stack(ctx, flat, align, gap, 0)) +} + +/// Layout the inner contents of a matrix. +fn layout_mat_body(ctx: &mut MathContext, rows: &[Vec<Content>]) -> SourceResult<Frame> { + let row_gap = ROW_GAP.scaled(ctx); + let col_gap = COL_GAP.scaled(ctx); + + let ncols = rows.first().map_or(0, |row| row.len()); + let nrows = rows.len(); + if ncols == 0 || nrows == 0 { + return Ok(Frame::new(Size::zero())); + } + + let mut heights = vec![(Abs::zero(), Abs::zero()); nrows]; + + ctx.style(ctx.style.for_denominator()); + let mut cols = vec![vec![]; ncols]; + for (row, (ascent, descent)) in rows.iter().zip(&mut heights) { + for (cell, col) in row.iter().zip(&mut cols) { + let cell = ctx.layout_row(cell)?; + ascent.set_max(cell.ascent()); + descent.set_max(cell.descent()); + col.push(cell); + } + } + ctx.unstyle(); + + let mut frame = Frame::new(Size::new( + Abs::zero(), + heights.iter().map(|&(a, b)| a + b).sum::<Abs>() + row_gap * (nrows - 1) as f64, + )); + let mut x = Abs::zero(); + for col in cols { + let AlignmentResult { points, width: rcol } = alignments(&col); + let mut y = Abs::zero(); + for (cell, &(ascent, descent)) in col.into_iter().zip(&heights) { + let cell = cell.into_aligned_frame(ctx, &points, Align::Center); + let pos = Point::new( + if points.is_empty() { x + (rcol - cell.width()) / 2.0 } else { x }, + y + ascent - cell.ascent(), + ); + frame.push_frame(pos, cell); + y += ascent + descent + row_gap; + } + x += rcol + col_gap; + } + frame.size_mut().x = x - col_gap; + + Ok(frame) +} + +/// Layout the outer wrapper around a vector's or matrices' body. +fn layout_delimiters( + ctx: &mut MathContext, + mut frame: Frame, + left: Option<char>, + right: Option<char>, + span: Span, +) -> SourceResult<()> { + let axis = scaled!(ctx, axis_height); + let short_fall = DELIM_SHORT_FALL.scaled(ctx); + let height = frame.height(); + let target = height + VERTICAL_PADDING.of(height); + frame.set_baseline(height / 2.0 + axis); + + if let Some(left) = left { + ctx.push( + GlyphFragment::new(ctx, left, span).stretch_vertical(ctx, target, short_fall), + ); + } + + ctx.push(FrameFragment::new(ctx, frame)); + + if let Some(right) = right { + ctx.push( + GlyphFragment::new(ctx, right, span) + .stretch_vertical(ctx, target, short_fall), + ); + } + + Ok(()) +} diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs new file mode 100644 index 00000000..0429265f --- /dev/null +++ b/crates/typst-library/src/math/mod.rs @@ -0,0 +1,480 @@ +//! Mathematical formulas. + +#[macro_use] +mod ctx; +mod accent; +mod align; +mod attach; +mod cancel; +mod delimited; +mod frac; +mod fragment; +mod matrix; +mod op; +mod root; +mod row; +mod spacing; +mod stretch; +mod style; +mod underover; + +pub use self::accent::*; +pub use self::align::*; +pub use self::attach::*; +pub use self::cancel::*; +pub use self::delimited::*; +pub use self::frac::*; +pub use self::matrix::*; +pub use self::op::*; +pub use self::root::*; +pub use self::style::*; +pub use self::underover::*; + +use ttf_parser::{GlyphId, Rect}; +use typst::eval::{Module, Scope}; +use typst::font::{Font, FontWeight}; +use typst::model::Guard; +use typst::util::option_eq; +use unicode_math_class::MathClass; + +use self::ctx::*; +use self::fragment::*; +use self::row::*; +use self::spacing::*; +use crate::layout::{HElem, ParElem, Spacing}; +use crate::meta::Supplement; +use crate::meta::{ + Count, Counter, CounterUpdate, LocalName, Numbering, Outlinable, Refable, +}; +use crate::prelude::*; +use crate::text::{ + families, variant, FontFamily, FontList, LinebreakElem, SpaceElem, TextElem, TextSize, +}; + +/// Create a module with all math definitions. +pub fn module() -> Module { + let mut math = Scope::deduplicating(); + math.define("equation", EquationElem::func()); + math.define("text", TextElem::func()); + + // Grouping. + math.define("lr", LrElem::func()); + math.define("abs", abs_func()); + math.define("norm", norm_func()); + math.define("floor", floor_func()); + math.define("ceil", ceil_func()); + math.define("round", round_func()); + + // Attachments and accents. + math.define("attach", AttachElem::func()); + math.define("scripts", ScriptsElem::func()); + math.define("limits", LimitsElem::func()); + math.define("accent", AccentElem::func()); + math.define("underline", UnderlineElem::func()); + math.define("overline", OverlineElem::func()); + math.define("underbrace", UnderbraceElem::func()); + math.define("overbrace", OverbraceElem::func()); + math.define("underbracket", UnderbracketElem::func()); + math.define("overbracket", OverbracketElem::func()); + math.define("cancel", CancelElem::func()); + + // Fractions and matrix-likes. + math.define("frac", FracElem::func()); + math.define("binom", BinomElem::func()); + math.define("vec", VecElem::func()); + math.define("mat", MatElem::func()); + math.define("cases", CasesElem::func()); + + // Roots. + math.define("sqrt", sqrt_func()); + math.define("root", RootElem::func()); + + // Styles. + math.define("upright", upright_func()); + math.define("bold", bold_func()); + math.define("italic", italic_func()); + math.define("serif", serif_func()); + math.define("sans", sans_func()); + math.define("cal", cal_func()); + math.define("frak", frak_func()); + math.define("mono", mono_func()); + math.define("bb", bb_func()); + + math.define("display", display_func()); + math.define("inline", inline_func()); + math.define("script", script_func()); + math.define("sscript", sscript_func()); + + // Text operators. + math.define("op", OpElem::func()); + op::define(&mut math); + + // Spacings. + spacing::define(&mut math); + + // Symbols. + for (name, symbol) in crate::symbols::SYM { + math.define(*name, symbol.clone()); + } + + Module::new("math").with_scope(math) +} + +/// A mathematical equation. +/// +/// Can be displayed inline with text or as a separate block. +/// +/// ## Example { #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 $ +/// ``` +/// +/// ## Syntax { #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). +/// +/// Display: Equation +/// Category: math +#[element( + Locatable, Synthesize, Show, Finalize, Layout, LayoutMath, Count, LocalName, Refable, + Outlinable +)] +pub struct EquationElem { + /// Whether the equation is displayed as a separate block. + #[default(false)] + pub block: bool, + + /// How to [number]($func/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) $ + /// ``` + pub numbering: Option<Numbering>, + + /// 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, +} + +impl Synthesize for EquationElem { + fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> { + // Resolve the supplement. + 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(vt, [self.clone()])?, + }; + + self.push_block(self.block(styles)); + self.push_numbering(self.numbering(styles)); + self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement)))); + + Ok(()) + } +} + +impl Show for EquationElem { + #[tracing::instrument(name = "EquationElem::show", skip_all)] + fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + let mut realized = self.clone().pack().guarded(Guard::Base(Self::func())); + if self.block(styles) { + realized = realized.aligned(Axes::with_x(Some(Align::Center.into()))) + } + Ok(realized) + } +} + +impl Finalize for EquationElem { + fn finalize(&self, realized: Content, _: StyleChain) -> Content { + realized + .styled(TextElem::set_weight(FontWeight::from_number(450))) + .styled(TextElem::set_font(FontList(vec![FontFamily::new( + "New Computer Modern Math", + )]))) + } +} + +impl Layout for EquationElem { + #[tracing::instrument(name = "EquationElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + const NUMBER_GUTTER: Em = Em::new(0.5); + + let block = self.block(styles); + + // Find a math font. + let variant = variant(styles); + let world = vt.world; + let Some(font) = families(styles) + .find_map(|family| { + let id = world.book().select(family.as_str(), variant)?; + let font = world.font(id)?; + let _ = font.ttf().tables().math?.constants?; + Some(font) + }) + else { + bail!(self.span(), "current font does not support math"); + }; + + let mut ctx = MathContext::new(vt, styles, regions, &font, block); + let mut frame = ctx.layout_frame(self)?; + + if block { + if let Some(numbering) = self.numbering(styles) { + let pod = Regions::one(regions.base(), Axes::splat(false)); + let counter = Counter::of(Self::func()) + .display(Some(numbering), false) + .layout(vt, styles, pod)? + .into_frame(); + + let width = if regions.size.x.is_finite() { + regions.size.x + } else { + frame.width() + + 2.0 * (counter.width() + NUMBER_GUTTER.resolve(styles)) + }; + + let height = frame.height().max(counter.height()); + frame.resize(Size::new(width, height), Align::CENTER_HORIZON); + + let x = if TextElem::dir_in(styles).is_positive() { + frame.width() - counter.width() + } else { + Abs::zero() + }; + let y = (frame.height() - counter.height()) / 2.0; + + frame.push_frame(Point::new(x, y), counter) + } + } else { + let slack = ParElem::leading_in(styles) * 0.7; + let top_edge = TextElem::top_edge_in(styles).resolve(styles, font.metrics()); + let bottom_edge = + -TextElem::bottom_edge_in(styles).resolve(styles, font.metrics()); + + let ascent = top_edge.max(frame.ascent() - slack); + let descent = bottom_edge.max(frame.descent() - slack); + frame.translate(Point::with_y(ascent - frame.baseline())); + frame.size_mut().y = ascent + descent; + } + + // Apply metadata. + frame.meta(styles, false); + + Ok(Fragment::frame(frame)) + } +} + +impl Count for EquationElem { + fn update(&self) -> Option<CounterUpdate> { + (self.block(StyleChain::default()) + && self.numbering(StyleChain::default()).is_some()) + .then(|| CounterUpdate::Step(NonZeroUsize::ONE)) + } +} + +impl LocalName for EquationElem { + fn local_name(&self, lang: Lang, region: Option<Region>) -> &'static str { + match lang { + Lang::ALBANIAN => "Ekuacion", + Lang::ARABIC => "معادلة", + Lang::BOKMÅL => "Ligning", + Lang::CHINESE if option_eq(region, "TW") => "方程式", + Lang::CHINESE => "等式", + Lang::CZECH => "Rovnice", + Lang::DANISH => "Ligning", + Lang::DUTCH => "Vergelijking", + Lang::FILIPINO => "Ekwasyon", + Lang::FRENCH => "Équation", + Lang::GERMAN => "Gleichung", + Lang::ITALIAN => "Equazione", + Lang::NYNORSK => "Likning", + Lang::POLISH => "Równanie", + Lang::PORTUGUESE => "Equação", + Lang::RUSSIAN => "Уравнение", + Lang::SLOVENIAN => "Enačba", + Lang::SPANISH => "Ecuación", + Lang::SWEDISH => "Ekvation", + Lang::TURKISH => "Denklem", + Lang::UKRAINIAN => "Рівняння", + Lang::VIETNAMESE => "Phương trình", + Lang::ENGLISH | _ => "Equation", + } + } +} + +impl Refable for 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(Self::func()) + } + + fn numbering(&self) -> Option<Numbering> { + self.numbering(StyleChain::default()) + } +} + +impl Outlinable for EquationElem { + fn outline(&self, vt: &mut Vt) -> SourceResult<Option<Content>> { + let Some(numbering) = self.numbering(StyleChain::default()) 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() + .at(vt, self.0.location().unwrap())? + .display(vt, &numbering)?; + + Ok(Some(supplement + numbers)) + } +} + +pub trait LayoutMath { + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()>; +} + +impl LayoutMath for EquationElem { + #[tracing::instrument(skip(ctx))] + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + self.body().layout_math(ctx) + } +} + +impl LayoutMath for Content { + #[tracing::instrument(skip(ctx))] + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + // Directly layout the body of nested equations instead of handling it + // like a normal equation so that things like this work: + // ``` + // #let my = $pi$ + // $ my r^2 $ + // ``` + if let Some(elem) = self.to::<EquationElem>() { + return elem.layout_math(ctx); + } + + if let Some(realized) = ctx.realize(self)? { + return realized.layout_math(ctx); + } + + if let Some(children) = self.to_sequence() { + for child in children { + child.layout_math(ctx)?; + } + return Ok(()); + } + + if let Some((elem, styles)) = self.to_styled() { + if TextElem::font_in(ctx.styles().chain(styles)) + != TextElem::font_in(ctx.styles()) + { + let frame = ctx.layout_content(self)?; + ctx.push(FrameFragment::new(ctx, frame).with_spaced(true)); + return Ok(()); + } + + let prev_map = std::mem::replace(&mut ctx.local, styles.clone()); + let prev_size = ctx.size; + ctx.local.apply(prev_map.clone()); + ctx.size = TextElem::size_in(ctx.styles()); + elem.layout_math(ctx)?; + ctx.size = prev_size; + ctx.local = prev_map; + return Ok(()); + } + + if self.is::<SpaceElem>() { + ctx.push(MathFragment::Space(ctx.space_width.scaled(ctx))); + return Ok(()); + } + + if self.is::<LinebreakElem>() { + ctx.push(MathFragment::Linebreak); + return Ok(()); + } + + if let Some(elem) = self.to::<HElem>() { + if let Spacing::Rel(rel) = elem.amount() { + if rel.rel.is_zero() { + ctx.push(MathFragment::Spacing(rel.abs.resolve(ctx.styles()))); + } + } + return Ok(()); + } + + if let Some(elem) = self.to::<TextElem>() { + let fragment = ctx.layout_text(elem)?; + ctx.push(fragment); + return Ok(()); + } + + if let Some(elem) = self.with::<dyn LayoutMath>() { + return elem.layout_math(ctx); + } + + let mut frame = ctx.layout_content(self)?; + if !frame.has_baseline() { + let axis = scaled!(ctx, axis_height); + frame.set_baseline(frame.height() / 2.0 + axis); + } + ctx.push(FrameFragment::new(ctx, frame).with_spaced(true)); + + Ok(()) + } +} diff --git a/crates/typst-library/src/math/op.rs b/crates/typst-library/src/math/op.rs new file mode 100644 index 00000000..8ad74c49 --- /dev/null +++ b/crates/typst-library/src/math/op.rs @@ -0,0 +1,113 @@ +use typst::eval::Scope; + +use super::*; + +/// A text operator in an equation. +/// +/// ## Example { #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`, `ctg`, `coth`, `csc`, `deg`, `det`, `dim`, +/// `exp`, `gcd`, `hom`, `mod`, `inf`, `ker`, `lg`, `lim`, `ln`, `log`, +/// `max`, `min`, `Pr`, `sec`, `sin`, `sinc`, `sinh`, `sup`, `tan`, `tg`, +/// `tanh`, `liminf`, and `limsup`. +/// +/// Display: Text Operator +/// Category: math +#[element(LayoutMath)] +pub struct OpElem { + /// The operator's text. + #[required] + pub text: EcoString, + + /// Whether the operator should show attachments as limits in display mode. + #[default(false)] + pub limits: bool, +} + +impl LayoutMath for OpElem { + #[tracing::instrument(skip(ctx))] + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + let fragment = + ctx.layout_text(&TextElem::new(self.text()).spanned(self.span()))?; + ctx.push( + FrameFragment::new(ctx, fragment.into_frame()) + .with_class(MathClass::Large) + .with_limits(if self.limits(ctx.styles()) { + Limits::Display + } else { + Limits::Never + }), + ); + Ok(()) + } +} + +macro_rules! ops { + ($($name:ident $(: $value:literal)? $(($tts:tt))?),* $(,)?) => { + pub(super) fn define(math: &mut Scope) { + $(math.define( + stringify!($name), + OpElem::new(ops!(@name $name $(: $value)?).into()) + .with_limits(ops!(@limit $($tts)*)) + .pack() + );)* + + let dif = |d| { + HElem::new(THIN.into()).pack() + + MathStyleElem::new(TextElem::packed(d)).with_italic(Some(false)).pack() + }; + 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, + ctg, + coth, + csc, + deg, + det (limits), + dim, + exp, + gcd (limits), + hom, + mod, + inf (limits), + ker, + lg, + lim (limits), + ln, + log, + max (limits), + min (limits), + Pr (limits), + sec, + sin, + sinc, + sinh, + sup (limits), + tan, + tg, + tanh, + liminf: "lim inf" (limits), + limsup: "lim sup" (limits), +} diff --git a/crates/typst-library/src/math/root.rs b/crates/typst-library/src/math/root.rs new file mode 100644 index 00000000..d1c5f46a --- /dev/null +++ b/crates/typst-library/src/math/root.rs @@ -0,0 +1,156 @@ +use super::*; + +/// A square root. +/// +/// ## Example { #example } +/// ```example +/// $ sqrt(x^2) = x = sqrt(x)^2 $ +/// ``` +/// +/// Display: Square Root +/// Category: math +#[func] +pub fn sqrt( + /// The expression to take the square root of. + radicand: Content, +) -> Content { + RootElem::new(radicand).pack() +} + +/// A general root. +/// +/// ## Example { #example } +/// ```example +/// $ root(3, x) $ +/// ``` +/// +/// Display: Root +/// Category: math +#[element(LayoutMath)] +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, +} + +impl LayoutMath for RootElem { + #[tracing::instrument(skip(ctx))] + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + layout(ctx, self.index(ctx.styles()).as_ref(), &self.radicand(), self.span()) + } +} + +/// Layout a root. +/// +/// https://www.w3.org/TR/mathml-core/#radicals-msqrt-mroot +fn layout( + ctx: &mut MathContext, + mut index: Option<&Content>, + radicand: &Content, + span: Span, +) -> SourceResult<()> { + let gap = scaled!( + ctx, + text: radical_vertical_gap, + display: radical_display_style_vertical_gap, + ); + let thickness = scaled!(ctx, radical_rule_thickness); + let extra_ascender = scaled!(ctx, radical_extra_ascender); + let kern_before = scaled!(ctx, radical_kern_before_degree); + let kern_after = scaled!(ctx, radical_kern_after_degree); + let raise_factor = percent!(ctx, radical_degree_bottom_raise_percent); + + // Layout radicand. + ctx.style(ctx.style.with_cramped(true)); + let radicand = ctx.layout_frame(radicand)?; + ctx.unstyle(); + + // Layout root symbol. + let target = radicand.height() + thickness + gap; + let sqrt = precomposed(ctx, index, target) + .map(|frame| { + index = None; + frame + }) + .unwrap_or_else(|| { + let glyph = GlyphFragment::new(ctx, '√', span); + glyph.stretch_vertical(ctx, target, Abs::zero()).frame + }); + + // Layout the index. + // Script-script style looks too small, we use Script style instead. + ctx.style(ctx.style.with_size(MathSize::Script)); + let index = index.map(|elem| ctx.layout_frame(elem)).transpose()?; + ctx.unstyle(); + + let gap = gap.max((sqrt.height() - radicand.height() - thickness) / 2.0); + let descent = radicand.descent() + gap; + let inner_ascent = extra_ascender + thickness + gap + radicand.ascent(); + + let mut sqrt_offset = Abs::zero(); + let mut shift_up = Abs::zero(); + let mut ascent = inner_ascent; + + if let Some(index) = &index { + sqrt_offset = kern_before + index.width() + kern_after; + shift_up = raise_factor * sqrt.height() - descent + index.descent(); + ascent.set_max(shift_up + index.ascent()); + } + + let radicant_offset = sqrt_offset + sqrt.width(); + let width = radicant_offset + radicand.width(); + let size = Size::new(width, ascent + descent); + + let sqrt_pos = Point::new(sqrt_offset, ascent - inner_ascent); + let line_pos = Point::new(radicant_offset, ascent - inner_ascent + thickness / 2.0); + let radicand_pos = Point::new(radicant_offset, ascent - radicand.ascent()); + + let mut frame = Frame::new(size); + frame.set_baseline(ascent); + + if let Some(index) = index { + let index_pos = Point::new(kern_before, ascent - shift_up - index.ascent()); + frame.push_frame(index_pos, index); + } + + frame.push_frame(sqrt_pos, sqrt); + frame.push( + line_pos, + FrameItem::Shape( + Geometry::Line(Point::with_x(radicand.width())).stroked(Stroke { + paint: TextElem::fill_in(ctx.styles()), + thickness, + ..Stroke::default() + }), + span, + ), + ); + + frame.push_frame(radicand_pos, radicand); + ctx.push(FrameFragment::new(ctx, frame)); + + Ok(()) +} + +/// Select a precomposed radical, if the font has it. +fn precomposed(ctx: &MathContext, index: Option<&Content>, target: Abs) -> Option<Frame> { + let elem = index?.to::<TextElem>()?; + let c = match elem.text().as_str() { + "3" => '∛', + "4" => '∜', + _ => return None, + }; + + ctx.ttf.glyph_index(c)?; + let glyph = GlyphFragment::new(ctx, c, elem.span()); + let variant = glyph.stretch_vertical(ctx, target, Abs::zero()).frame; + if variant.height() < target { + return None; + } + + Some(variant) +} diff --git a/crates/typst-library/src/math/row.rs b/crates/typst-library/src/math/row.rs new file mode 100644 index 00000000..687f82b8 --- /dev/null +++ b/crates/typst-library/src/math/row.rs @@ -0,0 +1,258 @@ +use std::iter::once; + +use crate::layout::AlignElem; + +use super::*; + +pub const TIGHT_LEADING: Em = Em::new(0.25); + +#[derive(Debug, Default, Clone)] +pub struct MathRow(Vec<MathFragment>); + +impl MathRow { + pub fn new(fragments: Vec<MathFragment>) -> Self { + let iter = fragments.into_iter().peekable(); + let mut last: Option<usize> = None; + let mut space: Option<MathFragment> = None; + let mut resolved: Vec<MathFragment> = vec![]; + + for mut fragment in iter { + match fragment { + // Keep space only if supported by spaced fragments. + MathFragment::Space(_) => { + if last.is_some() { + space = Some(fragment); + } + continue; + } + + // Explicit spacing disables automatic spacing. + MathFragment::Spacing(_) => { + last = None; + space = None; + resolved.push(fragment); + continue; + } + + // Alignment points are resolved later. + MathFragment::Align => { + resolved.push(fragment); + continue; + } + + // New line, new things. + MathFragment::Linebreak => { + resolved.push(fragment); + space = None; + last = None; + continue; + } + + _ => {} + } + + // Convert variable operators into binary operators if something + // precedes them and they are not preceded by a operator or comparator. + if fragment.class() == Some(MathClass::Vary) + && matches!( + last.and_then(|i| resolved[i].class()), + Some( + MathClass::Normal + | MathClass::Alphabetic + | MathClass::Closing + | MathClass::Fence + ) + ) + { + fragment.set_class(MathClass::Binary); + } + + // Insert spacing between the last and this item. + if let Some(i) = last { + if let Some(s) = spacing(&resolved[i], space.take(), &fragment) { + resolved.insert(i + 1, s); + } + } + + last = Some(resolved.len()); + resolved.push(fragment); + } + + Self(resolved) + } + + pub fn iter(&self) -> std::slice::Iter<'_, MathFragment> { + self.0.iter() + } + + /// Extract the sublines of the row. + /// + /// It is very unintuitive, but in current state of things, a `MathRow` can + /// contain several actual rows. That function deconstructs it to "single" + /// rows. Hopefully this is only a temporary hack. + pub fn rows(&self) -> Vec<Self> { + self.0 + .split(|frag| matches!(frag, MathFragment::Linebreak)) + .map(|slice| Self(slice.to_vec())) + .collect() + } + + pub fn ascent(&self) -> Abs { + self.iter().map(MathFragment::ascent).max().unwrap_or_default() + } + + pub fn descent(&self) -> Abs { + self.iter().map(MathFragment::descent).max().unwrap_or_default() + } + + pub fn class(&self) -> MathClass { + // Predict the class of the output of 'into_fragment' + if self.0.len() == 1 { + self.0 + .first() + .and_then(|fragment| fragment.class()) + .unwrap_or(MathClass::Special) + } else { + // FrameFragment::new() (inside 'into_fragment' in this branch) defaults + // to MathClass::Normal for its class. + MathClass::Normal + } + } + + pub fn into_frame(self, ctx: &MathContext) -> Frame { + let styles = ctx.styles(); + let align = AlignElem::alignment_in(styles).x.resolve(styles); + self.into_aligned_frame(ctx, &[], align) + } + + pub fn into_fragment(self, ctx: &MathContext) -> MathFragment { + if self.0.len() == 1 { + self.0.into_iter().next().unwrap() + } else { + FrameFragment::new(ctx, self.into_frame(ctx)).into() + } + } + + pub fn into_aligned_frame( + self, + ctx: &MathContext, + points: &[Abs], + align: Align, + ) -> Frame { + if self.iter().any(|frag| matches!(frag, MathFragment::Linebreak)) { + let leading = if ctx.style.size >= MathSize::Text { + ParElem::leading_in(ctx.styles()) + } else { + TIGHT_LEADING.scaled(ctx) + }; + + let mut rows: Vec<_> = self.rows(); + + if matches!(rows.last(), Some(row) if row.0.is_empty()) { + rows.pop(); + } + + let AlignmentResult { points, width } = alignments(&rows); + let mut frame = Frame::new(Size::zero()); + + for (i, row) in rows.into_iter().enumerate() { + let sub = row.into_line_frame(&points, align); + let size = frame.size_mut(); + if i > 0 { + size.y += leading; + } + + let mut pos = Point::with_y(size.y); + if points.is_empty() { + pos.x = align.position(width - sub.width()); + } + size.y += sub.height(); + size.x.set_max(sub.width()); + frame.push_frame(pos, sub); + } + frame + } else { + self.into_line_frame(points, align) + } + } + + fn into_line_frame(self, points: &[Abs], align: Align) -> Frame { + let ascent = self.ascent(); + let mut frame = Frame::new(Size::new(Abs::zero(), ascent + self.descent())); + frame.set_baseline(ascent); + + let mut next_x = { + let mut widths = Vec::new(); + if !points.is_empty() && align != Align::Left { + let mut width = Abs::zero(); + for fragment in self.iter() { + if matches!(fragment, MathFragment::Align) { + widths.push(width); + width = Abs::zero(); + } else { + width += fragment.width(); + } + } + widths.push(width); + } + let widths = widths; + + let mut prev_points = once(Abs::zero()).chain(points.iter().copied()); + let mut point_widths = points.iter().copied().zip(widths); + let mut alternator = LeftRightAlternator::Right; + move || match align { + Align::Left => prev_points.next(), + Align::Right => point_widths.next().map(|(point, width)| point - width), + _ => point_widths + .next() + .zip(prev_points.next()) + .zip(alternator.next()) + .map(|(((point, width), prev_point), alternator)| match alternator { + LeftRightAlternator::Left => prev_point, + LeftRightAlternator::Right => point - width, + }), + } + }; + let mut x = next_x().unwrap_or_default(); + + for fragment in self.0.into_iter() { + if matches!(fragment, MathFragment::Align) { + x = next_x().unwrap_or(x); + continue; + } + + let y = ascent - fragment.ascent(); + let pos = Point::new(x, y); + x += fragment.width(); + frame.push_frame(pos, fragment.into_frame()); + } + + frame.size_mut().x = x; + frame + } +} + +impl<T: Into<MathFragment>> From<T> for MathRow { + fn from(fragment: T) -> Self { + Self(vec![fragment.into()]) + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum LeftRightAlternator { + Left, + Right, +} + +impl Iterator for LeftRightAlternator { + type Item = LeftRightAlternator; + + fn next(&mut self) -> Option<Self::Item> { + let r = Some(*self); + match self { + Self::Left => *self = Self::Right, + Self::Right => *self = Self::Left, + } + r + } +} diff --git a/crates/typst-library/src/math/spacing.rs b/crates/typst-library/src/math/spacing.rs new file mode 100644 index 00000000..848aca78 --- /dev/null +++ b/crates/typst-library/src/math/spacing.rs @@ -0,0 +1,60 @@ +use super::*; + +pub(super) const THIN: Em = Em::new(1.0 / 6.0); +pub(super) const MEDIUM: Em = Em::new(2.0 / 9.0); +pub(super) const THICK: Em = Em::new(5.0 / 18.0); +pub(super) const QUAD: Em = Em::new(1.0); + +/// Hook up all spacings. +pub(super) fn define(math: &mut Scope) { + 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()); +} + +/// Create the spacing between two fragments in a given style. +pub(super) fn spacing( + l: &MathFragment, + space: Option<MathFragment>, + r: &MathFragment, +) -> Option<MathFragment> { + use MathClass::*; + + let class = |f: &MathFragment| f.class().unwrap_or(Special); + let resolve = |v: Em, f: &MathFragment| { + Some(MathFragment::Spacing(f.font_size().map_or(Abs::zero(), |size| v.at(size)))) + }; + let script = + |f: &MathFragment| f.style().map_or(false, |s| s.size <= MathSize::Script); + + match (class(l), class(r)) { + // No spacing before punctuation; thin spacing after punctuation, unless + // in script size. + (_, Punctuation) => None, + (Punctuation, _) if !script(l) => resolve(THIN, l), + + // No spacing after opening delimiters and before closing delimiters. + (Opening, _) | (_, Closing) => None, + + // Thick spacing around relations, unless followed by a another relation + // or in script size. + (Relation, Relation) => None, + (Relation, _) if !script(l) => resolve(THICK, l), + (_, Relation) if !script(r) => resolve(THICK, r), + + // Medium spacing around binary operators, unless in script size. + (Binary, _) if !script(l) => resolve(MEDIUM, l), + (_, Binary) if !script(r) => resolve(MEDIUM, r), + + // Thin spacing around large operators, unless next to a delimiter. + (Large, Opening | Fence) | (Closing | Fence, Large) => None, + (Large, _) => resolve(THIN, l), + (_, Large) => resolve(THIN, r), + + // Spacing around spaced frames. + _ if (l.is_spaced() || r.is_spaced()) => space, + + _ => None, + } +} diff --git a/crates/typst-library/src/math/stretch.rs b/crates/typst-library/src/math/stretch.rs new file mode 100644 index 00000000..910f7a81 --- /dev/null +++ b/crates/typst-library/src/math/stretch.rs @@ -0,0 +1,199 @@ +use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart}; +use ttf_parser::LazyArray16; + +use super::*; + +/// Maximum number of times extenders can be repeated. +const MAX_REPEATS: usize = 1024; + +impl GlyphFragment { + /// Try to stretch a glyph to a desired height. + pub fn stretch_vertical( + self, + ctx: &MathContext, + height: Abs, + short_fall: Abs, + ) -> VariantFragment { + stretch_glyph(ctx, self, height, short_fall, false) + } + + /// Try to stretch a glyph to a desired width. + pub fn stretch_horizontal( + self, + ctx: &MathContext, + width: Abs, + short_fall: Abs, + ) -> VariantFragment { + stretch_glyph(ctx, self, width, short_fall, true) + } +} + +/// Try to stretch a glyph to a desired width or height. +/// +/// The resulting frame may not have the exact desired width. +fn stretch_glyph( + ctx: &MathContext, + mut base: GlyphFragment, + target: Abs, + short_fall: Abs, + horizontal: bool, +) -> VariantFragment { + let short_target = target - short_fall; + let mut min_overlap = Abs::zero(); + let construction = ctx + .table + .variants + .and_then(|variants| { + min_overlap = variants.min_connector_overlap.scaled(ctx); + if horizontal { + variants.horizontal_constructions + } else { + variants.vertical_constructions + } + .get(base.id) + }) + .unwrap_or(GlyphConstruction { assembly: None, variants: LazyArray16::new(&[]) }); + + // If the base glyph is good enough, use it. + let advance = if horizontal { base.width } else { base.height() }; + if short_target <= advance { + return base.into_variant(); + } + + // Search for a pre-made variant with a good advance. + let mut best_id = base.id; + let mut best_advance = base.width; + for variant in construction.variants { + best_id = variant.variant_glyph; + best_advance = base.font.to_em(variant.advance_measurement).at(base.font_size); + if short_target <= best_advance { + break; + } + } + + // This is either good or the best we've got. + if short_target <= best_advance || construction.assembly.is_none() { + base.set_id(ctx, best_id); + return base.into_variant(); + } + + // Assemble from parts. + let assembly = construction.assembly.unwrap(); + assemble(ctx, base, assembly, min_overlap, target, horizontal) +} + +/// Assemble a glyph from parts. +fn assemble( + ctx: &MathContext, + base: GlyphFragment, + assembly: GlyphAssembly, + min_overlap: Abs, + target: Abs, + horizontal: bool, +) -> VariantFragment { + // Determine the number of times the extenders need to be repeated as well + // as a ratio specifying how much to spread the parts apart + // (0 = maximal overlap, 1 = minimal overlap). + let mut full; + let mut ratio; + let mut repeat = 0; + loop { + full = Abs::zero(); + ratio = 0.0; + + let mut parts = parts(assembly, repeat).peekable(); + let mut growable = Abs::zero(); + + while let Some(part) = parts.next() { + let mut advance = part.full_advance.scaled(ctx); + if let Some(next) = parts.peek() { + let max_overlap = part + .end_connector_length + .min(next.start_connector_length) + .scaled(ctx); + + advance -= max_overlap; + growable += max_overlap - min_overlap; + } + + full += advance; + } + + if full < target { + let delta = target - full; + ratio = (delta / growable).min(1.0); + full += ratio * growable; + } + + if target <= full || repeat >= MAX_REPEATS { + break; + } + + repeat += 1; + } + + let mut selected = vec![]; + let mut parts = parts(assembly, repeat).peekable(); + while let Some(part) = parts.next() { + let mut advance = part.full_advance.scaled(ctx); + if let Some(next) = parts.peek() { + let max_overlap = + part.end_connector_length.min(next.start_connector_length).scaled(ctx); + advance -= max_overlap; + advance += ratio * (max_overlap - min_overlap); + } + + let mut fragment = base.clone(); + fragment.set_id(ctx, part.glyph_id); + selected.push((fragment, advance)); + } + + let size; + let baseline; + if horizontal { + let height = base.ascent + base.descent; + size = Size::new(full, height); + baseline = base.ascent; + } else { + let axis = scaled!(ctx, axis_height); + let width = selected.iter().map(|(f, _)| f.width).max().unwrap_or_default(); + size = Size::new(width, full); + baseline = full / 2.0 + axis; + } + + let mut frame = Frame::new(size); + let mut offset = Abs::zero(); + frame.set_baseline(baseline); + frame.meta_iter(base.meta); + + for (fragment, advance) in selected { + let pos = if horizontal { + Point::new(offset, frame.baseline() - fragment.ascent) + } else { + Point::with_y(full - offset - fragment.height()) + }; + frame.push_frame(pos, fragment.into_frame()); + offset += advance; + } + + VariantFragment { + c: base.c, + id: None, + frame, + style: base.style, + font_size: base.font_size, + italics_correction: Abs::zero(), + class: base.class, + span: base.span, + limits: base.limits, + } +} + +/// Return an iterator over the assembly's parts with extenders repeated the +/// specified number of times. +fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator<Item = GlyphPart> + '_ { + assembly.parts.into_iter().flat_map(move |part| { + let count = if part.part_flags.extender() { repeat } else { 1 }; + std::iter::repeat(part).take(count) + }) +} diff --git a/crates/typst-library/src/math/style.rs b/crates/typst-library/src/math/style.rs new file mode 100644 index 00000000..235770db --- /dev/null +++ b/crates/typst-library/src/math/style.rs @@ -0,0 +1,620 @@ +use super::*; + +/// Bold font style in math. +/// +/// ## Example { #example } +/// ```example +/// $ bold(A) := B^+ $ +/// ``` +/// +/// Display: Bold +/// Category: math +#[func] +pub fn bold( + /// The content to style. + body: Content, +) -> Content { + MathStyleElem::new(body).with_bold(Some(true)).pack() +} + +/// Upright (non-italic) font style in math. +/// +/// ## Example { #example } +/// ```example +/// $ upright(A) != A $ +/// ``` +/// +/// Display: Upright +/// Category: math +#[func] +pub fn upright( + /// The content to style. + body: Content, +) -> Content { + MathStyleElem::new(body).with_italic(Some(false)).pack() +} + +/// Italic font style in math. +/// +/// For roman letters and greek lowercase letters, this is already the default. +/// +/// Display: Italic +/// Category: math +#[func] +pub fn italic( + /// The content to style. + body: Content, +) -> Content { + MathStyleElem::new(body).with_italic(Some(true)).pack() +} +/// Serif (roman) font style in math. +/// +/// This is already the default. +/// +/// Display: Serif +/// Category: math +#[func] +pub fn serif( + /// The content to style. + body: Content, +) -> Content { + MathStyleElem::new(body).with_variant(Some(MathVariant::Serif)).pack() +} + +/// Sans-serif font style in math. +/// +/// ## Example { #example } +/// ```example +/// $ sans(A B C) $ +/// ``` +/// +/// Display: Sans-serif +/// Category: math +#[func] +pub fn sans( + /// The content to style. + body: Content, +) -> Content { + MathStyleElem::new(body).with_variant(Some(MathVariant::Sans)).pack() +} + +/// Calligraphic font style in math. +/// +/// ## Example { #example } +/// ```example +/// Let $cal(P)$ be the set of ... +/// ``` +/// +/// Display: Calligraphic +/// Category: math +#[func] +pub fn cal( + /// The content to style. + body: Content, +) -> Content { + MathStyleElem::new(body).with_variant(Some(MathVariant::Cal)).pack() +} + +/// Fraktur font style in math. +/// +/// ## Example { #example } +/// ```example +/// $ frak(P) $ +/// ``` +/// +/// Display: Fraktur +/// Category: math +#[func] +pub fn frak( + /// The content to style. + body: Content, +) -> Content { + MathStyleElem::new(body).with_variant(Some(MathVariant::Frak)).pack() +} + +/// Monospace font style in math. +/// +/// ## Example { #example } +/// ```example +/// $ mono(x + y = z) $ +/// ``` +/// +/// Display: Monospace +/// Category: math +#[func] +pub fn mono( + /// The content to style. + body: Content, +) -> Content { + MathStyleElem::new(body).with_variant(Some(MathVariant::Mono)).pack() +} + +/// 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 { #example } +/// ```example +/// $ bb(b) $ +/// $ bb(N) = NN $ +/// $ f: NN -> RR $ +/// ``` +/// +/// Display: Blackboard Bold +/// Category: math +#[func] +pub fn bb( + /// The content to style. + body: Content, +) -> Content { + MathStyleElem::new(body).with_variant(Some(MathVariant::Bb)).pack() +} + +/// Forced display style in math. +/// +/// This is the normal size for block equations. +/// +/// ## Example { #example } +/// ```example +/// $sum_i x_i/2 = display(sum_i x_i/2)$ +/// ``` +/// +/// Display: Display Size +/// Category: math +#[func] +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 { + MathStyleElem::new(body) + .with_size(Some(MathSize::Display)) + .with_cramped(Some(cramped)) + .pack() +} + +/// Forced inline (text) style in math. +/// +/// This is the normal size for inline equations. +/// +/// ## Example { #example } +/// ```example +/// $ sum_i x_i/2 +/// = inline(sum_i x_i/2) $ +/// ``` +/// +/// Display: Inline Size +/// Category: math +#[func] +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 { + MathStyleElem::new(body) + .with_size(Some(MathSize::Text)) + .with_cramped(Some(cramped)) + .pack() +} + +/// Forced script style in math. +/// +/// This is the smaller size used in powers or sub- or superscripts. +/// +/// ## Example { #example } +/// ```example +/// $sum_i x_i/2 = script(sum_i x_i/2)$ +/// ``` +/// +/// Display: Script Size +/// Category: math +#[func] +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 { + MathStyleElem::new(body) + .with_size(Some(MathSize::Script)) + .with_cramped(Some(cramped)) + .pack() +} + +/// Forced second script style in math. +/// +/// This is the smallest size, used in second-level sub- and superscripts +/// (script of the script). +/// +/// ## Example { #example } +/// ```example +/// $sum_i x_i/2 = sscript(sum_i x_i/2)$ +/// ``` +/// +/// Display: Script-Script Size +/// Category: math +#[func] +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 { + MathStyleElem::new(body) + .with_size(Some(MathSize::ScriptScript)) + .with_cramped(Some(cramped)) + .pack() +} + +/// A font variant in math. +/// +/// Display: Bold +/// Category: math +#[element(LayoutMath)] +pub struct MathStyleElem { + /// The content to style. + #[required] + pub body: Content, + + /// The variant to select. + pub variant: Option<MathVariant>, + + /// Whether to use bold glyphs. + pub bold: Option<bool>, + + /// Whether to use italic glyphs. + pub italic: Option<bool>, + + /// Whether to use forced size + pub size: Option<MathSize>, + + /// Whether to limit height of exponents + pub cramped: Option<bool>, +} + +impl LayoutMath for MathStyleElem { + #[tracing::instrument(skip(ctx))] + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + let mut style = ctx.style; + if let Some(variant) = self.variant(StyleChain::default()) { + style = style.with_variant(variant); + } + if let Some(bold) = self.bold(StyleChain::default()) { + style = style.with_bold(bold); + } + if let Some(italic) = self.italic(StyleChain::default()) { + style = style.with_italic(italic); + } + if let Some(size) = self.size(StyleChain::default()) { + style = style.with_size(size); + } + if let Some(cramped) = self.cramped(StyleChain::default()) { + style = style.with_cramped(cramped); + } + ctx.style(style); + self.body().layout_math(ctx)?; + ctx.unstyle(); + Ok(()) + } +} + +/// Text properties in math. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct MathStyle { + /// The style variant to select. + pub variant: MathVariant, + /// The size of the glyphs. + pub size: MathSize, + /// Affects the height of exponents. + pub cramped: bool, + /// Whether to use bold glyphs. + pub bold: bool, + /// Whether to use italic glyphs. + pub italic: Smart<bool>, +} + +impl MathStyle { + /// This style, with the given `variant`. + pub fn with_variant(self, variant: MathVariant) -> Self { + Self { variant, ..self } + } + + /// This style, with the given `size`. + pub fn with_size(self, size: MathSize) -> Self { + Self { size, ..self } + } + + /// This style, with `cramped` set to the given value. + pub fn with_cramped(self, cramped: bool) -> Self { + Self { cramped, ..self } + } + + /// This style, with `bold` set to the given value. + pub fn with_bold(self, bold: bool) -> Self { + Self { bold, ..self } + } + + /// This style, with `italic` set to the given value. + pub fn with_italic(self, italic: bool) -> Self { + Self { italic: Smart::Custom(italic), ..self } + } + + /// The style for subscripts in the current style. + pub fn for_subscript(self) -> Self { + self.for_superscript().with_cramped(true) + } + + /// The style for superscripts in the current style. + pub fn for_superscript(self) -> Self { + self.with_size(match self.size { + MathSize::Display | MathSize::Text => MathSize::Script, + MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript, + }) + } + + /// The style for numerators in the current style. + pub fn for_numerator(self) -> Self { + self.with_size(match self.size { + MathSize::Display => MathSize::Text, + MathSize::Text => MathSize::Script, + MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript, + }) + } + + /// The style for denominators in the current style. + pub fn for_denominator(self) -> Self { + self.for_numerator().with_cramped(true) + } + + /// Apply the style to a character. + pub fn styled_char(self, c: char) -> char { + styled_char(self, c) + } +} + +/// The size of elements in an equation. +/// +/// See the TeXbook p. 141. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Cast)] +pub enum MathSize { + /// Second-level sub- and superscripts. + ScriptScript, + /// Sub- and superscripts. + Script, + /// Math in text. + Text, + /// Math on its own line. + Display, +} + +impl MathSize { + pub(super) fn factor(self, ctx: &MathContext) -> f64 { + match self { + Self::Display | Self::Text => 1.0, + Self::Script => percent!(ctx, script_percent_scale_down), + Self::ScriptScript => percent!(ctx, script_script_percent_scale_down), + } + } +} + +/// A mathematical style variant, as defined by Unicode. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Cast)] +pub enum MathVariant { + Serif, + Sans, + Cal, + Frak, + Mono, + Bb, +} + +impl Default for MathVariant { + fn default() -> Self { + Self::Serif + } +} + +/// Select the correct styled math letter. +/// +/// https://www.w3.org/TR/mathml-core/#new-text-transform-mappings +/// https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols +pub(super) fn styled_char(style: MathStyle, c: char) -> char { + use MathVariant::*; + + let MathStyle { variant, bold, .. } = style; + let italic = style.italic.unwrap_or(matches!( + c, + 'a'..='z' | 'ı' | 'ȷ' | 'A'..='Z' | 'α'..='ω' | + '∂' | 'ϵ' | 'ϑ' | 'ϰ' | 'ϕ' | 'ϱ' | 'ϖ' + )); + + if let Some(c) = basic_exception(c) { + return c; + } + + if let Some(c) = latin_exception(c, variant, bold, italic) { + return c; + } + + if let Some(c) = greek_exception(c, variant, bold, italic) { + return c; + } + + let base = match c { + 'A'..='Z' => 'A', + 'a'..='z' => 'a', + 'Α'..='Ω' => 'Α', + 'α'..='ω' => 'α', + '0'..='9' => '0', + _ => return c, + }; + + let tuple = (variant, bold, italic); + let start = match c { + // Latin upper. + 'A'..='Z' => match tuple { + (Serif, false, false) => 0x0041, + (Serif, true, false) => 0x1D400, + (Serif, false, true) => 0x1D434, + (Serif, true, true) => 0x1D468, + (Sans, false, false) => 0x1D5A0, + (Sans, true, false) => 0x1D5D4, + (Sans, false, true) => 0x1D608, + (Sans, true, true) => 0x1D63C, + (Cal, false, _) => 0x1D49C, + (Cal, true, _) => 0x1D4D0, + (Frak, false, _) => 0x1D504, + (Frak, true, _) => 0x1D56C, + (Mono, _, _) => 0x1D670, + (Bb, _, _) => 0x1D538, + }, + + // Latin lower. + 'a'..='z' => match tuple { + (Serif, false, false) => 0x0061, + (Serif, true, false) => 0x1D41A, + (Serif, false, true) => 0x1D44E, + (Serif, true, true) => 0x1D482, + (Sans, false, false) => 0x1D5BA, + (Sans, true, false) => 0x1D5EE, + (Sans, false, true) => 0x1D622, + (Sans, true, true) => 0x1D656, + (Cal, false, _) => 0x1D4B6, + (Cal, true, _) => 0x1D4EA, + (Frak, false, _) => 0x1D51E, + (Frak, true, _) => 0x1D586, + (Mono, _, _) => 0x1D68A, + (Bb, _, _) => 0x1D552, + }, + + // Greek upper. + 'Α'..='Ω' => match tuple { + (Serif, false, false) => 0x0391, + (Serif, true, false) => 0x1D6A8, + (Serif, false, true) => 0x1D6E2, + (Serif, true, true) => 0x1D71C, + (Sans, _, false) => 0x1D756, + (Sans, _, true) => 0x1D790, + (Cal | Frak | Mono | Bb, _, _) => return c, + }, + + // Greek lower. + 'α'..='ω' => match tuple { + (Serif, false, false) => 0x03B1, + (Serif, true, false) => 0x1D6C2, + (Serif, false, true) => 0x1D6FC, + (Serif, true, true) => 0x1D736, + (Sans, _, false) => 0x1D770, + (Sans, _, true) => 0x1D7AA, + (Cal | Frak | Mono | Bb, _, _) => return c, + }, + + // Numbers. + '0'..='9' => match tuple { + (Serif, false, _) => 0x0030, + (Serif, true, _) => 0x1D7CE, + (Bb, _, _) => 0x1D7D8, + (Sans, false, _) => 0x1D7E2, + (Sans, true, _) => 0x1D7EC, + (Mono, _, _) => 0x1D7F6, + (Cal | Frak, _, _) => return c, + }, + + _ => unreachable!(), + }; + + std::char::from_u32(start + (c as u32 - base as u32)).unwrap() +} + +fn basic_exception(c: char) -> Option<char> { + Some(match c { + '〈' => '⟨', + '〉' => '⟩', + '《' => '⟪', + '》' => '⟫', + _ => return None, + }) +} + +fn latin_exception( + c: char, + variant: MathVariant, + bold: bool, + italic: bool, +) -> Option<char> { + use MathVariant::*; + Some(match (c, variant, bold, italic) { + ('B', Cal, false, _) => 'ℬ', + ('E', Cal, false, _) => 'ℰ', + ('F', Cal, false, _) => 'ℱ', + ('H', Cal, false, _) => 'ℋ', + ('I', Cal, false, _) => 'ℐ', + ('L', Cal, false, _) => 'ℒ', + ('M', Cal, false, _) => 'ℳ', + ('R', Cal, false, _) => 'ℛ', + ('C', Frak, false, _) => 'ℭ', + ('H', Frak, false, _) => 'ℌ', + ('I', Frak, false, _) => 'ℑ', + ('R', Frak, false, _) => 'ℜ', + ('Z', Frak, false, _) => 'ℨ', + ('C', Bb, ..) => 'ℂ', + ('H', Bb, ..) => 'ℍ', + ('N', Bb, ..) => 'ℕ', + ('P', Bb, ..) => 'ℙ', + ('Q', Bb, ..) => 'ℚ', + ('R', Bb, ..) => 'ℝ', + ('Z', Bb, ..) => 'ℤ', + ('h', Serif, false, true) => 'ℎ', + ('e', Cal, false, _) => 'ℯ', + ('g', Cal, false, _) => 'ℊ', + ('o', Cal, false, _) => 'ℴ', + ('ı', Serif, .., true) => '𝚤', + ('ȷ', Serif, .., true) => '𝚥', + _ => return None, + }) +} + +fn greek_exception( + c: char, + variant: MathVariant, + bold: bool, + italic: bool, +) -> Option<char> { + use MathVariant::*; + let list = match c { + 'ϴ' => ['𝚹', '𝛳', '𝜭', '𝝧', '𝞡'], + '∇' => ['𝛁', '𝛻', '𝜵', '𝝯', '𝞩'], + '∂' => ['𝛛', '𝜕', '𝝏', '𝞉', '𝟃'], + 'ϵ' => ['𝛜', '𝜖', '𝝐', '𝞊', '𝟄'], + 'ϑ' => ['𝛝', '𝜗', '𝝑', '𝞋', '𝟅'], + 'ϰ' => ['𝛞', '𝜘', '𝝒', '𝞌', '𝟆'], + 'ϕ' => ['𝛟', '𝜙', '𝝓', '𝞍', '𝟇'], + 'ϱ' => ['𝛠', '𝜚', '𝝔', '𝞎', '𝟈'], + 'ϖ' => ['𝛡', '𝜛', '𝝕', '𝞏', '𝟉'], + _ => return None, + }; + + Some(match (variant, bold, italic) { + (Serif, true, false) => list[0], + (Serif, false, true) => list[1], + (Serif, true, true) => list[2], + (Sans, _, false) => list[3], + (Sans, _, true) => list[4], + _ => return None, + }) +} diff --git a/crates/typst-library/src/math/underover.rs b/crates/typst-library/src/math/underover.rs new file mode 100644 index 00000000..796c9ebc --- /dev/null +++ b/crates/typst-library/src/math/underover.rs @@ -0,0 +1,339 @@ +use super::*; + +const BRACE_GAP: Em = Em::new(0.25); +const BRACKET_GAP: Em = Em::new(0.25); + +/// A marker to distinguish under- vs. overlines. +enum LineKind { + Over, + Under, +} + +/// A horizontal line under content. +/// +/// ## Example { #example } +/// ```example +/// $ underline(1 + 2 + ... + 5) $ +/// ``` +/// +/// Display: Underline +/// Category: math +#[element(LayoutMath)] +pub struct UnderlineElem { + /// The content above the line. + #[required] + pub body: Content, +} + +impl LayoutMath for UnderlineElem { + #[tracing::instrument(skip(ctx))] + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + layout_underoverline(ctx, &self.body(), self.span(), LineKind::Under) + } +} + +/// A horizontal line over content. +/// +/// ## Example { #example } +/// ```example +/// $ overline(1 + 2 + ... + 5) $ +/// ``` +/// +/// Display: Overline +/// Category: math +#[element(LayoutMath)] +pub struct OverlineElem { + /// The content below the line. + #[required] + pub body: Content, +} + +impl LayoutMath for OverlineElem { + #[tracing::instrument(skip(ctx))] + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + layout_underoverline(ctx, &self.body(), self.span(), LineKind::Over) + } +} + +/// layout under- or overlined content +fn layout_underoverline( + ctx: &mut MathContext, + body: &Content, + span: Span, + line: LineKind, +) -> SourceResult<()> { + let (extra_height, content, line_pos, content_pos, baseline, bar_height); + match line { + LineKind::Under => { + let sep = scaled!(ctx, underbar_extra_descender); + bar_height = scaled!(ctx, underbar_rule_thickness); + let gap = scaled!(ctx, underbar_vertical_gap); + extra_height = sep + bar_height + gap; + + content = ctx.layout_fragment(body)?; + + line_pos = Point::with_y(content.height() + gap + bar_height / 2.0); + content_pos = Point::zero(); + baseline = content.ascent() + } + LineKind::Over => { + let sep = scaled!(ctx, overbar_extra_ascender); + bar_height = scaled!(ctx, overbar_rule_thickness); + let gap = scaled!(ctx, overbar_vertical_gap); + extra_height = sep + bar_height + gap; + + ctx.style(ctx.style.with_cramped(true)); + content = ctx.layout_fragment(body)?; + ctx.unstyle(); + + line_pos = Point::with_y(sep + bar_height / 2.0); + content_pos = Point::with_y(extra_height); + baseline = content.ascent() + extra_height; + } + } + + let width = content.width(); + let height = content.height() + extra_height; + let size = Size::new(width, height); + + let content_class = content.class().unwrap_or(MathClass::Normal); + let mut frame = Frame::new(size); + frame.set_baseline(baseline); + frame.push_frame(content_pos, content.into_frame()); + frame.push( + line_pos, + FrameItem::Shape( + Geometry::Line(Point::with_x(width)).stroked(Stroke { + paint: TextElem::fill_in(ctx.styles()), + thickness: bar_height, + ..Stroke::default() + }), + span, + ), + ); + + ctx.push(FrameFragment::new(ctx, frame).with_class(content_class)); + + Ok(()) +} + +/// A horizontal brace under content, with an optional annotation below. +/// +/// ## Example { #example } +/// ```example +/// $ underbrace(1 + 2 + ... + 5, "numbers") $ +/// ``` +/// +/// Display: Underbrace +/// Category: math +#[element(LayoutMath)] +pub struct UnderbraceElem { + /// The content above the brace. + #[required] + pub body: Content, + + /// The optional content below the brace. + #[positional] + pub annotation: Option<Content>, +} + +impl LayoutMath for UnderbraceElem { + #[tracing::instrument(skip(ctx))] + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + layout_underoverspreader( + ctx, + &self.body(), + &self.annotation(ctx.styles()), + '⏟', + BRACE_GAP, + false, + self.span(), + ) + } +} + +/// A horizontal brace over content, with an optional annotation above. +/// +/// ## Example { #example } +/// ```example +/// $ overbrace(1 + 2 + ... + 5, "numbers") $ +/// ``` +/// +/// Display: Overbrace +/// Category: math +#[element(LayoutMath)] +pub struct OverbraceElem { + /// The content below the brace. + #[required] + pub body: Content, + + /// The optional content above the brace. + #[positional] + pub annotation: Option<Content>, +} + +impl LayoutMath for OverbraceElem { + #[tracing::instrument(skip(ctx))] + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + layout_underoverspreader( + ctx, + &self.body(), + &self.annotation(ctx.styles()), + '⏞', + BRACE_GAP, + true, + self.span(), + ) + } +} + +/// A horizontal bracket under content, with an optional annotation below. +/// +/// ## Example { #example } +/// ```example +/// $ underbracket(1 + 2 + ... + 5, "numbers") $ +/// ``` +/// +/// Display: Underbracket +/// Category: math +#[element(LayoutMath)] +pub struct UnderbracketElem { + /// The content above the bracket. + #[required] + pub body: Content, + + /// The optional content below the bracket. + #[positional] + pub annotation: Option<Content>, +} + +impl LayoutMath for UnderbracketElem { + #[tracing::instrument(skip(ctx))] + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + layout_underoverspreader( + ctx, + &self.body(), + &self.annotation(ctx.styles()), + '⎵', + BRACKET_GAP, + false, + self.span(), + ) + } +} + +/// A horizontal bracket over content, with an optional annotation above. +/// +/// ## Example { #example } +/// ```example +/// $ overbracket(1 + 2 + ... + 5, "numbers") $ +/// ``` +/// +/// Display: Overbracket +/// Category: math +#[element(LayoutMath)] +pub struct OverbracketElem { + /// The content below the bracket. + #[required] + pub body: Content, + + /// The optional content above the bracket. + #[positional] + pub annotation: Option<Content>, +} + +impl LayoutMath for OverbracketElem { + #[tracing::instrument(skip(ctx))] + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + layout_underoverspreader( + ctx, + &self.body(), + &self.annotation(ctx.styles()), + '⎴', + BRACKET_GAP, + true, + self.span(), + ) + } +} + +/// Layout an over- or underbrace-like object. +fn layout_underoverspreader( + ctx: &mut MathContext, + body: &Content, + annotation: &Option<Content>, + c: char, + gap: Em, + reverse: bool, + span: Span, +) -> SourceResult<()> { + let gap = gap.scaled(ctx); + let body = ctx.layout_row(body)?; + let body_class = body.class(); + let body = body.into_fragment(ctx); + let glyph = GlyphFragment::new(ctx, c, span); + let stretched = glyph.stretch_horizontal(ctx, body.width(), Abs::zero()); + + let mut rows = vec![MathRow::new(vec![body]), stretched.into()]; + ctx.style(if reverse { + ctx.style.for_subscript() + } else { + ctx.style.for_superscript() + }); + rows.extend( + annotation + .as_ref() + .map(|annotation| ctx.layout_row(annotation)) + .transpose()?, + ); + ctx.unstyle(); + + let mut baseline = 0; + if reverse { + rows.reverse(); + baseline = rows.len() - 1; + } + + let frame = stack(ctx, rows, Align::Center, gap, baseline); + ctx.push(FrameFragment::new(ctx, frame).with_class(body_class)); + + Ok(()) +} + +/// Stack rows on top of each other. +/// +/// Add a `gap` between each row and uses the baseline of the `baseline`th +/// row for the whole frame. +pub(super) fn stack( + ctx: &MathContext, + rows: Vec<MathRow>, + align: Align, + gap: Abs, + baseline: usize, +) -> Frame { + let rows: Vec<_> = rows.into_iter().flat_map(|r| r.rows()).collect(); + let AlignmentResult { points, width } = alignments(&rows); + let rows: Vec<_> = rows + .into_iter() + .map(|row| row.into_aligned_frame(ctx, &points, align)) + .collect(); + + let mut y = Abs::zero(); + let mut frame = Frame::new(Size::new( + width, + rows.iter().map(|row| row.height()).sum::<Abs>() + + rows.len().saturating_sub(1) as f64 * gap, + )); + + for (i, row) in rows.into_iter().enumerate() { + let x = align.position(width - row.width()); + let pos = Point::new(x, y); + if i == baseline { + frame.set_baseline(y + row.baseline()); + } + y += row.height() + gap; + frame.push_frame(pos, row); + } + + frame +} diff --git a/crates/typst-library/src/meta/bibliography.rs b/crates/typst-library/src/meta/bibliography.rs new file mode 100644 index 00000000..0531997d --- /dev/null +++ b/crates/typst-library/src/meta/bibliography.rs @@ -0,0 +1,724 @@ +use std::collections::HashMap; +use std::ffi::OsStr; +use std::path::Path; +use std::sync::Arc; + +use ecow::{eco_vec, EcoVec}; +use hayagriva::io::{BibLaTeXError, YamlBibliographyError}; +use hayagriva::style::{self, Brackets, Citation, Database, DisplayString, Formatting}; +use hayagriva::Entry; +use typst::diag::FileError; +use typst::util::{option_eq, Bytes}; + +use super::{LinkElem, LocalName, RefElem}; +use crate::layout::{BlockElem, GridElem, ParElem, Sizing, TrackSizings, VElem}; +use crate::meta::{FootnoteElem, HeadingElem}; +use crate::prelude::*; +use crate::text::TextElem; + +/// 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]($func/cite) function (`[#cite("key")]`). The bibliography will +/// only show entries for works that were referenced in the document. +/// +/// # Example +/// ```example +/// This was already noted by +/// pirates long ago. @arrgh +/// +/// Multiple sources say ... +/// #cite("arrgh", "netwok"). +/// +/// #bibliography("works.bib") +/// ``` +/// +/// Display: Bibliography +/// Category: meta +#[element(Locatable, Synthesize, Show, Finalize, LocalName)] +pub struct BibliographyElem { + /// Path to a Hayagriva `.yml` or BibLaTeX `.bib` file. + #[required] + #[parse( + let Spanned { v: paths, span } = + args.expect::<Spanned<BibPaths>>("path to bibliography file")?; + + // Load bibliography files. + let data = paths.0 + .iter() + .map(|path| { + let id = vm.location().join(path).at(span)?; + vm.world().file(id).at(span) + }) + .collect::<SourceResult<Vec<Bytes>>>()?; + + // Check that parsing works. + let _ = load(&paths, &data).at(span)?; + + paths + )] + pub path: BibPaths, + + /// The raw file buffers. + #[internal] + #[required] + #[parse(data)] + pub data: Vec<Bytes>, + + /// The title of the bibliography. + /// + /// - When set to `{auto}`, an appropriate title for the [text + /// language]($func/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.")}` + /// ``` + #[default(Some(Smart::Auto))] + pub title: Option<Smart<Content>>, + + /// The bibliography style. + #[default(BibliographyStyle::Ieee)] + pub style: BibliographyStyle, +} + +/// A list of bibliography file paths. +#[derive(Debug, Default, Clone, Hash)] +pub struct BibPaths(Vec<EcoString>); + +cast! { + BibPaths, + self => self.0.into_value(), + v: EcoString => Self(vec![v]), + v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?), +} + +impl BibliographyElem { + /// Find the document's bibliography. + pub fn find(introspector: Tracked<Introspector>) -> StrResult<Self> { + let mut iter = introspector.query(&Self::func().select()).into_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 supported"); + } + + Ok(elem.to::<Self>().unwrap().clone()) + } + + /// Whether the bibliography contains the given key. + pub fn has(vt: &Vt, key: &str) -> bool { + vt.introspector + .query(&Self::func().select()) + .into_iter() + .flat_map(|elem| { + let elem = elem.to::<Self>().unwrap(); + load(&elem.path(), &elem.data()) + }) + .flatten() + .any(|entry| entry.key() == key) + } + + /// Find all bibliography keys. + pub fn keys( + introspector: Tracked<Introspector>, + ) -> Vec<(EcoString, Option<EcoString>)> { + Self::find(introspector) + .and_then(|elem| load(&elem.path(), &elem.data())) + .into_iter() + .flatten() + .map(|entry| { + let key = entry.key().into(); + let detail = + entry.title().map(|title| title.canonical.value.as_str().into()); + (key, detail) + }) + .collect() + } +} + +impl Synthesize for BibliographyElem { + fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> { + self.push_style(self.style(styles)); + Ok(()) + } +} + +impl Show for BibliographyElem { + #[tracing::instrument(name = "BibliographyElem::show", skip_all)] + fn show(&self, vt: &mut Vt, 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) { + let title = + title.unwrap_or_else(|| { + TextElem::packed(self.local_name( + TextElem::lang_in(styles), + TextElem::region_in(styles), + )) + .spanned(self.span()) + }); + + seq.push(HeadingElem::new(title).with_level(NonZeroUsize::ONE).pack()); + } + + Ok(vt.delayed(|vt| { + let works = Works::new(vt).at(self.span())?; + + let row_gutter = BlockElem::below_in(styles).amount(); + if works.references.iter().any(|(prefix, _)| prefix.is_some()) { + let mut cells = vec![]; + for (prefix, reference) in &works.references { + cells.push(prefix.clone().unwrap_or_default()); + cells.push(reference.clone()); + } + + seq.push(VElem::new(row_gutter).with_weakness(3).pack()); + seq.push( + GridElem::new(cells) + .with_columns(TrackSizings(vec![Sizing::Auto; 2])) + .with_column_gutter(TrackSizings(vec![COLUMN_GUTTER.into()])) + .with_row_gutter(TrackSizings(vec![row_gutter.into()])) + .pack(), + ); + } else { + let mut entries = vec![]; + for (_, reference) in &works.references { + entries.push(VElem::new(row_gutter).with_weakness(3).pack()); + entries.push(reference.clone()); + } + + seq.push( + Content::sequence(entries) + .styled(ParElem::set_hanging_indent(INDENT.into())), + ); + } + + Ok(Content::sequence(seq)) + })) + } +} + +impl Finalize for BibliographyElem { + fn finalize(&self, realized: Content, _: StyleChain) -> Content { + realized.styled(HeadingElem::set_numbering(None)) + } +} + +impl LocalName for BibliographyElem { + fn local_name(&self, lang: Lang, region: Option<Region>) -> &'static str { + match lang { + Lang::ALBANIAN => "Bibliografi", + Lang::ARABIC => "المراجع", + Lang::BOKMÅL => "Bibliografi", + Lang::CHINESE if option_eq(region, "TW") => "書目", + Lang::CHINESE => "参考文献", + Lang::CZECH => "Bibliografie", + Lang::DANISH => "Bibliografi", + Lang::DUTCH => "Bibliografie", + Lang::FILIPINO => "Bibliograpiya", + Lang::FRENCH => "Bibliographie", + Lang::GERMAN => "Bibliographie", + Lang::ITALIAN => "Bibliografia", + Lang::NYNORSK => "Bibliografi", + Lang::POLISH => "Bibliografia", + Lang::PORTUGUESE => "Bibliografia", + Lang::RUSSIAN => "Библиография", + Lang::SLOVENIAN => "Literatura", + Lang::SPANISH => "Bibliografía", + Lang::SWEDISH => "Bibliografi", + Lang::TURKISH => "Kaynakça", + Lang::UKRAINIAN => "Бібліографія", + Lang::VIETNAMESE => "Tài liệu tham khảo", + Lang::ENGLISH | _ => "Bibliography", + } + } +} + +/// A bibliography style. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum BibliographyStyle { + /// Follows guidance of the American Psychological Association. Based on the + /// 7th edition of the APA Publication Manual. + Apa, + /// The Chicago Author Date style. Based on the 17th edition of the Chicago + /// Manual of Style, Chapter 15. + ChicagoAuthorDate, + /// The Chicago Notes style. Based on the 17th edition of the Chicago + /// Manual of Style, Chapter 14. + ChicagoNotes, + /// The style of the Institute of Electrical and Electronics Engineers. + /// Based on the 2018 IEEE Reference Guide. + Ieee, + /// Follows guidance of the Modern Language Association. Based on the 8th + /// edition of the MLA Handbook. + Mla, +} + +impl BibliographyStyle { + /// The default citation style for this bibliography style. + pub fn default_citation_style(self) -> CitationStyle { + match self { + Self::Apa => CitationStyle::ChicagoAuthorDate, + Self::ChicagoAuthorDate => CitationStyle::ChicagoAuthorDate, + Self::ChicagoNotes => CitationStyle::ChicagoNotes, + Self::Ieee => CitationStyle::Numerical, + Self::Mla => CitationStyle::ChicagoAuthorDate, + } + } +} + +/// Cite a work from the bibliography. +/// +/// Before you starting citing, you need to add a +/// [bibliography]($func/bibliography) somewhere in your document. +/// +/// # Example +/// ```example +/// This was already noted by +/// pirates long ago. @arrgh +/// +/// Multiple sources say ... +/// #cite("arrgh", "netwok"). +/// +/// #bibliography("works.bib") +/// ``` +/// +/// # Syntax +/// This function indirectly has dedicated syntax. [References]($func/ref) +/// can be used to cite works from the bibliography. The label then +/// corresponds to the citation key. +/// +/// Display: Citation +/// Category: meta +#[element(Locatable, Synthesize, Show)] +pub struct CiteElem { + /// The citation keys that identify the elements that shall be cited in + /// the bibliography. + /// + /// Reference syntax supports only a single key. + #[variadic] + pub keys: Vec<EcoString>, + + /// 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 over and + /// over again. @distress[p.~7] + /// + /// #bibliography("works.bib") + /// ``` + #[positional] + pub supplement: Option<Content>, + + /// Whether the citation should include brackets. + /// + /// ```example + /// #set cite(brackets: false) + /// + /// @netwok follow these methods + /// in their work ... + /// + /// #bibliography( + /// "works.bib", + /// style: "chicago-author-date", + /// ) + /// ``` + #[default(true)] + pub brackets: bool, + + /// The citation style. + /// + /// When set to `{auto}`, automatically picks the preferred citation style + /// for the bibliography's style. + /// + /// ```example + /// #set cite(style: "alphanumerical") + /// Alphanumerical references. + /// @netwok + /// + /// #bibliography("works.bib") + /// ``` + pub style: Smart<CitationStyle>, +} + +impl Synthesize for CiteElem { + fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> { + self.push_supplement(self.supplement(styles)); + self.push_brackets(self.brackets(styles)); + self.push_style(self.style(styles)); + Ok(()) + } +} + +impl Show for CiteElem { + #[tracing::instrument(name = "CiteElem::show", skip(self, vt))] + fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> { + Ok(vt.delayed(|vt| { + let works = Works::new(vt).at(self.span())?; + let location = self.0.location().unwrap(); + works + .citations + .get(&location) + .cloned() + .flatten() + .ok_or("bibliography does not contain this key") + .at(self.span()) + })) + } +} + +cast! { + CiteElem, + v: Content => v.to::<Self>().cloned().ok_or("expected citation")?, +} + +/// A citation style. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum CitationStyle { + /// IEEE-style numerical reference markers. + Numerical, + /// A simple alphanumerical style. For example, the output could be Rass97 + /// or MKG+21. + Alphanumerical, + /// Citations that just consist of the entry keys. + Keys, + /// The Chicago Author Date style. Based on the 17th edition of the Chicago + /// Manual of Style, Chapter 15. + ChicagoAuthorDate, + /// The Chicago Notes style. Based on the 17th edition of the Chicago + /// Manual of Style, Chapter 14. + ChicagoNotes, + /// A Chicago-like author-title format. Results could look like this: + /// Prokopov, “It Is Fast or It Is Wrong”. + ChicagoAuthorTitle, +} + +impl CitationStyle { + fn is_short(self) -> bool { + matches!(self, Self::Numerical | Self::Alphanumerical | Self::Keys) + } +} + +/// Fully formatted citations and references. +#[derive(Default)] +struct Works { + citations: HashMap<Location, Option<Content>>, + references: Vec<(Option<Content>, Content)>, +} + +impl Works { + /// Prepare all things need to cite a work or format a bibliography. + fn new(vt: &Vt) -> StrResult<Arc<Self>> { + let bibliography = BibliographyElem::find(vt.introspector)?; + let citations = vt + .introspector + .query(&Selector::Or(eco_vec![ + RefElem::func().select(), + CiteElem::func().select(), + ])) + .into_iter() + .map(|elem| match elem.to::<RefElem>() { + Some(reference) => reference.citation().unwrap(), + _ => elem.to::<CiteElem>().unwrap().clone(), + }) + .collect(); + Ok(create(bibliography, citations)) + } +} + +/// Generate all citations and the whole bibliography. +#[comemo::memoize] +fn create(bibliography: BibliographyElem, citations: Vec<CiteElem>) -> Arc<Works> { + let span = bibliography.span(); + let entries = load(&bibliography.path(), &bibliography.data()).unwrap(); + let style = bibliography.style(StyleChain::default()); + let bib_location = bibliography.0.location().unwrap(); + let ref_location = |target: &Entry| { + let i = entries + .iter() + .position(|entry| entry.key() == target.key()) + .unwrap_or_default(); + bib_location.variant(i) + }; + + let mut db = Database::new(); + let mut ids = HashMap::new(); + let mut preliminary = vec![]; + + for citation in citations { + let cite_id = citation.0.location().unwrap(); + let entries = citation + .keys() + .into_iter() + .map(|key| { + let entry = entries.iter().find(|entry| entry.key() == key)?; + ids.entry(entry.key()).or_insert(cite_id); + db.push(entry); + Some(entry) + }) + .collect::<Option<Vec<_>>>(); + preliminary.push((citation, entries)); + } + + let mut current = CitationStyle::Numerical; + let mut citation_style: Box<dyn style::CitationStyle> = + Box::new(style::Numerical::new()); + + let citations = preliminary + .into_iter() + .map(|(citation, cited)| { + let location = citation.0.location().unwrap(); + let Some(cited) = cited else { return (location, None) }; + + let mut supplement = citation.supplement(StyleChain::default()); + let brackets = citation.brackets(StyleChain::default()); + let style = citation + .style(StyleChain::default()) + .unwrap_or(style.default_citation_style()); + + if style != current { + current = style; + citation_style = match style { + CitationStyle::Numerical => Box::new(style::Numerical::new()), + CitationStyle::Alphanumerical => { + Box::new(style::Alphanumerical::new()) + } + CitationStyle::ChicagoAuthorDate => { + Box::new(style::ChicagoAuthorDate::new()) + } + CitationStyle::ChicagoNotes => Box::new(style::ChicagoNotes::new()), + CitationStyle::ChicagoAuthorTitle => { + Box::new(style::AuthorTitle::new()) + } + CitationStyle::Keys => Box::new(style::Keys::new()), + }; + } + + let len = cited.len(); + let mut content = Content::empty(); + for (i, entry) in cited.into_iter().enumerate() { + let supplement = if i + 1 == len { supplement.take() } else { None }; + let mut display = db + .citation( + &mut *citation_style, + &[Citation { + entry, + supplement: supplement.is_some().then_some(SUPPLEMENT), + }], + ) + .display; + + if style.is_short() { + display.value = display.value.replace(' ', "\u{a0}"); + } + + if brackets && len == 1 { + display = display.with_default_brackets(&*citation_style); + } + + if i > 0 { + content += TextElem::packed(",\u{a0}"); + } + + // Format and link to the reference entry. + content += format_display_string(&display, supplement, citation.span()) + .linked(Destination::Location(ref_location(entry))); + } + + if brackets && len > 1 { + content = match citation_style.brackets() { + Brackets::None => content, + Brackets::Round => { + TextElem::packed('(') + content + TextElem::packed(')') + } + Brackets::Square => { + TextElem::packed('[') + content + TextElem::packed(']') + } + }; + } + + if style == CitationStyle::ChicagoNotes { + content = FootnoteElem::with_content(content).pack(); + } + + (location, Some(content)) + }) + .collect(); + + let bibliography_style: Box<dyn style::BibliographyStyle> = match style { + BibliographyStyle::Apa => Box::new(style::Apa::new()), + BibliographyStyle::ChicagoAuthorDate => Box::new(style::ChicagoAuthorDate::new()), + BibliographyStyle::ChicagoNotes => Box::new(style::ChicagoNotes::new()), + BibliographyStyle::Ieee => Box::new(style::Ieee::new()), + BibliographyStyle::Mla => Box::new(style::Mla::new()), + }; + + let references = db + .bibliography(&*bibliography_style, None) + .into_iter() + .map(|reference| { + let backlink = ref_location(reference.entry); + let prefix = reference.prefix.map(|prefix| { + // Format and link to first citation. + let bracketed = prefix.with_default_brackets(&*citation_style); + format_display_string(&bracketed, None, span) + .linked(Destination::Location(ids[reference.entry.key()])) + .backlinked(backlink) + }); + + let mut reference = format_display_string(&reference.display, None, span); + if prefix.is_none() { + reference = reference.backlinked(backlink); + } + + (prefix, reference) + }) + .collect(); + + Arc::new(Works { citations, references }) +} + +/// Load bibliography entries from a path. +#[comemo::memoize] +fn load(paths: &BibPaths, data: &[Bytes]) -> StrResult<EcoVec<hayagriva::Entry>> { + let mut result = EcoVec::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::InvalidUtf8)?; + let entries = parse_bib(path, src)?; + result.extend(entries); + } + + // Biblatex only checks for duplicate keys within files + // -> We have to do this between files again + let mut keys = result.iter().map(|r| r.key()).collect::<Vec<_>>(); + keys.sort_unstable(); + // Waiting for `slice_partition_dedup` #54279 + let mut duplicates = Vec::new(); + for pair in keys.windows(2) { + if pair[0] == pair[1] { + duplicates.push(pair[0]); + } + } + + if !duplicates.is_empty() { + Err(eco_format!("duplicate bibliography keys: {}", duplicates.join(", "))) + } else { + Ok(result) + } +} + +/// Parse a bibliography file (bib/yml/yaml) +fn parse_bib(path_str: &str, src: &str) -> StrResult<Vec<hayagriva::Entry>> { + let path = Path::new(path_str); + let ext = path.extension().and_then(OsStr::to_str).unwrap_or_default(); + match ext.to_lowercase().as_str() { + "yml" | "yaml" => { + hayagriva::io::from_yaml_str(src).map_err(format_hayagriva_error) + } + "bib" => hayagriva::io::from_biblatex_str(src).map_err(|err| { + err.into_iter() + .next() + .map(|error| format_biblatex_error(path_str, src, error)) + .unwrap_or_else(|| eco_format!("failed to parse {path_str}")) + }), + _ => bail!("unknown bibliography format (must be .yml/.yaml or .bib)"), + } +} + +/// Format a Hayagriva loading error. +fn format_hayagriva_error(error: YamlBibliographyError) -> EcoString { + eco_format!("{error}") +} + +/// Format a BibLaTeX loading error. +fn format_biblatex_error(path: &str, src: &str, error: BibLaTeXError) -> EcoString { + 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!("parsing failed at {path}:{line}: {msg}") +} + +/// Hayagriva only supports strings, but we have a content supplement. To deal +/// with this, we pass this string to hayagriva instead of our content, find it +/// in the output and replace it with the content. +const SUPPLEMENT: &str = "cdc579c45cf3d648905c142c7082683f"; + +/// Format a display string into content. +fn format_display_string( + string: &DisplayString, + mut supplement: Option<Content>, + span: Span, +) -> Content { + let mut stops: Vec<_> = string + .formatting + .iter() + .flat_map(|(range, _)| [range.start, range.end]) + .collect(); + + if let Some(i) = string.value.find(SUPPLEMENT) { + stops.push(i); + stops.push(i + SUPPLEMENT.len()); + } + + stops.sort(); + stops.dedup(); + stops.push(string.value.len()); + + let mut start = 0; + let mut seq = vec![]; + for stop in stops { + let segment = string.value.get(start..stop).unwrap_or_default(); + if segment.is_empty() { + continue; + } + + let mut content = if segment == SUPPLEMENT && supplement.is_some() { + supplement.take().unwrap_or_default() + } else { + TextElem::packed(segment).spanned(span) + }; + + for (range, fmt) in &string.formatting { + if !range.contains(&start) { + continue; + } + + content = match fmt { + Formatting::Bold => content.strong(), + Formatting::Italic => content.emph(), + Formatting::Link(link) => { + LinkElem::new(Destination::Url(link.as_str().into()).into(), content) + .pack() + } + }; + } + + seq.push(content); + start = stop; + } + + Content::sequence(seq) +} diff --git a/crates/typst-library/src/meta/context.rs b/crates/typst-library/src/meta/context.rs new file mode 100644 index 00000000..a42c6980 --- /dev/null +++ b/crates/typst-library/src/meta/context.rs @@ -0,0 +1,220 @@ +use crate::prelude::*; + +/// Provides access to the location of content. +/// +/// This is useful in combination with [queries]($func/query), +/// [counters]($func/counter), [state]($func/state), and [links]($func/link). +/// See their documentation for more details. +/// +/// ```example +/// #locate(loc => [ +/// My location: \ +/// #loc.position()! +/// ]) +/// ``` +/// +/// ## Methods +/// ### page() +/// Returns the page number for this location. +/// +/// Note that this does not return the value of the [page counter]($func/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. +/// +/// - returns: integer +/// +/// ### position() +/// 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. +/// +/// - returns: dictionary +/// +/// ### page-numbering() +/// 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`. +/// +/// - returns: string or function or none +/// +/// Display: Locate +/// Category: meta +#[func] +pub fn locate( + /// A function that receives a `location`. Its return value is displayed + /// in the document. + /// + /// This function is called once for each time the content returned by + /// `locate` appears in the document. That makes it possible to generate + /// content that depends on its own location in the document. + func: Func, +) -> Content { + LocateElem::new(func).pack() +} + +/// Executes a `locate` call. +/// +/// Display: Locate +/// Category: special +#[element(Locatable, Show)] +struct LocateElem { + /// The function to call with the location. + #[required] + func: Func, +} + +impl Show for LocateElem { + #[tracing::instrument(name = "LocateElem::show", skip(self, vt))] + fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> { + Ok(vt.delayed(|vt| { + let location = self.0.location().unwrap(); + Ok(self.func().call_vt(vt, [location])?.display()) + })) + } +} + +/// Provides access to active styles. +/// +/// The styles are currently opaque and only useful in combination with the +/// [`measure`]($func/measure) function. See its documentation for more details. +/// In the future, the provided styles might also be directly accessed to look +/// up styles defined by [set rules]($styling/#set-rules). +/// +/// ```example +/// #let thing(body) = style(styles => { +/// let size = measure(body, styles) +/// [Width of "#body" is #size.width] +/// }) +/// +/// #thing[Hey] \ +/// #thing[Welcome] +/// ``` +/// +/// Display: Style +/// Category: meta +#[func] +pub fn style( + /// 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 { + StyleElem::new(func).pack() +} + +/// Executes a style access. +/// +/// Display: Style +/// Category: special +#[element(Show)] +struct StyleElem { + /// The function to call with the styles. + #[required] + func: Func, +} + +impl Show for StyleElem { + #[tracing::instrument(name = "StyleElem::show", skip_all)] + fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + Ok(self.func().call_vt(vt, [styles.to_map()])?.display()) + } +} + +/// Provides access to the current outer container's (or page's, if none) size +/// (width and height). +/// +/// The given function must accept a single parameter, `size`, which is a +/// dictionary with keys `width` and `height`, both of type +/// [`length`]($type/length). +/// + +/// ```example +/// #let text = lorem(30) +/// #layout(size => style(styles => [ +/// #let (height,) = measure( +/// block(width: size.width, text), +/// styles, +/// ) +/// This text is #height high with +/// the current page width: \ +/// #text +/// ])) +/// ``` +/// +/// If the `layout` call is placed inside of a box width a width of `{800pt}` +/// and a height of `{400pt}`, then the specified function will be given the +/// parameter `{(width: 800pt, height: 400pt)}`. If it placed directly into the +/// page it receives the page's dimensions minus its margins. This is mostly +/// useful in combination with [measurement]($func/measure). +/// +/// You can also use this function to resolve [`ratio`]($type/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 this function will provide an infinite width or height if one of +/// the page width or height is `auto`, respectively. +/// +/// Display: Layout +/// Category: meta +#[func] +pub fn layout( + /// 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]($type/dictionary) with + /// the keys `width` and `height`. + /// + /// This function is called once for each time the content returned by + /// `layout` appears in the document. That makes it possible to generate + /// content that depends on the size of the container it is inside of. + func: Func, +) -> Content { + LayoutElem::new(func).pack() +} + +/// Executes a `layout` call. +/// +/// Display: Layout +/// Category: special +#[element(Layout)] +struct LayoutElem { + /// The function to call with the outer container's (or page's) size. + #[required] + func: Func, +} + +impl Layout for LayoutElem { + #[tracing::instrument(name = "LayoutElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + // 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 result = self + .func() + .call_vt(vt, [dict! { "width" => x, "height" => y }])? + .display(); + result.layout(vt, styles, regions) + } +} diff --git a/crates/typst-library/src/meta/counter.rs b/crates/typst-library/src/meta/counter.rs new file mode 100644 index 00000000..9a223b32 --- /dev/null +++ b/crates/typst-library/src/meta/counter.rs @@ -0,0 +1,683 @@ +use std::fmt::{self, Debug, Formatter, Write}; +use std::str::FromStr; + +use ecow::{eco_vec, EcoVec}; +use smallvec::{smallvec, SmallVec}; +use typst::eval::Tracer; +use typst::model::DelayedErrors; + +use super::{FigureElem, HeadingElem, Numbering, NumberingPattern}; +use crate::layout::PageElem; +use crate::math::EquationElem; +use crate::prelude::*; + +/// 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. +/// +/// ## Displaying a counter { #displaying } +/// To display the current value of the heading counter, you call the `counter` +/// function with the `key` set to `heading` and then call the `display` method +/// on the counter. To see any output, you also have to enable heading +/// [numbering]($func/heading.numbering). +/// +/// The `display` method optionally takes an argument telling it how to format +/// the counter. This can be a [numbering pattern or a +/// function]($func/numbering). +/// +/// ```example +/// #set heading(numbering: "1.") +/// +/// = Introduction +/// Some text here. +/// +/// = Background +/// The current value is: +/// #counter(heading).display() +/// +/// Or in roman numerals: +/// #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 (in the case of headings for sections, +/// subsections, and so on), the `step` method 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 multiple for multiple levels). For +/// more flexibility, you can instead also give it a function that gets 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 #counter(heading).display(). +/// ``` +/// +/// ## Custom counters { #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") +/// #mine.display() \ +/// #mine.step() +/// #mine.display() \ +/// #mine.update(c => c * 3) +/// #mine.display() \ +/// ``` +/// +/// ## How to step { #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 #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). +/// +/// ## Page counter { #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. +/// ``` +/// +/// ## Time travel { #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 +/// #locate(loc => { +/// let start-val = mine.at(loc) +/// let elements = query(<intro>, loc) +/// let intro-val = mine.at( +/// elements.first().location() +/// ) +/// let final-val = mine.final(loc) +/// [Starts as: #start-val \ +/// Value at intro is: #intro-val \ +/// Final value is: #final-val \ ] +/// }) +/// +/// #mine.update(n => n + 3) +/// +/// = Introduction <intro> +/// #lorem(10) +/// +/// #mine.step() +/// #mine.step() +/// ``` +/// +/// Let's dissect what happens in the example above: +/// +/// - We call [`locate`]($func/locate) to get access to the current location in +/// the document. We then pass this location to our counter's `at` method to +/// get its value at the current location. The `at` method always returns an +/// array because counters can have multiple levels. As the counter starts at +/// one, the first value is thus `{(1,)}`. +/// +/// - We now [`query`]($func/query) the document for all elements with the +/// `{<intro>}` label. The result is an array from which we extract the first +/// (and only) element's [location]($type/content.location). We then look up +/// the value of the counter at that location. The first update to the counter +/// sets it to `{1 + 3 = 4}`. At the introduction heading, the value is thus +/// `{(4,)}`. +/// +/// - Last but not least, we call the `final` method on the counter. It tells us +/// what the counter's value will be at the end of the document. We also need +/// to give it a location to prove that we are inside of a `locate` call, but +/// which one doesn't matter. After the heading follow two calls to `step()`, +/// so the final value is `{(6,)}`. +/// +/// ## Other kinds of state { #other-state } +/// The `counter` function is closely related to [state]($func/state) function. +/// Read its documentation for more details on state management in Typst and +/// why it doesn't just use normal variables for counters. +/// +/// ## Methods +/// ### display() +/// Displays the value of the counter. +/// +/// - numbering: string or function (positional) +/// A [numbering pattern or a function]($func/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]($type/arguments). +/// +/// If this is omitted, displays the counter with the numbering style for the +/// counted element or with the pattern `{"1.1"}` if no such style exists. +/// +/// - both: boolean (named) +/// 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. +/// +/// - returns: content +/// +/// ### step() +/// 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. +/// +/// - level: integer (named) +/// The depth at which to step the counter. Defaults to `{1}`. +/// +/// - returns: content +/// +/// ### update() +/// Updates the value of the counter. +/// +/// Just like with `step`, the update only occurs if you put the resulting +/// content into the document. +/// +/// - value: integer or array or function (positional, required) +/// 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). +/// +/// - returns: content +/// +/// ### at() +/// Gets the value of the counter at the given location. Always returns an +/// array of integers, even if the counter has just one number. +/// +/// - location: location (positional, required) +/// The location at which the counter value should be retrieved. A suitable +/// location can be retrieved from [`locate`]($func/locate) or +/// [`query`]($func/query). +/// +/// - returns: array +/// +/// ### final() +/// Gets 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. +/// +/// - location: location (positional, required) +/// Can be any location. Why is it required then? Typst has to evaluate parts +/// of your code multiple times to determine all counter values. By only +/// allowing this method within [`locate`]($func/locate) calls, the amount of +/// code that can depend on the method's result is reduced. If you could call +/// `final` directly at the top level of a module, the evaluation of the whole +/// module and its exports could depend on the counter's value. +/// +/// - returns: array +/// +/// Display: Counter +/// Category: meta +#[func] +pub fn counter( + /// The key that identifies this counter. + /// + /// - If it is a string, creates a custom counter that is only affected by + /// manual updates, + /// - If this is a `{<label>}`, counts through all elements with that label, + /// - If this is an element function or selector, counts through its elements, + /// - If this is the [`page`]($func/page) function, counts through pages. + key: CounterKey, +) -> Counter { + Counter::new(key) +} + +/// Counts through pages, elements, and more. +#[derive(Clone, PartialEq, Hash)] +pub struct Counter(CounterKey); + +impl Counter { + /// Create a new counter from a key. + pub fn new(key: CounterKey) -> Self { + Self(key) + } + + /// The counter for the given element. + pub fn of(func: ElemFunc) -> Self { + Self::new(CounterKey::Selector(Selector::Elem(func, None))) + } + + /// Call a method on counter. + #[tracing::instrument(skip(vm))] + pub fn call_method( + self, + vm: &mut Vm, + method: &str, + mut args: Args, + span: Span, + ) -> SourceResult<Value> { + let value = match method { + "display" => self + .display(args.eat()?, args.named("both")?.unwrap_or(false)) + .into_value(), + "step" => self + .update(CounterUpdate::Step( + args.named("level")?.unwrap_or(NonZeroUsize::ONE), + )) + .into_value(), + "update" => self.update(args.expect("value or function")?).into_value(), + "at" => self.at(&mut vm.vt, args.expect("location")?)?.into_value(), + "final" => self.final_(&mut vm.vt, args.expect("location")?)?.into_value(), + _ => bail!(span, "type counter has no method `{}`", method), + }; + args.finish()?; + Ok(value) + } + + /// Display the current value of the counter. + pub fn display(self, numbering: Option<Numbering>, both: bool) -> Content { + DisplayElem::new(self, numbering, both).pack() + } + + /// Get the value of the state at the given location. + pub fn at(&self, vt: &mut Vt, location: Location) -> SourceResult<CounterState> { + let sequence = self.sequence(vt)?; + let offset = vt.introspector.query(&self.selector().before(location, true)).len(); + let (mut state, page) = sequence[offset].clone(); + if self.is_page() { + let delta = vt.introspector.page(location).get().saturating_sub(page.get()); + state.step(NonZeroUsize::ONE, delta); + } + + Ok(state) + } + + /// Get the value of the state at the final location. + pub fn final_(&self, vt: &mut Vt, _: Location) -> SourceResult<CounterState> { + let sequence = self.sequence(vt)?; + let (mut state, page) = sequence.last().unwrap().clone(); + if self.is_page() { + let delta = vt.introspector.pages().get().saturating_sub(page.get()); + state.step(NonZeroUsize::ONE, delta); + } + Ok(state) + } + + /// Get the current and final value of the state combined in one state. + pub fn both(&self, vt: &mut Vt, location: Location) -> SourceResult<CounterState> { + let sequence = self.sequence(vt)?; + let offset = vt + .introspector + .query(&Selector::before(self.selector(), location, true)) + .len(); + 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 = + vt.introspector.page(location).get().saturating_sub(at_page.get()); + at_state.step(NonZeroUsize::ONE, at_delta); + let final_delta = + vt.introspector.pages().get().saturating_sub(final_page.get()); + final_state.step(NonZeroUsize::ONE, final_delta); + } + Ok(CounterState(smallvec![at_state.first(), final_state.first()])) + } + + /// Produce content that performs a state update. + pub fn update(self, update: CounterUpdate) -> Content { + UpdateElem::new(self, update).pack() + } + + /// 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, + vt: &mut Vt, + ) -> SourceResult<EcoVec<(CounterState, NonZeroUsize)>> { + self.sequence_impl( + vt.world, + vt.introspector, + vt.locator.track(), + TrackedMut::reborrow_mut(&mut vt.delayed), + TrackedMut::reborrow_mut(&mut vt.tracer), + ) + } + + /// Memoized implementation of `sequence`. + #[comemo::memoize] + fn sequence_impl( + &self, + world: Tracked<dyn World + '_>, + introspector: Tracked<Introspector>, + locator: Tracked<Locator>, + delayed: TrackedMut<DelayedErrors>, + tracer: TrackedMut<Tracer>, + ) -> SourceResult<EcoVec<(CounterState, NonZeroUsize)>> { + let mut locator = Locator::chained(locator); + let mut vt = Vt { + world, + introspector, + locator: &mut locator, + delayed, + tracer, + }; + let mut state = CounterState(match &self.0 { + // special case, because pages always start at one. + CounterKey::Page => smallvec![1], + _ => smallvec![0], + }); + 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.to::<UpdateElem>() { + Some(elem) => Some(elem.update()), + None => match elem.with::<dyn Count>() { + Some(countable) => countable.update(), + None => Some(CounterUpdate::Step(NonZeroUsize::ONE)), + }, + } { + state.update(&mut vt, update)?; + } + + stops.push((state.clone(), page)); + } + + Ok(stops) + } + + /// The selector relevant for this counter's updates. + fn selector(&self) -> Selector { + let mut selector = + Selector::Elem(UpdateElem::func(), Some(dict! { "counter" => self.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 + } +} + +impl Debug for Counter { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("counter(")?; + self.0.fmt(f)?; + f.write_char(')') + } +} + +cast! { + type Counter: "counter", +} + +/// Identifies a counter. +#[derive(Clone, PartialEq, Hash)] +pub enum CounterKey { + /// The page counter. + Page, + /// Counts elements matching the given selectors. Only works for locatable + /// elements or labels. + Selector(Selector), + /// Counts through manual counters with the same key. + Str(Str), +} + +cast! { + CounterKey, + v: Str => Self::Str(v), + label: Label => Self::Selector(Selector::Label(label)), + v: ElemFunc => { + if v == PageElem::func() { + Self::Page + } else { + Self::Selector(LocatableSelector::from_value(v.into_value())?.0) + } + }, + selector: LocatableSelector => Self::Selector(selector.0), +} + +impl Debug for CounterKey { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Page => f.pad("page"), + Self::Selector(selector) => selector.fmt(f), + Self::Str(str) => str.fmt(f), + } + } +} + +/// An update to perform on a counter. +#[derive(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), +} + +impl Debug for CounterUpdate { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("..") + } +} + +cast! { + type CounterUpdate: "counter update", + 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 { + /// Advance the counter and return the numbers for the given heading. + pub fn update(&mut self, vt: &mut Vt, update: CounterUpdate) -> SourceResult<()> { + match update { + CounterUpdate::Set(state) => *self = state, + CounterUpdate::Step(level) => self.step(level, 1), + CounterUpdate::Func(func) => { + *self = func.call_vt(vt, 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(); + + if self.0.len() >= level { + self.0[level - 1] = self.0[level - 1].saturating_add(by); + self.0.truncate(level); + } + + while self.0.len() < level { + self.0.push(1); + } + } + + /// 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, vt: &mut Vt, numbering: &Numbering) -> SourceResult<Content> { + Ok(numbering.apply_vt(vt, &self.0)?.display()) + } +} + +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::<StrResult<_>>()?), +} + +/// Executes a display of a state. +/// +/// Display: State +/// Category: special +#[element(Locatable, Show)] +struct DisplayElem { + /// The counter. + #[required] + counter: Counter, + + /// The numbering to display the counter with. + #[required] + numbering: Option<Numbering>, + + /// Whether to display both the current and final value. + #[required] + both: bool, +} + +impl Show for DisplayElem { + #[tracing::instrument(name = "DisplayElem::show", skip_all)] + fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + Ok(vt.delayed(|vt| { + let location = self.0.location().unwrap(); + let counter = self.counter(); + let numbering = self + .numbering() + .or_else(|| { + let CounterKey::Selector(Selector::Elem(func, _)) = counter.0 else { + return None; + }; + + if func == HeadingElem::func() { + HeadingElem::numbering_in(styles) + } else if func == FigureElem::func() { + FigureElem::numbering_in(styles) + } else if func == EquationElem::func() { + EquationElem::numbering_in(styles) + } else { + None + } + }) + .unwrap_or_else(|| NumberingPattern::from_str("1.1").unwrap().into()); + + let state = if self.both() { + counter.both(vt, location)? + } else { + counter.at(vt, location)? + }; + + state.display(vt, &numbering) + })) + } +} + +/// Executes a display of a state. +/// +/// Display: State +/// Category: special +#[element(Locatable, Show)] +struct UpdateElem { + /// The counter. + #[required] + counter: Counter, + + /// The update to perform on the counter. + #[required] + update: CounterUpdate, +} + +impl Show for UpdateElem { + #[tracing::instrument(name = "UpdateElem::show", skip(self))] + fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> { + Ok(Content::empty()) + } +} diff --git a/crates/typst-library/src/meta/document.rs b/crates/typst-library/src/meta/document.rs new file mode 100644 index 00000000..db036e0a --- /dev/null +++ b/crates/typst-library/src/meta/document.rs @@ -0,0 +1,86 @@ +use crate::layout::{LayoutRoot, PageElem}; +use crate::prelude::*; + +/// 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 appear before any of the document's contents. +/// +/// ```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. +/// +/// Display: Document +/// Category: meta +#[element(Construct, LayoutRoot)] +pub struct DocumentElem { + /// The document's title. This is often rendered as the title of the + /// PDF viewer window. + pub title: Option<EcoString>, + + /// The document's authors. + pub author: Author, + + /// The page runs. + #[internal] + #[variadic] + pub children: Vec<Content>, +} + +impl Construct for DocumentElem { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { + bail!(args.span, "can only be used in set rules") + } +} + +impl LayoutRoot for DocumentElem { + /// Layout the document into a sequence of frames, one per page. + #[tracing::instrument(name = "DocumentElem::layout_root", skip_all)] + fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document> { + tracing::info!("Document layout"); + + let mut pages = vec![]; + + for mut child in &self.children() { + let outer = styles; + let mut styles = styles; + if let Some((elem, local)) = child.to_styled() { + styles = outer.chain(local); + child = elem; + } + + if let Some(page) = child.to::<PageElem>() { + let number = NonZeroUsize::ONE.saturating_add(pages.len()); + let fragment = page.layout(vt, styles, number)?; + pages.extend(fragment); + } else { + bail!(child.span(), "unexpected document child"); + } + } + + Ok(Document { + pages, + title: self.title(styles), + author: self.author(styles).0, + }) + } +} + +/// A list of authors. +#[derive(Debug, Default, Clone, 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::<StrResult<_>>()?), +} diff --git a/crates/typst-library/src/meta/figure.rs b/crates/typst-library/src/meta/figure.rs new file mode 100644 index 00000000..0d218770 --- /dev/null +++ b/crates/typst-library/src/meta/figure.rs @@ -0,0 +1,351 @@ +use std::str::FromStr; + +use super::{ + Count, Counter, CounterKey, CounterUpdate, LocalName, Numbering, NumberingPattern, +}; +use crate::layout::{BlockElem, VElem}; +use crate::meta::{Outlinable, Refable, Supplement}; +use crate::prelude::*; +use crate::text::TextElem; +use crate::visualize::ImageElem; + +/// A figure with an optional caption. +/// +/// Automatically detects its contents to select the correct counting track. +/// For example, figures containing images will be numbered separately from +/// figures containing tables. +/// +/// ## Examples { #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]($func/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. +/// +/// ## Modifying the appearance { #modifying-appearance } +/// You can completely customize the look of your figures with a [show +/// rule]($styling/#show-rules). In the example below, we show the figure's +/// caption above its body and display its supplement and counter after the +/// caption. +/// +/// ```example +/// #show figure: it => align(center)[ +/// #it.caption | +/// #emph[ +/// #it.supplement +/// #it.counter.display(it.numbering) +/// ] +/// #v(10pt, weak: true) +/// #it.body +/// ] +/// +/// #figure( +/// image("molecular.jpg", width: 80%), +/// caption: [ +/// The molecular testing pipeline. +/// ], +/// ) +/// ``` +/// +/// 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 breakable +/// across pages as well by using `#show figure: set block(breakable: true)` +/// (see the [block]($func/block) documentation for more information). +/// +/// Display: Figure +/// Category: meta +#[element(Locatable, Synthesize, Count, Show, Finalize, Refable, Outlinable)] +pub struct FigureElem { + /// The content of the figure. Often, an [image]($func/image). + #[required] + pub body: Content, + + /// The figure's caption. + pub caption: Option<Content>, + + /// The kind of the figure this is. + /// + /// If set to `{auto}`, the figure will try to automatically determine its + /// kind. All figures of the same kind share a common counter. + /// + /// 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]($func/image), a [table]($func/table) or [code]($func/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 that is not supported by the figure, you will + /// need to manually specify the figure's supplement. + /// + /// ```example + /// #figure( + /// circle(radius: 10pt), + /// caption: [A curious atom.], + /// kind: "atom", + /// supplement: [Atom], + /// ) + /// ``` + #[default(Smart::Auto)] + 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]($func/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", + /// ) + /// ``` + pub supplement: Smart<Option<Supplement>>, + + /// How to number the figure. Accepts a + /// [numbering pattern or function]($func/numbering). + #[default(Some(NumberingPattern::from_str("1").unwrap().into()))] + 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`]($func/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)[$func/table]: `{counter(figure.where(kind: table))}` + /// - For (images)[$func/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>, +} + +impl Synthesize for FigureElem { + fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> { + let numbering = self.numbering(styles); + + // Determine the figure's kind. + let kind = self.kind(styles).unwrap_or_else(|| { + self.body() + .query_first(Selector::can::<dyn Figurable>()) + .cloned() + .map(|elem| FigureKind::Elem(elem.func())) + .unwrap_or_else(|| FigureKind::Elem(ImageElem::func())) + }); + + // Resolve the supplement. + let supplement = match self.supplement(styles) { + Smart::Auto => { + // Default to the local name for the kind, if available. + let name = match &kind { + FigureKind::Elem(func) => { + let empty = Content::new(*func); + empty.with::<dyn LocalName>().map(|c| { + TextElem::packed(c.local_name( + TextElem::lang_in(styles), + TextElem::region_in(styles), + )) + }) + } + FigureKind::Name(_) => None, + }; + + if numbering.is_some() && name.is_none() { + bail!(self.span(), "please specify the figure's supplement") + } + + name.unwrap_or_default() + } + Smart::Custom(None) => Content::empty(), + 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) => { + self.body().query_first(Selector::Elem(func, None)).cloned() + } + FigureKind::Name(_) => None, + }; + + let target = descendant.unwrap_or_else(|| self.body()); + supplement.resolve(vt, [target])? + } + }; + + // Construct the figure's counter. + let counter = Counter::new(CounterKey::Selector(Selector::Elem( + Self::func(), + Some(dict! { + "kind" => kind.clone(), + }), + ))); + + self.push_caption(self.caption(styles)); + self.push_kind(Smart::Custom(kind)); + self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement)))); + self.push_numbering(numbering); + self.push_outlined(self.outlined(styles)); + self.push_counter(Some(counter)); + + Ok(()) + } +} + +impl Show for FigureElem { + #[tracing::instrument(name = "FigureElem::show", skip_all)] + fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + let mut realized = self.body(); + + // Build the caption, if any. + if let Some(caption) = self.full_caption(vt)? { + realized += VElem::weak(self.gap(styles).into()).pack(); + realized += caption; + } + + // Wrap the contents in a block. + Ok(BlockElem::new() + .with_body(Some(realized)) + .pack() + .aligned(Axes::with_x(Some(Align::Center.into())))) + } +} + +impl Finalize for FigureElem { + fn finalize(&self, realized: Content, _: StyleChain) -> Content { + // Allow breakable figures with `show figure: set block(breakable: true)`. + realized.styled(BlockElem::set_breakable(false)) + } +} + +impl Count for 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(StyleChain::default()) + .is_some() + .then(|| CounterUpdate::Step(NonZeroUsize::ONE)) + } +} + +impl Refable for FigureElem { + 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 { + self.counter().unwrap_or_else(|| Counter::of(Self::func())) + } + + fn numbering(&self) -> Option<Numbering> { + self.numbering(StyleChain::default()) + } +} + +impl Outlinable for FigureElem { + fn outline(&self, vt: &mut Vt) -> SourceResult<Option<Content>> { + if !self.outlined(StyleChain::default()) { + return Ok(None); + } + + self.full_caption(vt) + } +} + +impl FigureElem { + /// Builds the full caption for the figure (with supplement and numbering). + pub fn full_caption(&self, vt: &mut Vt) -> SourceResult<Option<Content>> { + let Some(mut caption) = self.caption(StyleChain::default()) else { + return Ok(None); + }; + + if let ( + Smart::Custom(Some(Supplement::Content(mut supplement))), + Some(counter), + Some(numbering), + ) = ( + self.supplement(StyleChain::default()), + self.counter(), + self.numbering(StyleChain::default()), + ) { + let loc = self.0.location().unwrap(); + let numbers = counter.at(vt, loc)?.display(vt, &numbering)?; + + if !supplement.is_empty() { + supplement += TextElem::packed("\u{a0}"); + } + + caption = supplement + numbers + TextElem::packed(": ") + caption; + } + + Ok(Some(caption)) + } +} + +/// The `kind` parameter of a [`FigureElem`]. +#[derive(Debug, Clone)] +pub enum FigureKind { + /// The kind is an element function. + Elem(ElemFunc), + /// 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: ElemFunc => 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: LocalName {} diff --git a/crates/typst-library/src/meta/footnote.rs b/crates/typst-library/src/meta/footnote.rs new file mode 100644 index 00000000..31ec9fe9 --- /dev/null +++ b/crates/typst-library/src/meta/footnote.rs @@ -0,0 +1,299 @@ +use comemo::Prehashed; +use std::str::FromStr; + +use super::{Counter, Numbering, NumberingPattern}; +use crate::layout::{HElem, ParElem}; +use crate::meta::{Count, CounterUpdate}; +use crate::prelude::*; +use crate::text::{SuperElem, TextElem, TextSize}; +use crate::visualize::LineElem; + +/// The body of a footnote can be either some content or a label referencing +/// another footnote. +#[derive(Debug)] +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), +} + +/// 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`]($func/footnote.entry). The footnote itself is realized as +/// a normal superscript, so you can use a set rule on the +/// [`super`]($func/super) function to customize it. +/// +/// ## Example { #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]($func/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] more information. +/// +/// [issue]: https://github.com/typst/typst/issues/1467#issuecomment-1588799440 +/// +/// Display: Footnote +/// Category: meta +#[element(Locatable, Synthesize, Show, Count)] +#[scope( + scope.define("entry", FootnoteEntry::func()); + scope +)] +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]($func/counter) in the page [header]($func/page.header). In the + /// future, there might be a simpler way to achieve this. + /// + /// ```example + /// #set footnote(numbering: "*") + /// + /// Footnotes: + /// #footnote[Star], + /// #footnote[Dagger] + /// ``` + #[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, +} + +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)) + } + + /// 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, + } + } + + /// Returns the location of the definition of this footnote. + pub fn declaration_location(&self, vt: &Vt) -> StrResult<Location> { + match self.body() { + FootnoteBody::Reference(label) => { + let element: Prehashed<Content> = vt.introspector.query_label(&label)?; + let footnote = element + .to::<FootnoteElem>() + .ok_or("referenced element should be a footnote")?; + footnote.declaration_location(vt) + } + _ => Ok(self.0.location().unwrap()), + } + } +} + +impl Synthesize for FootnoteElem { + fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> { + self.push_numbering(self.numbering(styles)); + Ok(()) + } +} + +impl Show for FootnoteElem { + #[tracing::instrument(name = "FootnoteElem::show", skip_all)] + fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + Ok(vt.delayed(|vt| { + let loc = self.declaration_location(vt).at(self.span())?; + let numbering = self.numbering(styles); + let counter = Counter::of(Self::func()); + let num = counter.at(vt, loc)?.display(vt, &numbering)?; + let sup = SuperElem::new(num).pack(); + let hole = HElem::new(Abs::zero().into()).with_weak(true).pack(); + let loc = loc.variant(1); + Ok(hole + sup.linked(Destination::Location(loc))) + })) + } +} + +impl Count for FootnoteElem { + fn update(&self) -> Option<CounterUpdate> { + (!self.is_ref()).then(|| CounterUpdate::Step(NonZeroUsize::ONE)) + } +} + +/// 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 { #example } +/// ```example +/// #show footnote.entry: set text(red) +/// +/// My footnote listing +/// #footnote[It's down here] +/// has red text! +/// ``` +/// +/// Display: Footnote Entry +/// Category: meta +#[element(Show, Finalize)] +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: 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(PartialStroke { + 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 FootnoteEntry { + fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + let note = self.note(); + let number_gap = Em::new(0.05); + let numbering = note.numbering(StyleChain::default()); + let counter = Counter::of(FootnoteElem::func()); + let loc = note.0.location().unwrap(); + let num = counter.at(vt, loc)?.display(vt, &numbering)?; + let sup = SuperElem::new(num) + .pack() + .linked(Destination::Location(loc)) + .backlinked(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(), + ])) + } +} + +impl Finalize for FootnoteEntry { + fn finalize(&self, realized: Content, _: StyleChain) -> Content { + let text_size = Em::new(0.85); + let leading = Em::new(0.5); + realized + .styled(ParElem::set_leading(leading.into())) + .styled(TextElem::set_size(TextSize(text_size.into()))) + } +} + +cast! { + FootnoteElem, + v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::with_content(v.clone())), +} diff --git a/crates/typst-library/src/meta/heading.rs b/crates/typst-library/src/meta/heading.rs new file mode 100644 index 00000000..d70bc0a9 --- /dev/null +++ b/crates/typst-library/src/meta/heading.rs @@ -0,0 +1,239 @@ +use typst::font::FontWeight; +use typst::util::option_eq; + +use super::{Counter, CounterUpdate, LocalName, Numbering, Outlinable, Refable}; +use crate::layout::{BlockElem, HElem, VElem}; +use crate::meta::{Count, Supplement}; +use crate::prelude::*; +use crate::text::{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]($func/numbering). +/// +/// Independently from the numbering, Typst can also automatically generate an +/// [outline]($func/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 } +/// ```example +/// #set heading(numbering: "1.a)") +/// +/// = Introduction +/// In recent years, ... +/// +/// == Preliminaries +/// To start, ... +/// ``` +/// +/// ## Syntax { #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. +/// +/// Display: Heading +/// Category: meta +#[element(Locatable, Synthesize, Count, Show, Finalize, LocalName, Refable, Outlinable)] +pub struct HeadingElem { + /// The logical nesting depth of the heading, starting from one. + #[default(NonZeroUsize::ONE)] + pub level: NonZeroUsize, + + /// How to number the heading. Accepts a + /// [numbering pattern or function]($func/numbering). + /// + /// ```example + /// #set heading(numbering: "1.a.") + /// + /// = A section + /// == A subsection + /// === A sub-subsection + /// ``` + 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. + /// + /// ```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, + + /// The heading's title. + #[required] + pub body: Content, +} + +impl Synthesize for HeadingElem { + fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> { + // Resolve the supplement. + 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(vt, [self.clone()])?, + }; + + self.push_level(self.level(styles)); + self.push_numbering(self.numbering(styles)); + self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement)))); + self.push_outlined(self.outlined(styles)); + + Ok(()) + } +} + +impl Show for HeadingElem { + #[tracing::instrument(name = "HeadingElem::show", skip_all)] + fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + let mut realized = self.body(); + if let Some(numbering) = self.numbering(styles) { + realized = Counter::of(Self::func()) + .display(Some(numbering), false) + .spanned(self.span()) + + HElem::new(Em::new(0.3).into()).with_weak(true).pack() + + realized; + } + Ok(BlockElem::new().with_body(Some(realized)).pack()) + } +} + +impl Finalize for HeadingElem { + fn finalize(&self, realized: Content, styles: StyleChain) -> Content { + let level = self.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 styles = Styles::new(); + styles.set(TextElem::set_size(TextSize(size.into()))); + styles.set(TextElem::set_weight(FontWeight::BOLD)); + styles.set(BlockElem::set_above(VElem::block_around(above.into()))); + styles.set(BlockElem::set_below(VElem::block_around(below.into()))); + styles.set(BlockElem::set_sticky(true)); + realized.styled_with_map(styles) + } +} + +impl Count for HeadingElem { + fn update(&self) -> Option<CounterUpdate> { + self.numbering(StyleChain::default()) + .is_some() + .then(|| CounterUpdate::Step(self.level(StyleChain::default()))) + } +} + +cast! { + HeadingElem, + v: Content => v.to::<Self>().ok_or("expected heading")?.clone(), +} + +impl Refable for 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(Self::func()) + } + + fn numbering(&self) -> Option<Numbering> { + self.numbering(StyleChain::default()) + } +} + +impl Outlinable for HeadingElem { + fn outline(&self, vt: &mut Vt) -> SourceResult<Option<Content>> { + if !self.outlined(StyleChain::default()) { + return Ok(None); + } + + let mut content = self.body(); + if let Some(numbering) = self.numbering(StyleChain::default()) { + let numbers = Counter::of(Self::func()) + .at(vt, self.0.location().unwrap())? + .display(vt, &numbering)?; + content = numbers + SpaceElem::new().pack() + content; + }; + + Ok(Some(content)) + } + + fn level(&self) -> NonZeroUsize { + self.level(StyleChain::default()) + } +} + +impl LocalName for HeadingElem { + fn local_name(&self, lang: Lang, region: Option<Region>) -> &'static str { + match lang { + Lang::ALBANIAN => "Kapitull", + Lang::ARABIC => "الفصل", + Lang::BOKMÅL => "Kapittel", + Lang::CHINESE if option_eq(region, "TW") => "小節", + Lang::CHINESE => "小节", + Lang::CZECH => "Kapitola", + Lang::DANISH => "Afsnit", + Lang::DUTCH => "Hoofdstuk", + Lang::FILIPINO => "Seksyon", + Lang::FRENCH => "Chapitre", + Lang::GERMAN => "Abschnitt", + Lang::ITALIAN => "Sezione", + Lang::NYNORSK => "Kapittel", + Lang::POLISH => "Sekcja", + Lang::PORTUGUESE if option_eq(region, "PT") => "Secção", + Lang::PORTUGUESE => "Seção", + Lang::RUSSIAN => "Раздел", + Lang::SLOVENIAN => "Poglavje", + Lang::SPANISH => "Sección", + Lang::SWEDISH => "Kapitel", + Lang::TURKISH => "Bölüm", + Lang::UKRAINIAN => "Розділ", + Lang::VIETNAMESE => "Phần", // TODO: This may be wrong. + Lang::ENGLISH | _ => "Section", + } + } +} diff --git a/crates/typst-library/src/meta/link.rs b/crates/typst-library/src/meta/link.rs new file mode 100644 index 00000000..2a53b84f --- /dev/null +++ b/crates/typst-library/src/meta/link.rs @@ -0,0 +1,137 @@ +use crate::prelude::*; +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 } +/// ```example +/// #show link: underline +/// +/// https://example.com \ +/// +/// #link("https://example.com") \ +/// #link("https://example.com")[ +/// See example.com +/// ] +/// ``` +/// +/// ## Syntax { #syntax } +/// This function also has dedicated syntax: Text that starts with `http://` or +/// `https://` is automatically turned into a link. +/// +/// Display: Link +/// Category: meta +#[element(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]($func/label) attached to an element. If you also want + /// automatic text for the link based on the element, consider using + /// a [reference]($func/ref) instead. + /// + /// - A [location]($func/locate) resulting from a [`locate`]($func/locate) + /// call or [`query`]($func/query). + /// + /// - A dictionary with a `page` key of type [integer]($type/integer) and + /// `x` and `y` coordinates of type [length]($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, +} + +impl LinkElem { + /// Create a link element from a URL with its bare text. + pub fn from_url(url: EcoString) -> Self { + let body = body_from_url(&url); + Self::new(LinkTarget::Dest(Destination::Url(url)), body) + } +} + +impl Show for LinkElem { + #[tracing::instrument(name = "LinkElem::show", skip(self, vt))] + fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> { + let body = self.body(); + let linked = match self.dest() { + LinkTarget::Dest(dest) => body.linked(dest), + LinkTarget::Label(label) => vt + .delayed(|vt| { + let elem = vt.introspector.query_label(&label).at(self.span())?; + let dest = Destination::Location(elem.location().unwrap()); + Ok(Some(body.clone().linked(dest))) + }) + .unwrap_or(body), + }; + + Ok(linked.styled(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false))))) + } +} + +fn body_from_url(url: &EcoString) -> 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)] +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) + } +} diff --git a/crates/typst-library/src/meta/mod.rs b/crates/typst-library/src/meta/mod.rs new file mode 100644 index 00000000..dcac6379 --- /dev/null +++ b/crates/typst-library/src/meta/mod.rs @@ -0,0 +1,64 @@ +//! Interaction between document parts. + +mod bibliography; +mod context; +mod counter; +mod document; +mod figure; +mod footnote; +mod heading; +mod link; +mod numbering; +mod outline; +mod query; +mod reference; +mod state; + +pub use self::bibliography::*; +pub use self::context::*; +pub use self::counter::*; +pub use self::document::*; +pub use self::figure::*; +pub use self::footnote::*; +pub use self::heading::*; +pub use self::link::*; +pub use self::numbering::*; +pub use self::outline::*; +pub use self::query::*; +pub use self::reference::*; +pub use self::state::*; + +use crate::prelude::*; +use crate::text::TextElem; + +/// Hook up all meta definitions. +pub(super) fn define(global: &mut Scope) { + global.define("document", DocumentElem::func()); + global.define("ref", RefElem::func()); + global.define("link", LinkElem::func()); + global.define("outline", OutlineElem::func()); + global.define("heading", HeadingElem::func()); + global.define("figure", FigureElem::func()); + global.define("footnote", FootnoteElem::func()); + global.define("cite", CiteElem::func()); + global.define("bibliography", BibliographyElem::func()); + global.define("locate", locate_func()); + global.define("style", style_func()); + global.define("layout", layout_func()); + global.define("counter", counter_func()); + global.define("numbering", numbering_func()); + global.define("state", state_func()); + global.define("query", query_func()); + global.define("selector", selector_func()); +} + +/// The named with which an element is referenced. +pub trait LocalName { + /// Get the name in the given language and (optionally) region. + fn local_name(&self, lang: Lang, region: Option<Region>) -> &'static str; + + /// Resolve the local name with a style chain. + fn local_name_in(&self, styles: StyleChain) -> &'static str { + self.local_name(TextElem::lang_in(styles), TextElem::region_in(styles)) + } +} diff --git a/crates/typst-library/src/meta/numbering.rs b/crates/typst-library/src/meta/numbering.rs new file mode 100644 index 00000000..8698f7b9 --- /dev/null +++ b/crates/typst-library/src/meta/numbering.rs @@ -0,0 +1,525 @@ +use std::str::FromStr; + +use chinese_number::{ChineseCase, ChineseCountMethod, ChineseVariant, NumberToChinese}; +use ecow::EcoVec; + +use crate::prelude::*; +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 } +/// ```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, +/// ) +/// ``` +/// +/// Display: Numbering +/// Category: meta +#[func] +pub fn numbering( + /// Defines how the numbering works. + /// + /// **Counting symbols** are `1`, `a`, `A`, `i`, `I`, `い`, `イ`, `א`, `가`, + /// `ㄱ`, and `*`. They are replaced by the number in the sequence, in the + /// given case. + /// + /// The `*` character means that symbols should be used to count, in the + /// order of `*`, `†`, `‡`, `§`, `¶`, and `‖`. If there are more than six + /// items, the number is represented using multiple 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>, + /// The virtual machine. + vm: &mut Vm, +) -> SourceResult<Value> { + numbering.apply_vm(vm, &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_vm(&self, vm: &mut Vm, numbers: &[usize]) -> SourceResult<Value> { + Ok(match self { + Self::Pattern(pattern) => Value::Str(pattern.apply(numbers).into()), + Self::Func(func) => { + let args = Args::new(func.span(), numbers.iter().copied()); + func.call_vm(vm, args)? + } + }) + } + + /// Apply the pattern to the given numbers. + pub fn apply_vt(&self, vt: &mut Vt, numbers: &[usize]) -> SourceResult<Value> { + Ok(match self { + Self::Pattern(pattern) => Value::Str(pattern.apply(numbers).into()), + Self::Func(func) => func.call_vt(vt, 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 { + pieces: EcoVec<(EcoString, NumberingKind, Case)>, + 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, case), &n)) in + self.pieces.iter().zip(&mut numbers).enumerate() + { + if i > 0 || !self.trimmed { + fmt.push_str(prefix); + } + fmt.push_str(&kind.apply(n, *case)); + } + + for ((prefix, kind, case), &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, *case)); + } + + 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, case)) = self + .pieces + .iter() + .chain(self.pieces.last().into_iter().cycle()) + .nth(k) + { + fmt.push_str(&kind.apply(number, *case)); + } + 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.to_ascii_lowercase()) else { + continue; + }; + + let prefix = pattern[handled..i].into(); + let case = + if c.is_uppercase() || c == '壹' { Case::Upper } else { Case::Lower }; + pieces.push((prefix, kind, case)); + 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, case) in &self.pieces { + pat.push_str(prefix); + let mut c = kind.to_char(); + if *case == Case::Upper { + c = c.to_ascii_uppercase(); + } + pat.push(c); + } + pat.push_str(&self.suffix); + pat.into_value() + }, + v: Str => v.parse()?, +} + +/// Different kinds of numberings. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +enum NumberingKind { + Arabic, + Letter, + Roman, + Symbol, + Hebrew, + SimplifiedChinese, + // TODO: Pick the numbering pattern based on languages choice. + // As the `1st` numbering character of Chinese (Simplified) and + // Chinese (Traditional) is same, we are unable to determine + // if the context is Simplified or Traditional by only this + // character. + #[allow(unused)] + TraditionalChinese, + HiraganaIroha, + KatakanaIroha, + KoreanJamo, + KoreanSyllable, +} + +impl NumberingKind { + /// Create a numbering kind from a lowercase character. + pub fn from_char(c: char) -> Option<Self> { + Some(match c { + '1' => NumberingKind::Arabic, + 'a' => NumberingKind::Letter, + 'i' => NumberingKind::Roman, + '*' => NumberingKind::Symbol, + 'א' => NumberingKind::Hebrew, + '一' | '壹' => NumberingKind::SimplifiedChinese, + 'い' => NumberingKind::HiraganaIroha, + 'イ' => NumberingKind::KatakanaIroha, + 'ㄱ' => NumberingKind::KoreanJamo, + '가' => NumberingKind::KoreanSyllable, + _ => return None, + }) + } + + /// The lowercase character for this numbering kind. + pub fn to_char(self) -> char { + match self { + Self::Arabic => '1', + Self::Letter => 'a', + Self::Roman => 'i', + Self::Symbol => '*', + Self::Hebrew => 'א', + Self::SimplifiedChinese => '一', + Self::TraditionalChinese => '一', + Self::HiraganaIroha => 'い', + Self::KatakanaIroha => 'イ', + Self::KoreanJamo => 'ㄱ', + Self::KoreanSyllable => '가', + } + } + + /// Apply the numbering to the given number. + pub fn apply(self, mut n: usize, case: Case) -> EcoString { + match self { + Self::Arabic => { + eco_format!("{n}") + } + Self::Letter => zeroless::<26>( + |x| match case { + Case::Lower => char::from(b'a' + x as u8), + Case::Upper => char::from(b'A' + x as u8), + }, + n, + ), + Self::HiraganaIroha => zeroless::<47>( + |x| { + [ + 'い', 'ろ', 'は', 'に', 'ほ', 'へ', 'と', 'ち', 'り', 'ぬ', 'る', + 'を', 'わ', 'か', 'よ', 'た', 'れ', 'そ', 'つ', 'ね', 'な', 'ら', + 'む', 'う', 'ゐ', 'の', 'お', 'く', 'や', 'ま', 'け', 'ふ', 'こ', + 'え', 'て', 'あ', 'さ', 'き', 'ゆ', 'め', 'み', 'し', 'ゑ', 'ひ', + 'も', 'せ', 'す', + ][x] + }, + n, + ), + Self::KatakanaIroha => zeroless::<47>( + |x| { + [ + 'イ', 'ロ', 'ハ', 'ニ', 'ホ', 'ヘ', 'ト', 'チ', 'リ', 'ヌ', 'ル', + 'ヲ', 'ワ', 'カ', 'ヨ', 'タ', 'レ', 'ソ', 'ツ', 'ネ', 'ナ', 'ラ', + 'ム', 'ウ', 'ヰ', 'ノ', 'オ', 'ク', 'ヤ', 'マ', 'ケ', 'フ', 'コ', + 'エ', 'テ', 'ア', 'サ', 'キ', 'ユ', 'メ', 'ミ', 'シ', 'ヱ', 'ヒ', + 'モ', 'セ', 'ス', + ][x] + }, + n, + ), + Self::Roman => { + if n == 0 { + return '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 + } + 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 => { + 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 + } + l @ (Self::SimplifiedChinese | Self::TraditionalChinese) => { + let chinese_case = match case { + Case::Lower => ChineseCase::Lower, + Case::Upper => ChineseCase::Upper, + }; + + match (n as u8).to_chinese( + match l { + Self::SimplifiedChinese => ChineseVariant::Simple, + Self::TraditionalChinese => ChineseVariant::Traditional, + _ => unreachable!(), + }, + chinese_case, + ChineseCountMethod::TenThousand, + ) { + Ok(num_str) => EcoString::from(num_str), + Err(_) => '-'.into(), + } + } + Self::KoreanJamo => zeroless::<14>( + |x| { + [ + 'ㄱ', 'ㄴ', 'ㄷ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅅ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', + 'ㅌ', 'ㅍ', 'ㅎ', + ][x] + }, + n, + ), + Self::KoreanSyllable => zeroless::<14>( + |x| { + [ + '가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카', + '타', '파', '하', + ][x] + }, + n, + ), + } + } +} + +/// 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>( + mk_digit: impl Fn(usize) -> char, + mut n: usize, +) -> EcoString { + if n == 0 { + return '-'.into(); + } + let mut cs = vec![]; + while n > 0 { + n -= 1; + cs.push(mk_digit(n % N_DIGITS)); + n /= N_DIGITS; + } + cs.into_iter().rev().collect() +} diff --git a/crates/typst-library/src/meta/outline.rs b/crates/typst-library/src/meta/outline.rs new file mode 100644 index 00000000..ba858a69 --- /dev/null +++ b/crates/typst-library/src/meta/outline.rs @@ -0,0 +1,528 @@ +use std::str::FromStr; + +use typst::util::option_eq; + +use super::{ + Counter, CounterKey, HeadingElem, LocalName, Numbering, NumberingPattern, Refable, +}; +use crate::layout::{BoxElem, HElem, HideElem, ParbreakElem, RepeatElem, Spacing}; +use crate::prelude::*; +use crate::text::{LinebreakElem, 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 } +/// ```example +/// #outline() +/// +/// = Introduction +/// #lorem(5) +/// +/// = Prior work +/// #lorem(10) +/// ``` +/// +/// ## Alternative outlines { #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]($type/content.where). +/// +/// ```example +/// #outline( +/// title: [List of Figures], +/// target: figure.where(kind: image), +/// ) +/// +/// #figure( +/// image("tiger.jpg"), +/// caption: [A nice figure!], +/// ) +/// ``` +/// +/// ## Styling the outline { #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`]($func/outline.entry) element. +/// +/// Display: Outline +/// Category: meta +/// Keywords: Table of Contents +#[element(Show, Finalize, LocalName)] +#[scope( + scope.define("entry", OutlineEntry::func()); + scope +)] +pub struct OutlineElem { + /// The title of the outline. + /// + /// - When set to `{auto}`, an appropriate title for the + /// [text language]($func/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.")}` + /// ``` + #[default(Some(Smart::Auto))] + pub title: Option<Smart<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(Selector::Elem( + HeadingElem::func(), + Some(dict! { "outlined" => true }) + )))] + 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]($func/heading.numbering)). + /// - [Relative length]($type/relative-length): 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]($type/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)] + 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>, +} + +impl Show for OutlineElem { + #[tracing::instrument(name = "OutlineElem::show", skip_all)] + fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + let mut seq = vec![ParbreakElem::new().pack()]; + // Build the outline title. + if let Some(title) = self.title(styles) { + let title = + title.unwrap_or_else(|| { + TextElem::packed(self.local_name( + TextElem::lang_in(styles), + TextElem::region_in(styles), + )) + .spanned(self.span()) + }); + + seq.push(HeadingElem::new(title).with_level(NonZeroUsize::ONE).pack()); + } + + 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 = vt.introspector.query(&self.target(styles).0); + + for elem in &elems { + let Some(entry) = OutlineEntry::from_outlinable( + vt, + self.span(), + elem.clone().into_inner(), + self.fill(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>()) + .map_or(false, |last| last.level() >= level) + { + ancestors.pop(); + } + + OutlineIndent::apply(&indent, vt, &ancestors, &mut seq, self.span())?; + + // Add the overridable outline entry, followed by a line break. + seq.push(entry.pack()); + seq.push(LinebreakElem::new().pack()); + + ancestors.push(elem); + } + + seq.push(ParbreakElem::new().pack()); + + Ok(Content::sequence(seq)) + } +} + +impl Finalize for OutlineElem { + fn finalize(&self, realized: Content, _: StyleChain) -> Content { + realized + .styled(HeadingElem::set_outlined(false)) + .styled(HeadingElem::set_numbering(None)) + } +} + +impl LocalName for OutlineElem { + fn local_name(&self, lang: Lang, region: Option<Region>) -> &'static str { + match lang { + Lang::ALBANIAN => "Përmbajtja", + Lang::ARABIC => "المحتويات", + Lang::BOKMÅL => "Innhold", + Lang::CHINESE if option_eq(region, "TW") => "目錄", + Lang::CHINESE => "目录", + Lang::CZECH => "Obsah", + Lang::DANISH => "Indhold", + Lang::DUTCH => "Inhoudsopgave", + Lang::FILIPINO => "Talaan ng mga Nilalaman", + Lang::FRENCH => "Table des matières", + Lang::GERMAN => "Inhaltsverzeichnis", + Lang::ITALIAN => "Indice", + Lang::NYNORSK => "Innhald", + Lang::POLISH => "Spis treści", + Lang::PORTUGUESE if option_eq(region, "PT") => "Índice", + Lang::PORTUGUESE => "Sumário", + Lang::RUSSIAN => "Содержание", + Lang::SLOVENIAN => "Kazalo", + Lang::SPANISH => "Índice", + Lang::SWEDISH => "Innehåll", + Lang::TURKISH => "İçindekiler", + Lang::UKRAINIAN => "Зміст", + Lang::VIETNAMESE => "Mục lục", + Lang::ENGLISH | _ => "Contents", + } + } +} + +/// 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, vt: &mut Vt) -> SourceResult<Option<Content>>; + + /// Returns the nesting level of this element. + fn level(&self) -> NonZeroUsize { + NonZeroUsize::ONE + } +} + +#[derive(Debug, Clone)] +pub enum OutlineIndent { + Bool(bool), + Rel(Rel<Length>), + Func(Func), +} + +impl OutlineIndent { + fn apply( + indent: &Option<Smart<Self>>, + vt: &mut Vt, + ancestors: &Vec<&Content>, + seq: &mut Vec<Content>, + 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() + .at(vt, ancestor.location().unwrap())? + .display(vt, &numbering)?; + + hidden += numbers + SpaceElem::new().pack(); + }; + } + + if !ancestors.is_empty() { + seq.push(HideElem::new(hidden).pack()); + seq.push(SpaceElem::new().pack()); + } + } + + // 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_vt(vt, [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. +/// +/// ## Example { #example } +/// The example below shows how to style entries for top-level sections to make +/// them stand out. +/// +/// ```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 +/// ``` +/// +/// To completely customize an entry's line, you can also build it from scratch +/// by accessing the `level`, `element`, `body`, `fill` and `page` fields on the entry. +/// +/// Display: Outline Entry +/// Category: meta +#[element(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`]($type/content.location) method on content + /// and can be [linked]($func/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`]($func/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( + vt: &mut Vt, + span: Span, + elem: Content, + fill: Option<Content>, + ) -> SourceResult<Option<Self>> { + let Some(outlinable) = elem.with::<dyn Outlinable>() else { + bail!(span, "cannot outline {}", elem.func().name()); + }; + + let Some(body) = outlinable.outline(vt)? else { + return Ok(None); + }; + + let location = elem.location().unwrap(); + let page_numbering = vt + .introspector + .page_numbering(location) + .cast::<Option<Numbering>>() + .unwrap() + .unwrap_or_else(|| { + Numbering::Pattern(NumberingPattern::from_str("1").unwrap()) + }); + + let page = Counter::new(CounterKey::Page) + .at(vt, location)? + .display(vt, &page_numbering)?; + + Ok(Some(Self::new(outlinable.level(), elem, body, fill, page))) + } +} + +impl Show for OutlineEntry { + fn show(&self, _vt: &mut Vt, _: 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 { + bail!(self.span(), "cannot outline {}", elem.func().name()) + }; + + // The body text remains overridable. + seq.push(self.body().linked(Destination::Location(location))); + + // Add filler symbols between the section name and page number. + if let Some(filler) = self.fill() { + seq.push(SpaceElem::new().pack()); + seq.push( + BoxElem::new() + .with_body(Some(filler)) + .with_width(Fr::one().into()) + .pack(), + ); + seq.push(SpaceElem::new().pack()); + } else { + seq.push(HElem::new(Fr::one().into()).pack()); + } + + // Add the page number. + let page = self.page().linked(Destination::Location(location)); + seq.push(page); + + Ok(Content::sequence(seq)) + } +} diff --git a/crates/typst-library/src/meta/query.rs b/crates/typst-library/src/meta/query.rs new file mode 100644 index 00000000..826b812e --- /dev/null +++ b/crates/typst-library/src/meta/query.rs @@ -0,0 +1,145 @@ +use crate::prelude::*; + +/// 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 retrieve the current document location with the +/// [`locate`]($func/locate) function. You can then decide whether you want to +/// find all elements, just the ones before that location, or just the ones +/// after it. +/// +/// ## Finding elements { #finding-elements } +/// In the example below, we create a custom page header that displays the text +/// "Typst Academy" in small capitals and the current section title. On the +/// first page, the section title is omitted because the header is before the +/// first section heading. +/// +/// To realize this layout, we call `locate` and then query for all headings +/// after the current location. The function we pass to locate is called twice +/// in this case: Once per page. +/// +/// - On the first page the query for all headings before the current location +/// yields an empty array: There are no previous headings. We check for this +/// case and and just display "Typst Academy". +/// +/// - For the second page, we retrieve the last element from the query's result. +/// This is the latest heading before the current position and as such, it is +/// the heading of the section we are currently in. We access its content +/// through the `body` field and display it alongside "Typst Academy". +/// +/// ```example +/// >>> #set page( +/// >>> width: 240pt, +/// >>> height: 180pt, +/// >>> margin: (top: 35pt, rest: 15pt), +/// >>> header-ascent: 12pt, +/// >>> ) +/// #set page(header: locate(loc => { +/// let elems = query( +/// selector(heading).before(loc), +/// loc, +/// ) +/// let academy = smallcaps[ +/// Typst Academy +/// ] +/// if elems == () { +/// align(right, academy) +/// } else { +/// let body = elems.last().body +/// academy + h(1fr) + emph(body) +/// } +/// })) +/// +/// = Introduction +/// #lorem(23) +/// +/// = Background +/// #lorem(30) +/// +/// = Analysis +/// #lorem(15) +/// ``` +/// +/// ## 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 five headings. This is because Typst +/// simply gives up after five 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]($func/counter) and [state]($func/state). +/// +/// ```example +/// = Real +/// #locate(loc => { +/// let elems = query(heading, loc) +/// let count = elems.len() +/// count * [= Fake] +/// }) +/// ``` +/// +/// ## Migration Hints { #migration-hints } +/// The `before` and `after` arguments have been removed in version 0.3.0. You +/// can now use flexible selector combinator methods instead. For example, +/// `query(heading, before: loc)` becomes `query(heading.before(loc), loc)`. +/// Please refer to the [selector documentation]($type/selector) for more +/// details. +/// +/// Display: Query +/// Category: meta +#[func] +pub fn query( + /// Can be an element function like a `heading` or `figure`, a `{<label>}` + /// or a more complex selector like `{heading.where(level: 1)}`. + /// + /// Currently, only a subset of element functions is supported. Aside from + /// headings and figures, this includes equations, references and all + /// elements with an explicit label. As a result, you _can_ query for e.g. + /// [`strong`]($func/strong) elements, but you will find only those that + /// have an explicit label attached to them. This limitation will be + /// resolved in the future. + target: LocatableSelector, + /// Can be any location. Why is it required then? As noted before, Typst has + /// to evaluate parts of your code multiple times to determine the values of + /// all state. By only allowing this function within + /// [`locate`]($func/locate) calls, the amount of code that can depend on + /// the query's result is reduced. If you could call it directly at the top + /// level of a module, the evaluation of the whole module and its exports + /// could depend on the query's result. + location: Location, + /// The virtual machine. + vm: &mut Vm, +) -> Array { + let _ = location; + let vec = vm.vt.introspector.query(&target.0); + vec.into_iter() + .map(|elem| Value::Content(elem.into_inner())) + .collect() +} + +/// 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)}`. +/// +/// Display: Selector +/// Category: meta +#[func] +pub fn selector( + /// 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 +} diff --git a/crates/typst-library/src/meta/reference.rs b/crates/typst-library/src/meta/reference.rs new file mode 100644 index 00000000..5bd04431 --- /dev/null +++ b/crates/typst-library/src/meta/reference.rs @@ -0,0 +1,276 @@ +use super::{BibliographyElem, CiteElem, Counter, Figurable, Numbering}; +use crate::meta::FootnoteElem; +use crate::prelude::*; +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]($func/cite) from a +/// bibliography. +/// +/// Referenceable elements include [headings]($func/heading), +/// [figures]($func/figure), [equations]($func/math.equation), and +/// [footnotes]($func/footnote). To create a custom referenceable element like a +/// theorem, you can create a figure of a custom [`kind`]($func/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`]($func/link) function instead. +/// +/// ## Example { #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. +/// $ O(n) = 2^n $ <slow> +/// +/// #bibliography("works.bib") +/// ``` +/// +/// ## Syntax { #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 { #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. +/// 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> +/// ``` +/// +/// Display: Reference +/// Category: meta +#[element(Synthesize, Locatable, Show)] +pub struct RefElem { + /// The target label that should be referenced. + #[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. + /// ``` + pub supplement: Smart<Option<Supplement>>, + + /// A synthesized citation. + #[synthesized] + pub citation: Option<CiteElem>, + + /// The referenced element. + #[synthesized] + pub element: Option<Content>, +} + +impl Synthesize for RefElem { + fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> { + let citation = self.to_citation(vt, styles)?; + self.push_citation(Some(citation)); + self.push_element(None); + + let target = self.target(); + if !BibliographyElem::has(vt, &target.0) { + if let Ok(elem) = vt.introspector.query_label(&target) { + self.push_element(Some(elem.into_inner())); + return Ok(()); + } + } + + Ok(()) + } +} + +impl Show for RefElem { + #[tracing::instrument(name = "RefElem::show", skip_all)] + fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + Ok(vt.delayed(|vt| { + let target = self.target(); + let elem = vt.introspector.query_label(&self.target()); + let span = self.span(); + + if BibliographyElem::has(vt, &target.0) { + if elem.is_ok() { + bail!(span, "label occurs in the document and its bibliography"); + } + + return Ok(self.to_citation(vt, styles)?.pack().spanned(span)); + } + + let elem = elem.at(span)?; + + if elem.func() == FootnoteElem::func() { + return Ok(FootnoteElem::with_label(target).pack().spanned(span)); + } + + 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 heading numbering with `#set {}(numbering: \"1.\")`", + elem.func().name() + )) + .at(span)?; + + let numbers = refable + .counter() + .at(vt, elem.location().unwrap())? + .display(vt, &numbering.trimmed())?; + + let supplement = match self.supplement(styles) { + Smart::Auto => refable.supplement(), + Smart::Custom(None) => Content::empty(), + Smart::Custom(Some(supplement)) => { + supplement.resolve(vt, [(*elem).clone()])? + } + }; + + let mut content = numbers; + if !supplement.is_empty() { + content = supplement + TextElem::packed("\u{a0}") + content; + } + + Ok(content.linked(Destination::Location(elem.location().unwrap()))) + })) + } +} + +impl RefElem { + /// Turn the reference into a citation. + pub fn to_citation(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<CiteElem> { + let mut elem = CiteElem::new(vec![self.target().0]); + elem.0.set_location(self.0.location().unwrap()); + elem.synthesize(vt, styles)?; + elem.push_supplement(match self.supplement(styles) { + Smart::Custom(Some(Supplement::Content(content))) => Some(content), + _ => None, + }); + + Ok(elem) + } +} + +/// Additional content for a reference. +pub enum Supplement { + Content(Content), + Func(Func), +} + +impl Supplement { + /// Tries to resolve the supplement into its content. + pub fn resolve<T: IntoValue>( + &self, + vt: &mut Vt, + args: impl IntoIterator<Item = T>, + ) -> SourceResult<Content> { + Ok(match self { + Supplement::Content(content) => content.clone(), + Supplement::Func(func) => func.call_vt(vt, 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/meta/state.rs b/crates/typst-library/src/meta/state.rs new file mode 100644 index 00000000..aee53a29 --- /dev/null +++ b/crates/typst-library/src/meta/state.rs @@ -0,0 +1,440 @@ +use std::fmt::{self, Debug, Formatter, Write}; + +use ecow::{eco_vec, EcoVec}; +use typst::eval::Tracer; +use typst::model::DelayedErrors; + +use crate::prelude::*; + +/// 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 +/// #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 methods. The two most +/// important ones are `display` and `update`: +/// +/// - The `display` method shows the current value of the state. You can +/// optionally give it a function that receives the value and formats it in +/// some way. +/// +/// - The `update` method 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 #s.display(). +/// ] +/// +/// #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 #s.display(). +/// >>> ] +/// <<< ... +/// +/// #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]($func/counter) is very similar to its state system. +/// +/// ## Time Travel { #time-travel } +/// By using Typst's state management system you also get time travel +/// capabilities! By combining the state system with [`locate`]($func/locate) +/// and [`query`]($func/query), 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 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 #s.display(). +/// >>> ] +/// <<< ... +/// +/// Value at `<here>` is +/// #locate(loc => s.at( +/// query(<here>, loc) +/// .first() +/// .location() +/// )) +/// +/// #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 display `4` because Typst simply +/// gives up after a few attempts. +/// +/// ```example +/// #let s = state("x", 1) +/// #locate(loc => { +/// s.update(s.final(loc) + 1) +/// }) +/// #s.display() +/// ``` +/// +/// In general, you should _typically_ not generate state updates from within +/// `locate` calls or `display` calls of state or counters. Instead, pass a +/// function to `update` that determines the value of the state based on its +/// previous value. +/// +/// ## Methods +/// ### display() +/// Displays the value of the state. +/// +/// - format: function (positional) +/// 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. +/// +/// - returns: content +/// +/// ### update() +/// Updates 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. +/// +/// - value: any or function (positional, required) +/// 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. +/// +/// - returns: content +/// +/// ### at() +/// Gets the value of the state at the given location. +/// +/// - location: location (positional, required) +/// The location at which the state's value should be retrieved. A suitable +/// location can be retrieved from [`locate`]($func/locate) or +/// [`query`]($func/query). +/// +/// - returns: any +/// +/// ### final() +/// Gets the value of the state at the end of the document. +/// +/// - location: location (positional, required) +/// Can be any location. Why is it required then? As noted before, Typst has +/// to evaluate parts of your code multiple times to determine the values of +/// all state. By only allowing this method within [`locate`]($func/locate) +/// calls, the amount of code that can depend on the method's result is +/// reduced. If you could call `final` directly at the top level of a module, +/// the evaluation of the whole module and its exports could depend on the +/// state's value. +/// +/// - returns: any +/// +/// Display: State +/// Category: meta +#[func] +pub fn state( + /// The key that identifies this state. + key: Str, + /// The initial value of the state. + #[default] + init: Value, +) -> State { + State { key, init } +} + +/// A state. +#[derive(Clone, PartialEq, Hash)] +pub struct State { + /// The key that identifies the state. + key: Str, + /// The initial value of the state. + init: Value, +} + +impl State { + /// Call a method on a state. + #[tracing::instrument(skip(vm))] + pub fn call_method( + self, + vm: &mut Vm, + method: &str, + mut args: Args, + span: Span, + ) -> SourceResult<Value> { + let value = match method { + "display" => self.display(args.eat()?).into_value(), + "at" => self.at(&mut vm.vt, args.expect("location")?)?, + "final" => self.final_(&mut vm.vt, args.expect("location")?)?, + "update" => self.update(args.expect("value or function")?).into_value(), + _ => bail!(span, "type state has no method `{}`", method), + }; + args.finish()?; + Ok(value) + } + + /// Display the current value of the state. + pub fn display(self, func: Option<Func>) -> Content { + DisplayElem::new(self, func).pack() + } + + /// Get the value of the state at the given location. + #[tracing::instrument(skip(self, vt))] + pub fn at(self, vt: &mut Vt, location: Location) -> SourceResult<Value> { + let sequence = self.sequence(vt)?; + let offset = vt.introspector.query(&self.selector().before(location, true)).len(); + Ok(sequence[offset].clone()) + } + + /// Get the value of the state at the final location. + #[tracing::instrument(skip(self, vt))] + pub fn final_(self, vt: &mut Vt, _: Location) -> SourceResult<Value> { + let sequence = self.sequence(vt)?; + Ok(sequence.last().unwrap().clone()) + } + + /// Produce content that performs a state update. + pub fn update(self, update: StateUpdate) -> Content { + UpdateElem::new(self, update).pack() + } + + /// 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, vt: &mut Vt) -> SourceResult<EcoVec<Value>> { + self.sequence_impl( + vt.world, + vt.introspector, + vt.locator.track(), + TrackedMut::reborrow_mut(&mut vt.delayed), + TrackedMut::reborrow_mut(&mut vt.tracer), + ) + } + + /// Memoized implementation of `sequence`. + #[comemo::memoize] + fn sequence_impl( + &self, + world: Tracked<dyn World + '_>, + introspector: Tracked<Introspector>, + locator: Tracked<Locator>, + delayed: TrackedMut<DelayedErrors>, + tracer: TrackedMut<Tracer>, + ) -> SourceResult<EcoVec<Value>> { + let mut locator = Locator::chained(locator); + let mut vt = Vt { + world, + introspector, + locator: &mut locator, + delayed, + tracer, + }; + let mut state = self.init.clone(); + let mut stops = eco_vec![state.clone()]; + + for elem in introspector.query(&self.selector()) { + let elem = elem.to::<UpdateElem>().unwrap(); + match elem.update() { + StateUpdate::Set(value) => state = value, + StateUpdate::Func(func) => state = func.call_vt(&mut vt, [state])?, + } + stops.push(state.clone()); + } + + Ok(stops) + } + + /// The selector for this state's updates. + fn selector(&self) -> Selector { + Selector::Elem(UpdateElem::func(), Some(dict! { "state" => self.clone() })) + } +} + +impl Debug for State { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("state(")?; + self.key.fmt(f)?; + f.write_str(", ")?; + self.init.fmt(f)?; + f.write_char(')') + } +} + +cast! { + type State: "state", +} + +/// An update to perform on a state. +#[derive(Clone, PartialEq, Hash)] +pub enum StateUpdate { + /// Set the state to the specified value. + Set(Value), + /// Apply the given function to the state. + Func(Func), +} + +impl Debug for StateUpdate { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("..") + } +} + +cast! { + type StateUpdate: "state update", + v: Func => Self::Func(v), + v: Value => Self::Set(v), +} + +/// Executes a display of a state. +/// +/// Display: State +/// Category: special +#[element(Locatable, Show)] +struct DisplayElem { + /// The state. + #[required] + state: State, + + /// The function to display the state with. + #[required] + func: Option<Func>, +} + +impl Show for DisplayElem { + #[tracing::instrument(name = "DisplayElem::show", skip(self, vt))] + fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> { + Ok(vt.delayed(|vt| { + let location = self.0.location().unwrap(); + let value = self.state().at(vt, location)?; + Ok(match self.func() { + Some(func) => func.call_vt(vt, [value])?.display(), + None => value.display(), + }) + })) + } +} + +/// Executes a display of a state. +/// +/// Display: State +/// Category: special +#[element(Locatable, Show)] +struct UpdateElem { + /// The state. + #[required] + state: State, + + /// The update to perform on the state. + #[required] + update: StateUpdate, +} + +impl Show for UpdateElem { + #[tracing::instrument(name = "UpdateElem::show")] + fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> { + Ok(Content::empty()) + } +} diff --git a/crates/typst-library/src/prelude.rs b/crates/typst-library/src/prelude.rs new file mode 100644 index 00000000..c720e770 --- /dev/null +++ b/crates/typst-library/src/prelude.rs @@ -0,0 +1,42 @@ +//! Helpful imports for creating library functionality. + +#[doc(no_inline)] +pub use std::fmt::{self, Debug, Formatter}; +#[doc(no_inline)] +pub use std::num::NonZeroUsize; + +#[doc(no_inline)] +pub use comemo::{Track, Tracked, TrackedMut}; +#[doc(no_inline)] +pub use ecow::{eco_format, EcoString}; +#[doc(no_inline)] +pub use typst::diag::{bail, error, At, Hint, SourceResult, StrResult}; +#[doc(no_inline)] +pub use typst::doc::*; +#[doc(no_inline)] +pub use typst::eval::{ + array, cast, dict, format_str, func, Args, Array, AutoValue, Cast, Dict, FromValue, + Func, IntoValue, Never, NoneValue, Scope, Str, Symbol, Type, Value, Vm, +}; +#[doc(no_inline)] +pub use typst::file::FileId; +#[doc(no_inline)] +pub use typst::geom::*; +#[doc(no_inline)] +pub use typst::model::{ + element, Behave, Behaviour, Construct, Content, ElemFunc, Element, Finalize, Fold, + Introspector, Label, Locatable, LocatableSelector, Location, Locator, MetaElem, + PlainText, Resolve, Selector, Set, Show, StyleChain, StyleVec, Styles, Synthesize, + Unlabellable, Vt, +}; +#[doc(no_inline)] +pub use typst::syntax::{Span, Spanned}; +#[doc(no_inline)] +pub use typst::util::NonZeroExt; +#[doc(no_inline)] +pub use typst::World; + +#[doc(no_inline)] +pub use crate::layout::{Fragment, Layout, Regions}; +#[doc(no_inline)] +pub use crate::shared::{ContentExt, StylesExt}; diff --git a/crates/typst-library/src/shared/behave.rs b/crates/typst-library/src/shared/behave.rs new file mode 100644 index 00000000..6a1aa127 --- /dev/null +++ b/crates/typst-library/src/shared/behave.rs @@ -0,0 +1,109 @@ +//! Element interaction. + +use typst::model::{Behave, Behaviour, Content, StyleChain, StyleVec, StyleVecBuilder}; + +/// A wrapper around a [`StyleVecBuilder`] that allows elements to interact. +#[derive(Debug)] +pub struct BehavedBuilder<'a> { + /// The internal builder. + builder: StyleVecBuilder<'a, Content>, + /// Staged weak and ignorant elements that we can't yet commit to the + /// builder. The option is `Some(_)` for weak elements and `None` for + /// ignorant elements. + staged: Vec<(Content, Behaviour, StyleChain<'a>)>, + /// What the last non-ignorant item was. + last: Behaviour, +} + +impl<'a> BehavedBuilder<'a> { + /// Create a new style-vec builder. + pub fn new() -> Self { + Self { + builder: StyleVecBuilder::new(), + staged: vec![], + last: Behaviour::Destructive, + } + } + + /// Whether the builder is totally empty. + pub fn is_empty(&self) -> bool { + self.builder.is_empty() && self.staged.is_empty() + } + + /// Whether the builder is empty except for some weak elements that will + /// probably collapse. + pub fn is_basically_empty(&self) -> bool { + self.builder.is_empty() + && self + .staged + .iter() + .all(|(_, behaviour, _)| matches!(behaviour, Behaviour::Weak(_))) + } + + /// Push an item into the sequence. + pub fn push(&mut self, elem: Content, styles: StyleChain<'a>) { + let interaction = elem + .with::<dyn Behave>() + .map_or(Behaviour::Supportive, Behave::behaviour); + + match interaction { + Behaviour::Weak(level) => { + if matches!(self.last, Behaviour::Weak(_)) { + let item = elem.with::<dyn Behave>().unwrap(); + let i = self.staged.iter().position(|prev| { + let Behaviour::Weak(prev_level) = prev.1 else { return false }; + level < prev_level + || (level == prev_level && item.larger(&prev.0)) + }); + let Some(i) = i else { return }; + self.staged.remove(i); + } + + if self.last != Behaviour::Destructive { + self.staged.push((elem, interaction, styles)); + self.last = interaction; + } + } + Behaviour::Supportive => { + self.flush(true); + self.builder.push(elem, styles); + self.last = interaction; + } + Behaviour::Destructive => { + self.flush(false); + self.builder.push(elem, styles); + self.last = interaction; + } + Behaviour::Ignorant => { + self.staged.push((elem, interaction, styles)); + } + } + } + + /// Iterate over the contained elements. + pub fn elems(&self) -> impl DoubleEndedIterator<Item = &Content> { + self.builder.elems().chain(self.staged.iter().map(|(item, ..)| item)) + } + + /// Return the finish style vec and the common prefix chain. + pub fn finish(mut self) -> (StyleVec<Content>, StyleChain<'a>) { + self.flush(false); + self.builder.finish() + } + + /// Push the staged elements, filtering out weak elements if `supportive` is + /// false. + fn flush(&mut self, supportive: bool) { + for (item, interaction, styles) in self.staged.drain(..) { + if supportive || interaction == Behaviour::Ignorant { + self.builder.push(item, styles); + } + } + } +} + +impl<'a> Default for BehavedBuilder<'a> { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/typst-library/src/shared/ext.rs b/crates/typst-library/src/shared/ext.rs new file mode 100644 index 00000000..d7c80a30 --- /dev/null +++ b/crates/typst-library/src/shared/ext.rs @@ -0,0 +1,92 @@ +//! Extension traits. + +use crate::layout::{AlignElem, MoveElem, PadElem}; +use crate::prelude::*; +use crate::text::{EmphElem, FontFamily, FontList, StrongElem, TextElem, UnderlineElem}; + +/// Additional methods on content. +pub trait ContentExt { + /// Make this content strong. + fn strong(self) -> Self; + + /// Make this content emphasized. + fn emph(self) -> Self; + + /// Underline this content. + fn underlined(self) -> Self; + + /// Link the content somewhere. + fn linked(self, dest: Destination) -> Self; + + /// Make the content linkable by `.linked(Destination::Location(loc))`. + /// + /// Should be used in combination with [`Location::variant`]. + fn backlinked(self, loc: Location) -> Self; + + /// Set alignments for this content. + fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self; + + /// Pad this content at the sides. + fn padded(self, padding: Sides<Rel<Length>>) -> Self; + + /// Transform this content's contents without affecting layout. + fn moved(self, delta: Axes<Rel<Length>>) -> Self; +} + +impl ContentExt for Content { + fn strong(self) -> Self { + StrongElem::new(self).pack() + } + + fn emph(self) -> Self { + EmphElem::new(self).pack() + } + + fn underlined(self) -> Self { + UnderlineElem::new(self).pack() + } + + fn linked(self, dest: Destination) -> Self { + self.styled(MetaElem::set_data(vec![Meta::Link(dest)])) + } + + fn backlinked(self, loc: Location) -> Self { + let mut backlink = Content::empty(); + backlink.set_location(loc); + self.styled(MetaElem::set_data(vec![Meta::Elem(backlink)])) + } + + fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self { + self.styled(AlignElem::set_alignment(aligns)) + } + + 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() + } + + fn moved(self, delta: Axes<Rel<Length>>) -> Self { + MoveElem::new(self).with_dx(delta.x).with_dy(delta.y).pack() + } +} + +/// Additional methods for style lists. +pub trait StylesExt { + /// Set a font family composed of a preferred family and existing families + /// from a style chain. + fn set_family(&mut self, preferred: FontFamily, existing: StyleChain); +} + +impl StylesExt for Styles { + fn set_family(&mut self, preferred: FontFamily, existing: StyleChain) { + self.set(TextElem::set_font(FontList( + std::iter::once(preferred) + .chain(TextElem::font_in(existing)) + .collect(), + ))); + } +} diff --git a/crates/typst-library/src/shared/mod.rs b/crates/typst-library/src/shared/mod.rs new file mode 100644 index 00000000..f54241cf --- /dev/null +++ b/crates/typst-library/src/shared/mod.rs @@ -0,0 +1,7 @@ +//! Shared definitions for the standard library. + +mod behave; +mod ext; + +pub use behave::*; +pub use ext::*; diff --git a/crates/typst-library/src/symbols/emoji.rs b/crates/typst-library/src/symbols/emoji.rs new file mode 100644 index 00000000..44bc3e14 --- /dev/null +++ b/crates/typst-library/src/symbols/emoji.rs @@ -0,0 +1,1356 @@ +use typst::eval::{symbols, 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").with_scope(scope) +} + +/// A list of named emoji. +const EMOJI: &[(&str, Symbol)] = 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: '🍻'], + 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: '🥸', + 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: '🖕', + ], + 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: '🤝', + ], + 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: '🔰', + 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: '🥬', + 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: '🌲', + 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..5036aa11 --- /dev/null +++ b/crates/typst-library/src/symbols/mod.rs @@ -0,0 +1,15 @@ +//! Modifiable symbols. + +mod emoji; +mod sym; + +pub use emoji::*; +pub use sym::*; + +use crate::prelude::*; + +/// Hook up all symbol definitions. +pub(super) fn define(global: &mut Scope) { + global.define("sym", sym()); + global.define("emoji", 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..8b1b0b39 --- /dev/null +++ b/crates/typst-library/src/symbols/sym.rs @@ -0,0 +1,842 @@ +use typst::eval::{symbols, 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").with_scope(scope) +} + +/// The list of general symbols. +pub(crate) const SYM: &[(&str, Symbol)] = symbols! { + // Control. + wj: '\u{2060}', + zwj: '\u{200D}', + zwnj: '\u{200C}', + zws: '\u{200B}', + + // Spaces. + space: [ + ' ', + nobreak: '\u{A0}', + 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: '(', r: ')', t: '⏜', b: '⏝'], + brace: [l: '{', r: '}', t: '⏞', b: '⏟'], + bracket: [l: '[', l.double: '⟦', r: ']', r.double: '⟧', t: '⎴', b: '⎵'], + turtle: [l: '〔', r: '〕', t: '⏠', b: '⏡'], + bar: [v: '|', v.double: '‖', v.triple: '⦀', v.broken: '¦', v.circle: '⦶', h: '―'], + fence: [l: '⧘', l.double: '⧚', r: '⧙', r.double: '⧛', dotted: '⦙'], + angle: [ + '∠', + l: '⟨', + r: '⟩', + l.double: '《', + r.double: '》', + acute: '⦟', + arc: '∡', + arc.rev: '⦛', + rev: '⦣', + right: '∟', + right.rev: '⯾', + right.arc: '⊾', + right.dot: '⦝', + right.sq: '⦜', + spatial: '⟀', + spheric: '∢', + spheric.rev: '⦠', + spheric.top: '⦡', + ], + + // Punctuation. + amp: ['&', inv: '⅋'], + ast: [ + op: '∗', + basic: '*', + low: '⁎', + double: '⁑', + triple: '⁂', + small: '﹡', + circle: '⊛', + sq: '⧆', + ], + at: '@', + backslash: ['\\', circle: '⦸', not: '⧷'], + co: '℅', + colon: [':', eq: '≔', double.eq: '⩴'], + comma: ',', + dagger: ['†', double: '‡'], + dash: [ + en: '–', + em: '—', + fig: '‒', + wave: '〜', + colon: '∹', + circle: '⊝', + wave.double: '〰', + ], + dot: [ + op: '⋅', + basic: '.', + c: '·', + circle: '⊙', + circle.big: '⨀', + square: '⊡', + double: '¨', + triple: '\u{20db}', + 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: '%', + copyright: ['©', sound: '℗'], + permille: '‰', + pilcrow: ['¶', rev: '⁋'], + section: '§', + semi: [';', rev: '⁏'], + slash: ['/', double: '⫽', triple: '⫻', big: '\u{29f8}'], + dots: [h.c: '⋯', h: '…', v: '⋮', down: '⋱', up: '⋰'], + tilde: [ + op: '∼', + basic: '~', + eq: '≃', + eq.not: '≄', + eq.rev: '⋍', + eqq: '≅', + eqq.not: '≇', + neqq: '≆', + not: '≁', + rev: '∽', + rev.eqq: '≌', + triple: '≋', + ], + + // Accents, quotes, and primes. + acute: ['´', double: '˝'], + breve: '˘', + caret: '‸', + caron: 'ˇ', + hat: '^', + diaer: '¨', + grave: '`', + 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: '∔', + minus: '±', + small: '﹢', + square: '⊞', + triangle: '⨹', + ], + 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: '⋟', + ], + gt: [ + '>', + circle: '⧁', + dot: '⋗', + double: '≫', + eq: '≥', + eq.slant: '⩾', + eq.lt: '⋛', + eq.not: '≱', + eqq: '≧', + lt: '≷', + lt.not: '≹', + neqq: '≩', + not: '≯', + ntilde: '⋧', + small: '﹥', + tilde: '≳', + tilde.not: '≵', + tri: '⊳', + tri.eq: '⊵', + tri.eq.not: '⋭', + tri.not: '⋫', + triple: '⋙', + triple.nested: '⫸', + ], + lt: [ + '<', + circle: '⧀', + dot: '⋖', + double: '≪', + eq: '≤', + eq.slant: '⩽' , + eq.gt: '⋚', + eq.not: '≰', + eqq: '≦', + gt: '≶', + gt.not: '≸', + neqq: '≨', + not: '≮', + ntilde: '⋦', + small: '﹤', + tilde: '≲', + tilde.not: '≴', + tri: '⊲', + tri.eq: '⊴', + tri.eq.not: '⋬', + tri.not: '⋪', + triple: '⋘', + triple.nested: '⫷', + ], + approx: ['≈', eq: '≊', not: '≉'], + prec: [ + '≺', + approx: '⪷', + double: '⪻', + eq: '≼', + eq.not: '⋠', + eqq: '⪳', + napprox: '⪹', + neqq: '⪵', + not: '⊀', + ntilde: '⋨', + tilde: '≾', + ], + succ: [ + '≻', + approx: '⪸', + double: '⪼', + eq: '≽', + eq.not: '⋡', + eqq: '⪴', + napprox: '⪺', + neqq: '⪶', + not: '⊁', + ntilde: '⋩', + tilde: '≿', + ], + ident: ['≡', not: '≢', strict: '≣'], + prop: '∝', + + // Set theory. + nothing: ['∅', 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: '∞', + oo: '∞', + diff: '∂', + nabla: '∇', + sum: ['∑', integral: '⨋'], + product: ['∏', co: '∐'], + integral: [ + '∫', + arrow.hook: '⨗', + ccw: '⨑', + cont: '∮', + cont.ccw: '∳', + cont.cw: '∲', + cw: '∱', + double: '∬', + quad: '⨌', + sect: '⨙', + sq: '⨖', + 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: '⊧', + therefore: '∴', + because: '∵', + qed: '∎', + + // Function and category theory. + compose: '∘', + convolve: '∗', + multimap: '⊸', + + // Number theory. + divides: ['∣', not: '∤'], + perp: ['⟂', circle: '⦹'], + + // Algebra. + wreath: '≀', + + // Geometry. + parallel: ['∥', circle: '⦷', not: '∦'], + + // 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: ['☐', x: '☒'], + checkmark: ['✓', light: '🗸'], + floral: ['❦', l: '☙', r: '❧'], + notes: [up: '🎜', down: '🎝'], + refmark: '※', + servicemark: '℠', + maltese: '✠', + suit: [club: '♣', diamond: '♦', heart: '♥', spade: '♠'], + + // Shapes. + bullet: '•', + 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.r: '▷', + stroked.l: '◁', + stroked.t: '△', + stroked.b: '▽', + stroked.bl: '◺', + stroked.br: '◿', + stroked.tl: '◸', + stroked.tr: '◹', + stroked.small.r: '▹', + stroked.small.b: '▿', + stroked.small.l: '◃', + stroked.small.t: '▵', + stroked.rounded: '🛆', + stroked.nested: '⟁', + stroked.dot: '◬', + filled.r: '▶', + filled.l: '◀', + filled.t: '▲', + filled.b: '▼', + filled.bl: '◣', + filled.br: '◢', + filled.tl: '◤', + filled.tr: '◥', + filled.small.r: '▸', + filled.small.b: '▾', + filled.small.l: '◂', + filled.small.t: '▴', + ], + 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: '⬧', + ], + star: [op: '⋆', stroked: '★', filled: '★'], + + // Arrows, harpoons, and tacks. + arrow: [ + r: '→', + r.long.bar: '⟼', + r.bar: '↦', + r.curve: '⤷', + 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.triple: '⇛', + r.twohead.bar: '⤅', + r.twohead: '↠', + r.wave: '↝', + l: '←', + l.bar: '↤', + l.curve: '⤶', + 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.triple: '⇚', + l.twohead.bar: '⬶', + l.twohead: '↞', + l.wave: '↜', + t: '↑', + t.bar: '↥', + t.curve: '⤴', + t.dashed: '⇡', + t.double: '⇑', + t.filled: '⬆', + t.quad: '⟰', + t.stop: '⤒', + t.stroked: '⇧', + t.triple: '⤊', + t.twohead: '↟', + b: '↓', + b.bar: '↧', + b.curve: '⤵', + b.dashed: '⇣', + b.double: '⇓', + b.filled: '⬇', + b.quad: '⟱', + b.stop: '⤓', + b.stroked: '⇩', + b.triple: '⤋', + b.twohead: '↡', + 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: [ + rt: '⇀', + rt.bar: '⥛', + rt.stop: '⥓', + rb: '⇁', + rb.bar: '⥟', + rb.stop: '⥗', + 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. + alef: 'א', + bet: 'ב', + gimel: 'ג', + 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/deco.rs b/crates/typst-library/src/text/deco.rs new file mode 100644 index 00000000..9ec4ca32 --- /dev/null +++ b/crates/typst-library/src/text/deco.rs @@ -0,0 +1,420 @@ +use kurbo::{BezPath, Line, ParamCurve}; +use ttf_parser::{GlyphId, OutlineBuilder}; + +use super::TextElem; +use crate::prelude::*; + +/// Underlines text. +/// +/// ## Example { #example } +/// ```example +/// This is #underline[important]. +/// ``` +/// +/// Display: Underline +/// Category: text +#[element(Show)] +pub struct UnderlineElem { + /// How to stroke the line. + /// + /// See the [line's documentation]($func/line.stroke) for more details. 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<PartialStroke>, + + /// 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, + + /// The content to underline. + #[required] + pub body: Content, +} + +impl Show for UnderlineElem { + #[tracing::instrument(name = "UnderlineElem::show", skip_all)] + fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + Ok(self.body().styled(TextElem::set_deco(Decoration { + line: DecoLine::Underline, + stroke: self.stroke(styles).unwrap_or_default(), + offset: self.offset(styles), + extent: self.extent(styles), + evade: self.evade(styles), + }))) + } +} + +/// Adds a line over text. +/// +/// ## Example { #example } +/// ```example +/// #overline[A line over text.] +/// ``` +/// +/// Display: Overline +/// Category: text +#[element(Show)] +pub struct OverlineElem { + /// How to stroke the line. + /// + /// See the [line's documentation]($func/line.stroke) for more details. 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<PartialStroke>, + + /// 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, + + /// The content to add a line over. + #[required] + pub body: Content, +} + +impl Show for OverlineElem { + #[tracing::instrument(name = "OverlineElem::show", skip_all)] + fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + Ok(self.body().styled(TextElem::set_deco(Decoration { + line: DecoLine::Overline, + stroke: self.stroke(styles).unwrap_or_default(), + offset: self.offset(styles), + extent: self.extent(styles), + evade: self.evade(styles), + }))) + } +} + +/// Strikes through text. +/// +/// ## Example { #example } +/// ```example +/// This is #strike[not] relevant. +/// ``` +/// +/// Display: Strikethrough +/// Category: text +#[element(Show)] +pub struct StrikeElem { + /// How to stroke the line. + /// + /// See the [line's documentation]($func/line.stroke) for more details. 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<PartialStroke>, + + /// 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, + + /// The content to strike through. + #[required] + pub body: Content, +} + +impl Show for StrikeElem { + #[tracing::instrument(name = "StrikeElem::show", skip_all)] + fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + Ok(self.body().styled(TextElem::set_deco(Decoration { + line: DecoLine::Strikethrough, + stroke: self.stroke(styles).unwrap_or_default(), + offset: self.offset(styles), + extent: self.extent(styles), + evade: false, + }))) + } +} + +/// Defines a line that is positioned over, under or on top of text. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Decoration { + pub line: DecoLine, + pub stroke: PartialStroke<Abs>, + pub offset: Smart<Abs>, + pub extent: Abs, + pub evade: bool, +} + +impl Fold for Decoration { + type Output = Vec<Self>; + + fn fold(self, mut outer: Self::Output) -> Self::Output { + outer.insert(0, self); + outer + } +} + +cast! { + type Decoration: "decoration", +} + +/// A kind of decorative line. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum DecoLine { + Underline, + Strikethrough, + Overline, +} + +/// Add line decorations to a single run of shaped text. +pub(super) fn decorate( + frame: &mut Frame, + deco: &Decoration, + text: &TextItem, + shift: Abs, + pos: Point, + width: Abs, +) { + let font_metrics = text.font.metrics(); + let metrics = match deco.line { + DecoLine::Strikethrough => font_metrics.strikethrough, + DecoLine::Overline => font_metrics.overline, + DecoLine::Underline => font_metrics.underline, + }; + + let offset = deco.offset.unwrap_or(-metrics.position.at(text.size)) - shift; + let stroke = deco.stroke.clone().unwrap_or(Stroke { + paint: text.fill.clone(), + thickness: metrics.thickness.at(text.size), + ..Stroke::default() + }); + + let gap_padding = 0.08 * text.size; + let min_width = 0.162 * text.size; + + let start = pos.x - deco.extent; + let end = pos.x + (width + 2.0 * deco.extent); + + let mut push_segment = |from: Abs, to: Abs| { + let origin = Point::new(from, pos.y + offset); + let target = Point::new(to - from, Abs::zero()); + + if target.x >= min_width || !deco.evade { + let shape = Geometry::Line(target).stroked(stroke.clone()); + frame.push(origin, FrameItem::Shape(shape, Span::detached())); + } + }; + + if !deco.evade { + push_segment(start, end); + return; + } + + let line = Line::new( + kurbo::Point::new(pos.x.to_raw(), offset.to_raw()), + kurbo::Point::new((pos.x + width).to_raw(), offset.to_raw()), + ); + + let mut x = pos.x; + let mut intersections = vec![]; + + for glyph in text.glyphs.iter() { + let dx = glyph.x_offset.at(text.size) + x; + let mut builder = + BezPathBuilder::new(font_metrics.units_per_em, text.size, dx.to_raw()); + + let bbox = text.font.ttf().outline_glyph(GlyphId(glyph.id), &mut builder); + let path = builder.finish(); + + x += glyph.x_advance.at(text.size); + + // Only do the costly segments intersection test if the line + // intersects the bounding box. + let intersect = bbox.map_or(false, |bbox| { + let y_min = -text.font.to_em(bbox.y_max).at(text.size); + let y_max = -text.font.to_em(bbox.y_min).at(text.size); + offset >= y_min && offset <= y_max + }); + + if intersect { + // Find all intersections of segments with the line. + intersections.extend( + path.segments() + .flat_map(|seg| seg.intersect_line(line)) + .map(|is| Abs::raw(line.eval(is.line_t).x)), + ); + } + } + + // Add start and end points, taking padding into account. + intersections.push(start - gap_padding); + intersections.push(end + gap_padding); + // When emitting the decorative line segments, we move from left to + // right. The intersections are not necessarily in this order, yet. + intersections.sort(); + + for edge in intersections.windows(2) { + let l = edge[0]; + let r = edge[1]; + + // If we are too close, don't draw the segment + if r - l < gap_padding { + continue; + } else { + push_segment(l + gap_padding, r - gap_padding); + } + } +} + +/// Builds a kurbo [`BezPath`] for a glyph. +struct BezPathBuilder { + path: BezPath, + units_per_em: f64, + font_size: Abs, + x_offset: f64, +} + +impl BezPathBuilder { + fn new(units_per_em: f64, font_size: Abs, x_offset: f64) -> Self { + Self { + path: BezPath::new(), + units_per_em, + font_size, + x_offset, + } + } + + fn finish(self) -> BezPath { + self.path + } + + fn p(&self, x: f32, y: f32) -> kurbo::Point { + kurbo::Point::new(self.s(x) + self.x_offset, -self.s(y)) + } + + fn s(&self, v: f32) -> f64 { + Em::from_units(v, self.units_per_em).at(self.font_size).to_raw() + } +} + +impl OutlineBuilder for BezPathBuilder { + fn move_to(&mut self, x: f32, y: f32) { + self.path.move_to(self.p(x, y)); + } + + fn line_to(&mut self, x: f32, y: f32) { + self.path.line_to(self.p(x, y)); + } + + fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { + self.path.quad_to(self.p(x1, y1), self.p(x, y)); + } + + fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { + self.path.curve_to(self.p(x1, y1), self.p(x2, y2), self.p(x, y)); + } + + fn close(&mut self) { + self.path.close_path(); + } +} diff --git a/crates/typst-library/src/text/misc.rs b/crates/typst-library/src/text/misc.rs new file mode 100644 index 00000000..811b027e --- /dev/null +++ b/crates/typst-library/src/text/misc.rs @@ -0,0 +1,330 @@ +use super::TextElem; +use crate::prelude::*; + +/// A text space. +/// +/// Display: Space +/// Category: text +#[element(Behave, Unlabellable, PlainText)] +pub struct SpaceElem {} + +impl Behave for SpaceElem { + fn behaviour(&self) -> Behaviour { + Behaviour::Weak(2) + } +} + +impl Unlabellable for SpaceElem {} + +impl PlainText for SpaceElem { + fn plain_text(&self, text: &mut EcoString) { + text.push(' '); + } +} + +/// 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 } +/// ```example +/// *Date:* 26.12.2022 \ +/// *Topic:* Infrastructure Test \ +/// *Severity:* High \ +/// ``` +/// +/// ## Syntax { #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. +/// +/// Display: Line Break +/// Category: text +#[element(Behave)] +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 Behave for LinebreakElem { + fn behaviour(&self) -> Behaviour { + Behaviour::Destructive + } +} + +/// Strongly emphasizes content by increasing the font weight. +/// +/// Increases the current font weight by a given `delta`. +/// +/// ## Example { #example } +/// ```example +/// This is *strong.* \ +/// This is #strong[too.] \ +/// +/// #show strong: set text(red) +/// And this is *evermore.* +/// ``` +/// +/// ## Syntax { #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. +/// +/// Display: Strong Emphasis +/// Category: text +#[element(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 StrongElem { + #[tracing::instrument(name = "StrongElem::show", skip_all)] + fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + Ok(self.body().styled(TextElem::set_delta(Delta(self.delta(styles))))) + } +} + +/// A delta that is summed up when folded. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Delta(pub i64); + +cast! { + Delta, + self => self.0.into_value(), + v: i64 => Self(v), +} + +impl Fold for Delta { + type Output = i64; + + fn fold(self, outer: Self::Output) -> Self::Output { + outer + self.0 + } +} + +/// Emphasizes content by setting it in italics. +/// +/// - If the current [text style]($func/text.style) is `{"normal"}`, +/// this turns it into `{"italic"}`. +/// - If it is already `{"italic"}` or `{"oblique"}`, +/// it turns it back to `{"normal"}`. +/// +/// ## Example { #example } +/// ```example +/// This is _emphasized._ \ +/// This is #emph[too.] +/// +/// #show emph: it => { +/// text(blue, it.body) +/// } +/// +/// This is _emphasized_ differently. +/// ``` +/// +/// ## Syntax { #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. +/// +/// Display: Emphasis +/// Category: text +#[element(Show)] +pub struct EmphElem { + /// The content to emphasize. + #[required] + pub body: Content, +} + +impl Show for EmphElem { + #[tracing::instrument(name = "EmphElem::show", skip(self))] + fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> { + Ok(self.body().styled(TextElem::set_emph(Toggle))) + } +} + +/// A toggle that turns on and off alternatingly if folded. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Toggle; + +cast! { + Toggle, + self => Value::None, + _: Value => Self, +} + +impl Fold for Toggle { + type Output = bool; + + fn fold(self, outer: Self::Output) -> Self::Output { + !outer + } +} + +/// Converts text or content to lowercase. +/// +/// ## Example { #example } +/// ```example +/// #lower("ABC") \ +/// #lower[*My Text*] \ +/// #lower[already low] +/// ``` +/// +/// Display: Lowercase +/// Category: text +#[func] +pub fn lower( + /// The text to convert to lowercase. + text: Caseable, +) -> Caseable { + case(text, Case::Lower) +} + +/// Converts text or content to uppercase. +/// +/// ## Example { #example } +/// ```example +/// #upper("abc") \ +/// #upper[*my text*] \ +/// #upper[ALREADY HIGH] +/// ``` +/// +/// Display: Uppercase +/// Category: text +#[func] +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(), + } + } +} + +/// Displays text in small capitals. +/// +/// _Note:_ This enables the OpenType `smcp` feature for the font. Not all fonts +/// support this feature. Sometimes smallcaps are part of a dedicated font and +/// sometimes they are not available at all. In the future, this function will +/// support selecting a dedicated smallcaps font as well as synthesizing +/// smallcaps from normal letters, but this is not yet implemented. +/// +/// ## Example { #example } +/// ```example +/// #set par(justify: true) +/// #set heading(numbering: "I.") +/// +/// #show heading: it => { +/// set block(below: 10pt) +/// set text(weight: "regular") +/// align(center, smallcaps(it)) +/// } +/// +/// = Introduction +/// #lorem(40) +/// ``` +/// +/// Display: Small Capitals +/// Category: text +#[func] +pub fn smallcaps( + /// The text to display to small capitals. + body: Content, +) -> Content { + body.styled(TextElem::set_smallcaps(true)) +} + +/// 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 } +/// ```example +/// = Blind Text +/// #lorem(30) +/// +/// = More Blind Text +/// #lorem(15) +/// ``` +/// +/// Display: Blind Text +/// Category: text +#[func] +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..ff8cbad8 --- /dev/null +++ b/crates/typst-library/src/text/mod.rs @@ -0,0 +1,769 @@ +//! Text handling. + +mod deco; +mod misc; +mod quotes; +mod raw; +mod shaping; +mod shift; + +pub use self::deco::*; +pub use self::misc::*; +pub use self::quotes::*; +pub use self::raw::*; +pub use self::shaping::*; +pub use self::shift::*; + +use rustybuzz::Tag; +use typst::font::{FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontMetric}; + +use crate::layout::ParElem; +use crate::prelude::*; + +/// Hook up all text definitions. +pub(super) fn define(global: &mut Scope) { + global.define("text", TextElem::func()); + global.define("linebreak", LinebreakElem::func()); + global.define("smartquote", SmartQuoteElem::func()); + global.define("strong", StrongElem::func()); + global.define("emph", EmphElem::func()); + global.define("lower", lower_func()); + global.define("upper", upper_func()); + global.define("smallcaps", smallcaps_func()); + global.define("sub", SubElem::func()); + global.define("super", SuperElem::func()); + global.define("underline", UnderlineElem::func()); + global.define("strike", StrikeElem::func()); + global.define("overline", OverlineElem::func()); + global.define("raw", RawElem::func()); + global.define("lorem", lorem_func()); +} + +/// 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 } +/// ```example +/// #set text(18pt) +/// With a set rule. +/// +/// #emph(text(blue)[ +/// With a function call. +/// ]) +/// ``` +/// +/// Display: Text +/// Category: text +#[element(Construct, PlainText)] +pub struct TextElem { + /// A prioritized sequence of font families. + /// + /// 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. + /// + /// ```example + /// #set text(font: ( + /// "Inria Serif", + /// "Noto Sans Arabic", + /// )) + /// + /// This is Latin. \ + /// هذا عربي. + /// + /// ``` + #[default(FontList(vec![FontFamily::new("Linux Libertine")]))] + 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)] + 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]($func/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: "Linux Libertine", style: "italic")[Italic] + /// #text(font: "DejaVu Sans", style: "oblique")[Oblique] + /// ``` + 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]($func/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] + /// ``` + pub weight: FontWeight, + + /// The desired width of the glyphs. Accepts a ratio between `{50%}` and + /// `{200%}`. When the desired weight 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`]($func/text.tracking) + /// property instead. + /// + /// ```example + /// #text(stretch: 75%)[Condensed] \ + /// #text(stretch: 100%)[Normal] + /// ``` + 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(Abs::pt(11.0))] + pub size: TextSize, + + /// The glyph fill color. + /// + /// ```example + /// #set text(fill: red) + /// This text is red. + /// ``` + #[parse(args.named_or_find("fill")?)] + #[default(Color::BLACK.into())] + pub fill: Paint, + + /// The amount of space that should be added between characters. + /// + /// ```example + /// #set text(tracking: 1.5pt) + /// Distant text. + /// ``` + #[resolve] + 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`]($func/text.tracking) property instead. + /// + /// ```example + /// #set text(spacing: 200%) + /// Text with distant words. + /// ``` + #[resolve] + #[default(Rel::one())] + pub spacing: Rel<Length>, + + /// An amount to shift the text baseline by. + /// + /// ```example + /// A #text(baseline: 3pt)[lowered] + /// word. + /// ``` + #[resolve] + 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)] + 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(TextEdge::Metric(VerticalFontMetric::CapHeight))] + pub top_edge: TextEdge, + + /// 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(TextEdge::Metric(VerticalFontMetric::Baseline))] + pub bottom_edge: TextEdge, + + /// 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]($func/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)] + 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. + pub region: Option<Region>, + + /// 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]($func/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 do get in touch with us through the + /// [contact form](https://typst.app/contact) or our + /// [Discord server]($community/#discord)! + /// + /// ```example + /// #set text(dir: rtl) + /// هذا عربي. + /// ``` + #[resolve] + 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]($func/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] + pub hyphenate: Hyphenate, + + /// 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)] + 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)] + pub alternates: bool, + + /// Which stylistic set 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. When set + /// to an integer between `{1}` and `{20}`, enables the corresponding + /// OpenType font feature from `ss01`, ..., `ss20`. + pub stylistic_set: Option<StylisticSet>, + + /// 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)] + pub ligatures: bool, + + /// Whether ligatures that should be used sparingly are active. Setting this + /// to `{true}` enables the OpenType `dlig` font feature. + #[default(false)] + pub discretionary_ligatures: bool, + + /// Whether historical ligatures are active. Setting this to `{true}` + /// enables the OpenType `hlig` font feature. + #[default(false)] + 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. + /// ``` + 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. + /// ``` + 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)] + 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)] + 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] + pub features: FontFeatures, + + /// Content in which all text is styled according to the other arguments. + #[external] + #[required] + pub body: Content, + + /// The text. + #[internal] + #[required] + pub text: EcoString, + + /// A delta to apply on the font weight. + #[internal] + #[fold] + pub delta: Delta, + + /// Whether the font style should be inverted. + #[internal] + #[fold] + #[default(false)] + pub emph: Toggle, + + /// Decorative lines. + #[internal] + #[fold] + pub deco: Decoration, + + /// A case transformation that should be applied to the text. + #[internal] + pub case: Option<Case>, + + /// Whether small capital glyphs should be used. ("smcp") + #[internal] + #[default(false)] + 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 Construct for TextElem { + fn construct(_: &mut Vm, 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(args)?; + let body = args.expect::<Content>("body")?; + Ok(body.styled_with_map(styles)) + } +} + +impl PlainText for 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 IntoIterator for FontList { + type IntoIter = std::vec::IntoIter<FontFamily>; + type Item = FontFamily; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_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::<StrResult<_>>()?), +} + +/// The size of text. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct TextSize(pub Length); + +impl Fold for TextSize { + type Output = Abs; + + fn fold(self, outer: Self::Output) -> Self::Output { + self.0.em.at(outer) + self.0.abs + } +} + +cast! { + TextSize, + self => self.0.into_value(), + v: Length => Self(v), +} + +/// Specifies the bottom or top edge of text. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum TextEdge { + /// An edge specified using one of the well-known font metrics. + Metric(VerticalFontMetric), + /// An edge specified as a length. + Length(Length), +} + +impl TextEdge { + /// Resolve the value of the text edge given a font's metrics. + pub fn resolve(self, styles: StyleChain, metrics: &FontMetrics) -> Abs { + match self { + Self::Metric(metric) => metrics.vertical(metric).resolve(styles), + Self::Length(length) => length.resolve(styles), + } + } +} + +cast! { + TextEdge, + self => match self { + Self::Metric(metric) => metric.into_value(), + Self::Length(length) => length.into_value(), + }, + v: VerticalFontMetric => Self::Metric(v), + v: Length => Self::Length(v), +} + +/// 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.map_or(false, |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 stylistic set in a font. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct StylisticSet(u8); + +impl StylisticSet { + /// Create a new set, clamping to 1-20. + pub fn new(index: u8) -> Self { + Self(index.clamp(1, 20)) + } + + /// Get the value, guaranteed to be 1-20. + pub fn get(self) -> u8 { + self.0 + } +} + +cast! { + StylisticSet, + self => self.0.into_value(), + v: i64 => match v { + 1 ..= 20 => Self::new(v as u8), + _ => bail!("stylistic set must be between 1 and 20"), + }, +} + +/// 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::<StrResult<_>>()?), + 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::<StrResult<_>>()?), +} + +impl Fold for FontFeatures { + type Output = Self; + + fn fold(mut self, outer: Self::Output) -> Self::Output { + self.0.extend(outer.0); + self + } +} diff --git a/crates/typst-library/src/text/quotes.rs b/crates/typst-library/src/text/quotes.rs new file mode 100644 index 00000000..d0798064 --- /dev/null +++ b/crates/typst-library/src/text/quotes.rs @@ -0,0 +1,209 @@ +use typst::syntax::is_newline; + +use crate::prelude::*; + +/// A language-aware quote that reacts to its context. +/// +/// Automatically turns into an appropriate opening or closing quote based on +/// the active [text language]($func/text.lang). +/// +/// ## Example { #example } +/// ```example +/// "This is in quotes." +/// +/// #set text(lang: "de") +/// "Das ist in Anführungszeichen." +/// +/// #set text(lang: "fr") +/// "C'est entre guillemets." +/// ``` +/// +/// ## Syntax { #syntax } +/// This function also has dedicated syntax: The normal quote characters +/// (`'` and `"`). Typst automatically makes your quotes smart. +/// +/// Display: Smart Quote +/// Category: text +#[element] +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, +} + +/// State machine for smart quote substitution. +#[derive(Debug, Clone)] +pub struct Quoter { + /// How many quotes have been opened. + quote_depth: usize, + /// Whether an opening quote might follow. + expect_opening: bool, + /// Whether the last character was numeric. + last_num: bool, +} + +impl Quoter { + /// Start quoting. + pub fn new() -> Self { + Self { + quote_depth: 0, + expect_opening: true, + last_num: false, + } + } + + /// Process the last seen character. + pub fn last(&mut self, c: char) { + self.expect_opening = is_ignorable(c) || is_opening_bracket(c); + self.last_num = c.is_numeric(); + } + + /// Process and substitute a quote. + pub fn quote<'a>( + &mut self, + quotes: &Quotes<'a>, + double: bool, + peeked: Option<char>, + ) -> &'a str { + let peeked = peeked.unwrap_or(' '); + if self.expect_opening { + self.quote_depth += 1; + quotes.open(double) + } else if self.quote_depth > 0 + && (peeked.is_ascii_punctuation() || is_ignorable(peeked)) + { + self.quote_depth -= 1; + quotes.close(double) + } else if self.last_num { + quotes.prime(double) + } else { + quotes.fallback(double) + } + } +} + +impl Default for Quoter { + fn default() -> Self { + Self::new() + } +} + +fn is_ignorable(c: char) -> bool { + c.is_whitespace() || is_newline(c) +} + +fn is_opening_bracket(c: char) -> bool { + matches!(c, '(' | '{' | '[') +} + +/// Decides which quotes to substitute smart quotes with. +pub struct Quotes<'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> Quotes<'s> { + /// Create a new `Quotes` struct with 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, Lithuanian, + /// Latvian, Slovak, Slovenian, Spanish, Bosnian, Finnish, Swedish, French, + /// Hungarian, Polish, Romanian, Japanese, Traditional Chinese, Russian, and + /// Norwegian. + /// + /// For unknown languages, the English quotes are used. + pub fn from_lang(lang: Lang, region: Option<Region>) -> Self { + let region = region.as_ref().map(Region::as_str); + let (single_open, single_close, double_open, double_close) = match lang.as_str() { + "de" if matches!(region, Some("CH" | "LI")) => ("‹", "›", "«", "»"), + "cs" | "da" | "de" | "et" | "is" | "lt" | "lv" | "sk" | "sl" => { + ("‚", "‘", "„", "“") + } + "fr" => ("‹\u{00A0}", "\u{00A0}›", "«\u{00A0}", "\u{00A0}»"), + "bs" | "fi" | "sv" => ("’", "’", "”", "”"), + "es" if matches!(region, Some("ES") | None) => ("“", "”", "«", "»"), + "hu" | "pl" | "ro" => ("’", "’", "„", "”"), + "ru" | "no" | "nb" | "nn" | "ua" => ("’", "’", "«", "»"), + _ if lang.dir() == Dir::RTL => ("’", "‘", "”", "“"), + _ => return Self::default(), + }; + + Self { + single_open, + single_close, + double_open, + double_close, + } + } + + /// The opening quote. + fn open(&self, double: bool) -> &'s str { + if double { + self.double_open + } else { + self.single_open + } + } + + /// The closing quote. + fn close(&self, double: bool) -> &'s str { + if double { + self.double_close + } else { + self.single_close + } + } + + /// Which character should be used as a prime. + fn prime(&self, double: bool) -> &'static str { + if double { + "″" + } else { + "′" + } + } + + /// Which character should be used as a fallback quote. + fn fallback(&self, double: bool) -> &'static str { + if double { + "\"" + } else { + "’" + } + } +} + +impl Default for Quotes<'_> { + /// Returns the english quotes as default. + fn default() -> Self { + Self { + single_open: "‘", + single_close: "’", + double_open: "“", + double_close: "”", + } + } +} diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs new file mode 100644 index 00000000..24f39bb6 --- /dev/null +++ b/crates/typst-library/src/text/raw.rs @@ -0,0 +1,398 @@ +use once_cell::sync::Lazy; +use syntect::highlighting as synt; +use typst::syntax::{self, LinkedNode}; + +use super::{ + FontFamily, FontList, Hyphenate, LinebreakElem, SmartQuoteElem, TextElem, TextSize, +}; +use crate::layout::BlockElem; +use crate::meta::{Figurable, LocalName}; +use crate::prelude::*; + +/// 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 } +/// ````example +/// Adding `rbx` to `rcx` gives +/// the desired result. +/// +/// ```rust +/// fn main() { +/// println!("Hello World!"); +/// } +/// ``` +/// ```` +/// +/// ## Syntax { #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. +/// 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 is rendered as is, in particular, there are no +/// escape sequences. +/// +/// Display: Raw Text / Code +/// Category: text +#[element(Synthesize, Show, Finalize, 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: EcoString, + + /// Whether the raw text is displayed as a separate block. + /// + /// In markup mode, using one-backtick notation makes this `{false}`, + /// whereas using three-backtick notation makes it `{true}`. + /// + /// ````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. + /// + /// ```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"}` and `{"typc"}` tags for Typst markup and Typst code, + /// respectively. + /// + /// ````example + /// ```typ + /// This is *Typst!* + /// ``` + /// ```` + 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(HorizontalAlign(GenAlign::Start))] + pub align: HorizontalAlign, +} + +impl RawElem { + /// The supported language names and tags. + pub fn languages() -> Vec<(&'static str, Vec<&'static str>)> { + 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"])]) + .collect() + } +} + +impl Synthesize for RawElem { + fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> { + self.push_lang(self.lang(styles)); + Ok(()) + } +} + +impl Show for RawElem { + #[tracing::instrument(name = "RawElem::show", skip_all)] + fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + let text = self.text(); + let lang = self.lang(styles).as_ref().map(|s| s.to_lowercase()); + let foreground = THEME + .settings + .foreground + .map(to_typst) + .map_or(Color::BLACK, Color::from); + + let mut realized = if matches!(lang.as_deref(), Some("typ" | "typst" | "typc")) { + let root = match lang.as_deref() { + Some("typc") => syntax::parse_code(&text), + _ => syntax::parse(&text), + }; + + let mut seq = vec![]; + let highlighter = synt::Highlighter::new(&THEME); + highlight_themed( + &LinkedNode::new(&root), + vec![], + &highlighter, + &mut |node, style| { + seq.push(styled(&text[node.range()], foreground.into(), style)); + }, + ); + + Content::sequence(seq) + } else if let Some(syntax) = + lang.and_then(|token| SYNTAXES.find_syntax_by_token(&token)) + { + let mut seq = vec![]; + let mut highlighter = syntect::easy::HighlightLines::new(syntax, &THEME); + for (i, line) in text.lines().enumerate() { + if i != 0 { + seq.push(LinebreakElem::new().pack()); + } + + for (style, piece) in + highlighter.highlight_line(line, &SYNTAXES).into_iter().flatten() + { + seq.push(styled(piece, foreground.into(), style)); + } + } + + Content::sequence(seq) + } else { + TextElem::packed(text) + }; + + if self.block(styles) { + // Align the text before inserting it into the block. + realized = realized.aligned(Axes::with_x(Some(self.align(styles).into()))); + realized = BlockElem::new().with_body(Some(realized)).pack(); + } + + Ok(realized) + } +} + +impl Finalize for RawElem { + fn finalize(&self, realized: Content, _: StyleChain) -> Content { + let mut styles = Styles::new(); + styles.set(TextElem::set_overhang(false)); + styles.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); + styles.set(TextElem::set_size(TextSize(Em::new(0.8).into()))); + styles + .set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")]))); + styles.set(SmartQuoteElem::set_enabled(false)); + realized.styled_with_map(styles) + } +} + +impl LocalName for RawElem { + fn local_name(&self, lang: Lang, _: Option<Region>) -> &'static str { + match lang { + Lang::ALBANIAN => "List", + Lang::ARABIC => "قائمة", + Lang::BOKMÅL => "Utskrift", + Lang::CHINESE => "代码", + Lang::CZECH => "Seznam", + Lang::DANISH => "Liste", + Lang::DUTCH => "Listing", + Lang::FILIPINO => "Listahan", + Lang::FRENCH => "Liste", + Lang::GERMAN => "Listing", + Lang::ITALIAN => "Codice", + Lang::NYNORSK => "Utskrift", + Lang::POLISH => "Program", + Lang::RUSSIAN => "Листинг", + Lang::SLOVENIAN => "Program", + Lang::SWEDISH => "Listing", + Lang::TURKISH => "Liste", + Lang::UKRAINIAN => "Лістинг", + Lang::VIETNAMESE => "Chương trình", // TODO: This may be wrong. + Lang::ENGLISH | _ => "Listing", + } + } +} + +impl Figurable for RawElem {} + +impl PlainText for RawElem { + fn plain_text(&self, text: &mut EcoString) { + text.push_str(&self.text()); + } +} + +/// Highlight a syntax node in a theme by calling `f` with ranges and their +/// styles. +fn highlight_themed<F>( + node: &LinkedNode, + scopes: Vec<syntect::parsing::Scope>, + highlighter: &synt::Highlighter, + f: &mut F, +) where + F: FnMut(&LinkedNode, synt::Style), +{ + if node.children().len() == 0 { + let style = highlighter.style_for_stack(&scopes); + f(node, style); + return; + } + + for child in node.children() { + let mut scopes = scopes.clone(); + if let Some(tag) = typst::ide::highlight(&child) { + scopes.push(syntect::parsing::Scope::new(tag.tm_scope()).unwrap()) + } + highlight_themed(&child, scopes, highlighter, f); + } +} + +/// Style a piece of text with a syntect style. +fn styled(piece: &str, foreground: Paint, style: synt::Style) -> Content { + let mut body = TextElem::packed(piece); + + let paint = to_typst(style.foreground).into(); + if paint != foreground { + body = body.styled(TextElem::set_fill(paint)); + } + + if style.font_style.contains(synt::FontStyle::BOLD) { + body = body.strong(); + } + + if style.font_style.contains(synt::FontStyle::ITALIC) { + body = body.emph(); + } + + if style.font_style.contains(synt::FontStyle::UNDERLINE) { + body = body.underlined(); + } + + body +} + +fn to_typst(synt::Color { r, g, b, a }: synt::Color) -> RgbaColor { + RgbaColor { r, g, b, a } +} + +fn to_syn(RgbaColor { r, g, b, a }: RgbaColor) -> synt::Color { + synt::Color { r, g, b, a } +} + +/// The syntect syntax definitions. +/// +/// Code for syntax set generation is below. The `syntaxes` directory is from +/// <https://github.com/sharkdp/bat/tree/master/assets/syntaxes> +/// +/// ```ignore +/// fn main() { +/// let mut builder = syntect::parsing::SyntaxSet::load_defaults_nonewlines().into_builder(); +/// builder.add_from_folder("syntaxes/02_Extra", false).unwrap(); +/// syntect::dumps::dump_to_file(&builder.build(), "syntect.bin").unwrap(); +/// } +/// ``` +/// +/// The following syntaxes are disabled due to compatibility issues: +/// ```text +/// syntaxes/02_Extra/Assembly (ARM).sublime-syntax +/// syntaxes/02_Extra/Elixir/Regular Expressions (Elixir).sublime-syntax +/// syntaxes/02_Extra/JavaScript (Babel).sublime-syntax +/// syntaxes/02_Extra/LiveScript.sublime-syntax +/// syntaxes/02_Extra/PowerShell.sublime-syntax +/// syntaxes/02_Extra/SCSS_Sass/Syntaxes/Sass.sublime-syntax +/// syntaxes/02_Extra/SLS/SLS.sublime-syntax +/// syntaxes/02_Extra/VimHelp.sublime-syntax +/// syntaxes/02_Extra/cmd-help/syntaxes/cmd-help.sublime-syntax +/// ``` +pub static SYNTAXES: Lazy<syntect::parsing::SyntaxSet> = + Lazy::new(|| syntect::dumps::from_binary(include_bytes!("../../assets/syntect.bin"))); + +/// The default theme used for syntax highlighting. +pub static 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), + ], +}); + +/// 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::<RgbaColor>().unwrap())), + background: None, + font_style, + }, + } +} diff --git a/crates/typst-library/src/text/shaping.rs b/crates/typst-library/src/text/shaping.rs new file mode 100644 index 00000000..ec8812fe --- /dev/null +++ b/crates/typst-library/src/text/shaping.rs @@ -0,0 +1,973 @@ +use std::borrow::Cow; +use std::ops::Range; +use std::str::FromStr; + +use az::SaturatingAs; +use rustybuzz::{Feature, Tag, UnicodeBuffer}; +use typst::font::{Font, FontStyle, FontVariant}; +use typst::util::SliceExt; +use unicode_script::{Script, UnicodeScript}; + +use super::{decorate, FontFamily, NumberType, NumberWidth, TextElem}; +use crate::layout::SpanMapper; +use crate::prelude::*; + +/// The result of shaping text. +/// +/// This type contains owned or borrowed shaped text runs, which can be +/// measured, used to reshape substrings more quickly and converted into a +/// frame. +pub struct ShapedText<'a> { + /// The start of the text in the full paragraph. + pub base: usize, + /// The text that was shaped. + pub text: &'a str, + /// The text direction. + pub dir: Dir, + /// The text language. + pub lang: Lang, + /// The text region. + pub region: Option<Region>, + /// The text's style properties. + pub styles: StyleChain<'a>, + /// The font variant. + pub variant: FontVariant, + /// The font size. + pub size: Abs, + /// The width of the text's bounding box. + pub width: Abs, + /// The shaped glyphs. + pub glyphs: Cow<'a, [ShapedGlyph]>, +} + +/// A single glyph resulting from shaping. +#[derive(Debug, Clone)] +pub struct ShapedGlyph { + /// The font the glyph is contained in. + pub font: Font, + /// The glyph's index in the font. + pub glyph_id: u16, + /// The advance width of the glyph. + pub x_advance: Em, + /// The horizontal offset of the glyph. + pub x_offset: Em, + /// The vertical offset of the glyph. + pub y_offset: Em, + /// The adjustability of the glyph. + pub adjustability: Adjustability, + /// The byte range of this glyph's cluster in the full paragraph. A cluster + /// is a sequence of one or multiple glyphs that cannot be separated and + /// must always be treated as a union. + /// + /// The range values of the glyphs in a [`ShapedText`] should not + /// overlap with each other, and they should be monotonically + /// increasing (for left-to-right or top-to-bottom text) or + /// monotonically decreasing (for right-to-left or bottom-to-top + /// text). + pub range: Range<usize>, + /// Whether splitting the shaping result before this glyph would yield the + /// same results as shaping the parts to both sides of `text_index` + /// separately. + pub safe_to_break: bool, + /// The first char in this glyph's cluster. + pub c: char, + /// The source code location of the glyph and its byte offset within it. + pub span: (Span, u16), +} + +#[derive(Debug, Clone, Default)] +pub struct Adjustability { + /// The left and right strechability + pub stretchability: (Em, Em), + /// The left and right shrinkability + pub shrinkability: (Em, Em), +} + +impl ShapedGlyph { + /// Whether the glyph is a space. + pub fn is_space(&self) -> bool { + matches!(self.c, ' ' | '\u{00A0}' | ' ') + } + + /// Whether the glyph is justifiable. + pub fn is_justifiable(&self) -> bool { + // GB style is not relevant here. + self.is_space() + || self.is_cjk_script() + || self.is_cjk_left_aligned_punctuation(true) + || self.is_cjk_right_aligned_punctuation() + || self.is_cjk_center_aligned_punctuation(true) + } + + pub fn is_cjk_script(&self) -> bool { + use Script::*; + // U+30FC: Katakana-Hiragana Prolonged Sound Mark + matches!(self.c.script(), Hiragana | Katakana | Han) || self.c == '\u{30FC}' + } + + pub fn is_cjk_punctuation(&self) -> bool { + self.is_cjk_left_aligned_punctuation(true) + || self.is_cjk_right_aligned_punctuation() + || self.is_cjk_center_aligned_punctuation(true) + } + + /// See <https://www.w3.org/TR/clreq/#punctuation_width_adjustment> + pub fn is_cjk_left_aligned_punctuation(&self, gb_style: bool) -> bool { + // CJK quotation marks shares codepoints with latin quotation marks. + // But only the CJK ones have full width. + if matches!(self.c, '”' | '’') + && self.x_advance + self.stretchability().1 == Em::one() + { + return true; + } + + if gb_style && matches!(self.c, ',' | '。' | '、' | ':' | ';') { + return true; + } + + matches!(self.c, '》' | ')' | '』' | '」') + } + + /// See <https://www.w3.org/TR/clreq/#punctuation_width_adjustment> + pub fn is_cjk_right_aligned_punctuation(&self) -> bool { + // CJK quotation marks shares codepoints with latin quotation marks. + // But only the CJK ones have full width. + if matches!(self.c, '“' | '‘') + && self.x_advance + self.stretchability().0 == Em::one() + { + return true; + } + + matches!(self.c, '《' | '(' | '『' | '「') + } + + /// See <https://www.w3.org/TR/clreq/#punctuation_width_adjustment> + pub fn is_cjk_center_aligned_punctuation(&self, gb_style: bool) -> bool { + if !gb_style && matches!(self.c, ',' | '。' | '、' | ':' | ';') { + return true; + } + + // U+30FB: Katakana Middle Dot + matches!(self.c, '\u{30FB}') + } + + pub fn base_adjustability(&self, gb_style: bool) -> Adjustability { + let width = self.x_advance; + if self.is_space() { + Adjustability { + // The number for spaces is from Knuth-Plass' paper + stretchability: (Em::zero(), width / 2.0), + shrinkability: (Em::zero(), width / 3.0), + } + } else if self.is_cjk_left_aligned_punctuation(gb_style) { + Adjustability { + stretchability: (Em::zero(), Em::zero()), + shrinkability: (Em::zero(), width / 2.0), + } + } else if self.is_cjk_right_aligned_punctuation() { + Adjustability { + stretchability: (Em::zero(), Em::zero()), + shrinkability: (width / 2.0, Em::zero()), + } + } else if self.is_cjk_center_aligned_punctuation(gb_style) { + Adjustability { + stretchability: (Em::zero(), Em::zero()), + shrinkability: (width / 4.0, width / 4.0), + } + } else { + Adjustability::default() + } + } + + /// The stretchability of the character. + pub fn stretchability(&self) -> (Em, Em) { + self.adjustability.stretchability + } + + /// The shrinkability of the character. + pub fn shrinkability(&self) -> (Em, Em) { + self.adjustability.shrinkability + } + + /// Shrink the width of glyph on the left side. + pub fn shrink_left(&mut self, amount: Em) { + self.x_offset -= amount; + self.x_advance -= amount; + self.adjustability.shrinkability.0 -= amount; + self.adjustability.stretchability.0 += amount; + } + + /// Shrink the width of glyph on the right side. + pub fn shrink_right(&mut self, amount: Em) { + self.x_advance -= amount; + self.adjustability.shrinkability.1 -= amount; + self.adjustability.stretchability.1 += amount; + } +} + +/// A side you can go toward. +enum Side { + /// To the left-hand side. + Left, + /// To the right-hand side. + Right, +} + +impl<'a> ShapedText<'a> { + /// Build the shaped text's frame. + /// + /// The `justification` defines how much extra advance width each + /// [justifiable glyph](ShapedGlyph::is_justifiable) will get. + pub fn build( + &self, + vt: &Vt, + justification_ratio: f64, + extra_justification: Abs, + ) -> Frame { + let (top, bottom) = self.measure(vt); + let size = Size::new(self.width, top + bottom); + + let mut offset = Abs::zero(); + let mut frame = Frame::new(size); + frame.set_baseline(top); + + let shift = TextElem::baseline_in(self.styles); + let lang = TextElem::lang_in(self.styles); + let decos = TextElem::deco_in(self.styles); + let fill = TextElem::fill_in(self.styles); + + for ((font, y_offset), group) in + self.glyphs.as_ref().group_by_key(|g| (g.font.clone(), g.y_offset)) + { + let mut range = group[0].range.clone(); + for glyph in group { + range.start = range.start.min(glyph.range.start); + range.end = range.end.max(glyph.range.end); + } + + let pos = Point::new(offset, top + shift - y_offset.at(self.size)); + let glyphs = group + .iter() + .map(|glyph| { + let adjustability_left = if justification_ratio < 0.0 { + glyph.shrinkability().0 + } else { + glyph.stretchability().0 + }; + let adjustability_right = if justification_ratio < 0.0 { + glyph.shrinkability().1 + } else { + glyph.stretchability().1 + }; + + let justification_left = adjustability_left * justification_ratio; + let mut justification_right = + adjustability_right * justification_ratio; + if glyph.is_justifiable() { + justification_right += + Em::from_length(extra_justification, self.size) + } + + frame.size_mut().x += justification_left.at(self.size) + + justification_right.at(self.size); + + Glyph { + id: glyph.glyph_id, + x_advance: glyph.x_advance + + justification_left + + justification_right, + x_offset: glyph.x_offset + justification_left, + range: (glyph.range.start - range.start).saturating_as() + ..(glyph.range.end - range.start).saturating_as(), + span: glyph.span, + } + }) + .collect(); + + let item = TextItem { + font, + size: self.size, + lang, + fill: fill.clone(), + text: self.text[range.start - self.base..range.end - self.base].into(), + glyphs, + }; + + let layer = frame.layer(); + let width = item.width(); + + // Apply line decorations. + for deco in &decos { + decorate(&mut frame, deco, &item, shift, pos, width); + } + + frame.insert(layer, pos, FrameItem::Text(item)); + offset += width; + } + + // Apply metadata. + frame.meta(self.styles, false); + + frame + } + + /// Measure the top and bottom extent of this text. + fn measure(&self, vt: &Vt) -> (Abs, Abs) { + let mut top = Abs::zero(); + let mut bottom = Abs::zero(); + + let top_edge = TextElem::top_edge_in(self.styles); + let bottom_edge = TextElem::bottom_edge_in(self.styles); + + // Expand top and bottom by reading the font's vertical metrics. + let mut expand = |font: &Font| { + let metrics = font.metrics(); + top.set_max(top_edge.resolve(self.styles, metrics)); + bottom.set_max(-bottom_edge.resolve(self.styles, metrics)); + }; + + if self.glyphs.is_empty() { + // When there are no glyphs, we just use the vertical metrics of the + // first available font. + let world = vt.world; + for family in families(self.styles) { + if let Some(font) = world + .book() + .select(family.as_str(), self.variant) + .and_then(|id| world.font(id)) + { + expand(&font); + break; + } + } + } else { + for g in self.glyphs.iter() { + expand(&g.font); + } + } + + (top, bottom) + } + + /// How many glyphs are in the text where we can insert additional + /// space when encountering underfull lines. + pub fn justifiables(&self) -> usize { + self.glyphs.iter().filter(|g| g.is_justifiable()).count() + } + + /// Whether the last glyph is a CJK character which should not be justified + /// on line end. + pub fn cjk_justifiable_at_last(&self) -> bool { + self.glyphs + .last() + .map(|g| g.is_cjk_script() || g.is_cjk_punctuation()) + .unwrap_or(false) + } + + /// The stretchability of the text. + pub fn stretchability(&self) -> Abs { + self.glyphs + .iter() + .map(|g| g.stretchability().0 + g.stretchability().1) + .sum::<Em>() + .at(self.size) + } + + /// The shrinkability of the text + pub fn shrinkability(&self) -> Abs { + self.glyphs + .iter() + .map(|g| g.shrinkability().0 + g.shrinkability().1) + .sum::<Em>() + .at(self.size) + } + + /// Reshape a range of the shaped text, reusing information from this + /// shaping process if possible. + /// + /// The text `range` is relative to the whole paragraph. + pub fn reshape( + &'a self, + vt: &Vt, + spans: &SpanMapper, + text_range: Range<usize>, + ) -> ShapedText<'a> { + let text = &self.text[text_range.start - self.base..text_range.end - self.base]; + if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { + #[cfg(debug_assertions)] + assert_all_glyphs_in_range(glyphs, text, text_range.clone()); + Self { + base: text_range.start, + text, + dir: self.dir, + lang: self.lang, + region: self.region, + styles: self.styles, + size: self.size, + variant: self.variant, + width: glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size), + glyphs: Cow::Borrowed(glyphs), + } + } else { + shape( + vt, + text_range.start, + text, + spans, + self.styles, + self.dir, + self.lang, + self.region, + ) + } + } + + /// Push a hyphen to end of the text. + pub fn push_hyphen(&mut self, vt: &Vt) { + families(self.styles).find_map(|family| { + let world = vt.world; + let font = world + .book() + .select(family.as_str(), self.variant) + .and_then(|id| world.font(id))?; + let ttf = font.ttf(); + let glyph_id = ttf.glyph_index('-')?; + let x_advance = font.to_em(ttf.glyph_hor_advance(glyph_id)?); + let range = self + .glyphs + .last() + .map(|g| g.range.end..g.range.end) + .unwrap_or_default(); + self.width += x_advance.at(self.size); + self.glyphs.to_mut().push(ShapedGlyph { + font, + glyph_id: glyph_id.0, + x_advance, + x_offset: Em::zero(), + y_offset: Em::zero(), + adjustability: Adjustability::default(), + range, + safe_to_break: true, + c: '-', + span: (Span::detached(), 0), + }); + Some(()) + }); + } + + /// Find the subslice of glyphs that represent the given text range if both + /// sides are safe to break. + fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> { + let Range { mut start, mut end } = text_range; + if !self.dir.is_positive() { + std::mem::swap(&mut start, &mut end); + } + + let left = self.find_safe_to_break(start, Side::Left)?; + let right = self.find_safe_to_break(end, Side::Right)?; + Some(&self.glyphs[left..right]) + } + + /// Find the glyph offset matching the text index that is most towards the + /// given side and safe-to-break. + fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> { + let ltr = self.dir.is_positive(); + + // Handle edge cases. + let len = self.glyphs.len(); + if text_index == self.base { + return Some(if ltr { 0 } else { len }); + } else if text_index == self.base + self.text.len() { + return Some(if ltr { len } else { 0 }); + } + + // Find any glyph with the text index. + let mut idx = self + .glyphs + .binary_search_by(|g| { + let ordering = g.range.start.cmp(&text_index); + if ltr { + ordering + } else { + ordering.reverse() + } + }) + .ok()?; + + let next = match towards { + Side::Left => usize::checked_sub, + Side::Right => usize::checked_add, + }; + + // Search for the outermost glyph with the text index. + while let Some(next) = next(idx, 1) { + if self.glyphs.get(next).map_or(true, |g| g.range.start != text_index) { + break; + } + idx = next; + } + + // RTL needs offset one because the left side of the range should be + // exclusive and the right side inclusive, contrary to the normal + // behaviour of ranges. + self.glyphs[idx].safe_to_break.then_some(idx + usize::from(!ltr)) + } +} + +impl Debug for ShapedText<'_> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.text.fmt(f) + } +} + +/// Holds shaping results and metadata common to all shaped segments. +struct ShapingContext<'a, 'v> { + vt: &'a Vt<'v>, + spans: &'a SpanMapper, + glyphs: Vec<ShapedGlyph>, + used: Vec<Font>, + styles: StyleChain<'a>, + size: Abs, + variant: FontVariant, + tags: Vec<rustybuzz::Feature>, + fallback: bool, + dir: Dir, +} + +/// Shape text into [`ShapedText`]. +#[allow(clippy::too_many_arguments)] +pub fn shape<'a>( + vt: &Vt, + base: usize, + text: &'a str, + spans: &SpanMapper, + styles: StyleChain<'a>, + dir: Dir, + lang: Lang, + region: Option<Region>, +) -> ShapedText<'a> { + let size = TextElem::size_in(styles); + let mut ctx = ShapingContext { + vt, + spans, + size, + glyphs: vec![], + used: vec![], + styles, + variant: variant(styles), + tags: tags(styles), + fallback: TextElem::fallback_in(styles), + dir, + }; + + if !text.is_empty() { + shape_segment(&mut ctx, base, text, families(styles)); + } + + track_and_space(&mut ctx); + calculate_adjustability(&mut ctx, lang, region); + + #[cfg(debug_assertions)] + assert_all_glyphs_in_range(&ctx.glyphs, text, base..(base + text.len())); + #[cfg(debug_assertions)] + assert_glyph_ranges_in_order(&ctx.glyphs, dir); + + ShapedText { + base, + text, + dir, + lang, + region, + styles, + variant: ctx.variant, + size, + width: ctx.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(size), + glyphs: Cow::Owned(ctx.glyphs), + } +} + +/// Shape text with font fallback using the `families` iterator. +fn shape_segment( + ctx: &mut ShapingContext, + base: usize, + text: &str, + mut families: impl Iterator<Item = FontFamily> + Clone, +) { + // Fonts dont have newlines and tabs. + if text.chars().all(|c| c == '\n' || c == '\t') { + return; + } + + // Find the next available family. + let world = ctx.vt.world; + let book = world.book(); + let mut selection = families.find_map(|family| { + book.select(family.as_str(), ctx.variant) + .and_then(|id| world.font(id)) + .filter(|font| !ctx.used.contains(font)) + }); + + // Do font fallback if the families are exhausted and fallback is enabled. + if selection.is_none() && ctx.fallback { + let first = ctx.used.first().map(Font::info); + selection = book + .select_fallback(first, ctx.variant, text) + .and_then(|id| world.font(id)) + .filter(|font| !ctx.used.contains(font)); + } + + // Extract the font id or shape notdef glyphs if we couldn't find any font. + let Some(font) = selection else { + if let Some(font) = ctx.used.first().cloned() { + shape_tofus(ctx, base, text, font); + } + return; + }; + + ctx.used.push(font.clone()); + + // Fill the buffer with our text. + let mut buffer = UnicodeBuffer::new(); + buffer.push_str(text); + buffer.set_language(language(ctx.styles)); + buffer.set_direction(match ctx.dir { + Dir::LTR => rustybuzz::Direction::LeftToRight, + Dir::RTL => rustybuzz::Direction::RightToLeft, + _ => unimplemented!("vertical text layout"), + }); + + // Shape! + let buffer = rustybuzz::shape(font.rusty(), &ctx.tags, buffer); + let infos = buffer.glyph_infos(); + let pos = buffer.glyph_positions(); + let ltr = ctx.dir.is_positive(); + + // Collect the shaped glyphs, doing fallback and shaping parts again with + // the next font if necessary. + let mut i = 0; + while i < infos.len() { + let info = &infos[i]; + let cluster = info.cluster as usize; + + // Add the glyph to the shaped output. + if info.glyph_id != 0 { + // Determine the text range of the glyph. + let start = base + cluster; + let end = base + + if ltr { i.checked_add(1) } else { i.checked_sub(1) } + .and_then(|last| infos.get(last)) + .map_or(text.len(), |info| info.cluster as usize); + + ctx.glyphs.push(ShapedGlyph { + font: font.clone(), + glyph_id: info.glyph_id as u16, + // TODO: Don't ignore y_advance. + x_advance: font.to_em(pos[i].x_advance), + x_offset: font.to_em(pos[i].x_offset), + y_offset: font.to_em(pos[i].y_offset), + adjustability: Adjustability::default(), + range: start..end, + safe_to_break: !info.unsafe_to_break(), + c: text[cluster..].chars().next().unwrap(), + span: ctx.spans.span_at(start), + }); + } else { + // First, search for the end of the tofu sequence. + let k = i; + while infos.get(i + 1).map_or(false, |info| info.glyph_id == 0) { + i += 1; + } + + // Then, determine the start and end text index for the tofu + // sequence. + // + // Examples: + // Everything is shown in visual order. Tofus are written as "_". + // We want to find out that the tofus span the text `2..6`. + // Note that the clusters are longer than 1 char. + // + // Left-to-right: + // Text: h a l i h a l l o + // Glyphs: A _ _ C E + // Clusters: 0 2 4 6 8 + // k=1 i=2 + // + // Right-to-left: + // Text: O L L A H I L A H + // Glyphs: E C _ _ A + // Clusters: 8 6 4 2 0 + // k=2 i=3 + let start = infos[if ltr { k } else { i }].cluster as usize; + let end = if ltr { i.checked_add(1) } else { k.checked_sub(1) } + .and_then(|last| infos.get(last)) + .map_or(text.len(), |info| info.cluster as usize); + + // Trim half-baked cluster. + let remove = base + start..base + end; + while ctx.glyphs.last().map_or(false, |g| remove.contains(&g.range.start)) { + ctx.glyphs.pop(); + } + + // Recursively shape the tofu sequence with the next family. + shape_segment(ctx, base + start, &text[start..end], families.clone()); + } + + i += 1; + } + + ctx.used.pop(); +} + +/// Shape the text with tofus from the given font. +fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) { + let x_advance = font.advance(0).unwrap_or_default(); + let add_glyph = |(cluster, c): (usize, char)| { + let start = base + cluster; + let end = start + c.len_utf8(); + ctx.glyphs.push(ShapedGlyph { + font: font.clone(), + glyph_id: 0, + x_advance, + x_offset: Em::zero(), + y_offset: Em::zero(), + adjustability: Adjustability::default(), + range: start..end, + safe_to_break: true, + c, + span: ctx.spans.span_at(start), + }); + }; + if ctx.dir.is_positive() { + text.char_indices().for_each(add_glyph); + } else { + text.char_indices().rev().for_each(add_glyph); + } +} + +/// Apply tracking and spacing to the shaped glyphs. +fn track_and_space(ctx: &mut ShapingContext) { + let tracking = Em::from_length(TextElem::tracking_in(ctx.styles), ctx.size); + let spacing = + TextElem::spacing_in(ctx.styles).map(|abs| Em::from_length(abs, ctx.size)); + + let mut glyphs = ctx.glyphs.iter_mut().peekable(); + while let Some(glyph) = glyphs.next() { + // Make non-breaking space same width as normal space. + if glyph.c == '\u{00A0}' { + glyph.x_advance -= nbsp_delta(&glyph.font).unwrap_or_default(); + } + + if glyph.is_space() { + glyph.x_advance = spacing.relative_to(glyph.x_advance); + } + + if glyphs + .peek() + .map_or(false, |next| glyph.range.start != next.range.start) + { + glyph.x_advance += tracking; + } + } +} + +pub fn is_gb_style(lang: Lang, region: Option<Region>) -> bool { + // Most CJK variants, including zh-CN, ja-JP, zh-SG, zh-MY use GB-style punctuation, + // while zh-HK and zh-TW use alternative style. We default to use GB-style. + !(lang == Lang::CHINESE + && matches!(region.as_ref().map(Region::as_str), Some("TW" | "HK"))) +} + +/// Calculate stretchability and shrinkability of each glyph, +/// and CJK punctuation adjustments according to Chinese Layout Requirements. +fn calculate_adjustability(ctx: &mut ShapingContext, lang: Lang, region: Option<Region>) { + let gb_style = is_gb_style(lang, region); + + for glyph in &mut ctx.glyphs { + glyph.adjustability = glyph.base_adjustability(gb_style); + } + + let mut glyphs = ctx.glyphs.iter_mut().peekable(); + while let Some(glyph) = glyphs.next() { + // Only GB style needs further adjustment. + if glyph.is_cjk_punctuation() && !gb_style { + continue; + } + + // Now we apply consecutive punctuation adjustment, specified in Chinese Layout + // Requirements, section 3.1.6.1 Punctuation Adjustment Space, and Japanese Layout + // Requirements, section 3.1 Line Composition Rules for Punctuation Marks + let Some(next) = glyphs.peek_mut() else { continue }; + let width = glyph.x_advance; + let delta = width / 2.0; + if glyph.is_cjk_punctuation() + && next.is_cjk_punctuation() + && (glyph.shrinkability().1 + next.shrinkability().0) >= delta + { + let left_delta = glyph.shrinkability().1.min(delta); + glyph.shrink_right(left_delta); + next.shrink_left(delta - left_delta); + } + } +} + +/// Difference between non-breaking and normal space. +fn nbsp_delta(font: &Font) -> Option<Em> { + let space = font.ttf().glyph_index(' ')?.0; + let nbsp = font.ttf().glyph_index('\u{00A0}')?.0; + Some(font.advance(nbsp)? - font.advance(space)?) +} + +/// 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 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) { + variant.style = match variant.style { + FontStyle::Normal => FontStyle::Italic, + FontStyle::Italic => FontStyle::Normal, + FontStyle::Oblique => FontStyle::Normal, + } + } + + variant +} + +/// Resolve a prioritized iterator over the font families. +pub fn families(styles: StyleChain) -> impl Iterator<Item = FontFamily> + Clone { + const FALLBACKS: &[&str] = &[ + "linux libertine", + "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() + .chain(tail.iter().copied().map(FontFamily::new)) +} + +/// Collect the tags of the OpenType features to apply. +fn tags(styles: StyleChain) -> Vec<Feature> { + let mut tags = vec![]; + let mut feat = |tag, value| { + 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); + } + + let storage; + if let Some(set) = TextElem::stylistic_set_in(styles) { + storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 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"hilg", 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 +} + +/// Process the language and and region of a style chain into a +/// rustybuzz-compatible BCP 47 language. +fn language(styles: StyleChain) -> rustybuzz::Language { + let mut bcp: EcoString = TextElem::lang_in(styles).as_str().into(); + if let Some(region) = TextElem::region_in(styles) { + bcp.push('-'); + bcp.push_str(region.as_str()); + } + rustybuzz::Language::from_str(&bcp).unwrap() +} + +/// Returns true if all glyphs in `glyphs` have ranges within the range `range`. +#[cfg(debug_assertions)] +fn assert_all_glyphs_in_range(glyphs: &[ShapedGlyph], text: &str, range: Range<usize>) { + if glyphs + .iter() + .any(|g| g.range.start < range.start || g.range.end > range.end) + { + panic!("one or more glyphs in {text:?} fell out of range"); + } +} + +/// Asserts that the ranges of `glyphs` is in the proper order according to `dir`. +/// +/// This asserts instead of returning a bool in order to provide a more informative message when the invariant is violated. +#[cfg(debug_assertions)] +fn assert_glyph_ranges_in_order(glyphs: &[ShapedGlyph], dir: Dir) { + if glyphs.is_empty() { + return; + } + + // Iterator::is_sorted and friends are unstable as of Rust 1.70.0 + for i in 0..(glyphs.len() - 1) { + let a = &glyphs[i]; + let b = &glyphs[i + 1]; + let ord = a.range.start.cmp(&b.range.start); + let ord = if dir.is_positive() { ord } else { ord.reverse() }; + if ord == std::cmp::Ordering::Greater { + panic!( + "glyph ranges should be monotonically {}, \ + but found glyphs out of order:\n\n\ + first: {a:#?}\nsecond: {b:#?}", + if dir.is_positive() { "increasing" } else { "decreasing" }, + ); + } + } +} diff --git a/crates/typst-library/src/text/shift.rs b/crates/typst-library/src/text/shift.rs new file mode 100644 index 00000000..65e309e1 --- /dev/null +++ b/crates/typst-library/src/text/shift.rs @@ -0,0 +1,229 @@ +use super::{variant, SpaceElem, TextElem, TextSize}; +use crate::prelude::*; + +/// Renders text in subscript. +/// +/// The text is rendered smaller and its baseline is lowered. +/// +/// ## Example { #example } +/// ```example +/// Revenue#sub[yearly] +/// ``` +/// +/// Display: Subscript +/// Category: text +#[element(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 SubElem { + #[tracing::instrument(name = "SubElem::show", skip_all)] + fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + let body = self.body(); + let mut transformed = None; + if self.typographic(styles) { + if let Some(text) = search_text(&body, true) { + if is_shapable(vt, &text, styles) { + transformed = Some(TextElem::packed(text)); + } + } + }; + + Ok(transformed.unwrap_or_else(|| { + 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 } +/// ```example +/// 1#super[st] try! +/// ``` +/// +/// Display: Superscript +/// Category: text +#[element(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 SuperElem { + #[tracing::instrument(name = "SuperElem::show", skip_all)] + fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + let body = self.body(); + let mut transformed = None; + if self.typographic(styles) { + if let Some(text) = search_text(&body, false) { + if is_shapable(vt, &text, styles) { + transformed = Some(TextElem::packed(text)); + } + } + }; + + Ok(transformed.unwrap_or_else(|| { + 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` leafs. +fn search_text(content: &Content, sub: bool) -> Option<EcoString> { + if content.is::<SpaceElem>() { + Some(' '.into()) + } else if let Some(elem) = content.to::<TextElem>() { + convert_script(&elem.text(), sub) + } else if let Some(children) = content.to_sequence() { + let mut full = EcoString::new(); + for item in children { + match search_text(item, sub) { + Some(text) => full.push_str(&text), + None => return None, + } + } + Some(full) + } else { + None + } +} + +/// Checks whether the first retrievable family contains all code points of the +/// given string. +fn is_shapable(vt: &Vt, text: &str, styles: StyleChain) -> bool { + let world = vt.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 string to sub- or superscript codepoints if all characters +/// can be mapped to such a codepoint. +fn convert_script(text: &str, sub: bool) -> Option<EcoString> { + let mut result = EcoString::with_capacity(text.len()); + let converter = if sub { to_subscript_codepoint } else { to_superscript_codepoint }; + + for c in text.chars() { + match converter(c) { + Some(c) => result.push(c), + None => return None, + } + } + + Some(result) +} + +/// Convert a character to its corresponding Unicode superscript. +fn to_superscript_codepoint(c: char) -> Option<char> { + char::from_u32(match c { + '0' => 0x2070, + '1' => 0x00B9, + '2' => 0x00B2, + '3' => 0x00B3, + '4'..='9' => 0x2070 + (c as u32 + 4 - '4' as u32), + '+' => 0x207A, + '-' => 0x207B, + '=' => 0x207C, + '(' => 0x207D, + ')' => 0x207E, + 'n' => 0x207F, + 'i' => 0x2071, + ' ' => 0x0020, + _ => return None, + }) +} + +/// Convert a character to its corresponding Unicode subscript. +fn to_subscript_codepoint(c: char) -> Option<char> { + char::from_u32(match c { + '0' => 0x2080, + '1'..='9' => 0x2080 + (c as u32 - '0' as u32), + '+' => 0x208A, + '-' => 0x208B, + '=' => 0x208C, + '(' => 0x208D, + ')' => 0x208E, + 'a' => 0x2090, + 'e' => 0x2091, + 'o' => 0x2092, + 'x' => 0x2093, + 'h' => 0x2095, + 'k' => 0x2096, + 'l' => 0x2097, + 'm' => 0x2098, + 'n' => 0x2099, + 'p' => 0x209A, + 's' => 0x209B, + 't' => 0x209C, + ' ' => 0x0020, + _ => return None, + }) +} diff --git a/crates/typst-library/src/visualize/image.rs b/crates/typst-library/src/visualize/image.rs new file mode 100644 index 00000000..b169cc4d --- /dev/null +++ b/crates/typst-library/src/visualize/image.rs @@ -0,0 +1,197 @@ +use std::ffi::OsStr; +use std::path::Path; + +use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat}; +use typst::util::Bytes; + +use crate::meta::{Figurable, LocalName}; +use crate::prelude::*; +use crate::text::families; + +/// A raster or vector graphic. +/// +/// Supported formats are PNG, JPEG, GIF and SVG. +/// +/// _Note:_ Work on SVG export is ongoing and there might be visual inaccuracies +/// in the resulting PDF. Make sure to double-check embedded SVG images. If you +/// have an issue, also feel free to report it on [GitHub][gh-svg]. +/// +/// ## Example { #example } +/// ```example +/// #figure( +/// image("molecular.jpg", width: 80%), +/// caption: [ +/// A step in the molecular testing +/// pipeline of our lab. +/// ], +/// ) +/// ``` +/// +/// [gh-svg]: https://github.com/typst/typst/issues?q=is%3Aopen+is%3Aissue+label%3Asvg +/// +/// Display: Image +/// Category: visualize +#[element(Layout, LocalName, Figurable)] +pub struct ImageElem { + /// Path to an image file. + #[required] + #[parse( + let Spanned { v: path, span } = + args.expect::<Spanned<EcoString>>("path to image file")?; + let id = vm.location().join(&path).at(span)?; + let data = vm.world().file(id).at(span)?; + path + )] + pub path: EcoString, + + /// The raw file data. + #[internal] + #[required] + #[parse(data)] + pub data: Bytes, + + /// The width of the image. + pub width: Smart<Rel<Length>>, + + /// The height of the image. + pub height: Smart<Rel<Length>>, + + /// A text describing the image. + pub alt: Option<EcoString>, + + /// How the image should adjust itself to a given area. + #[default(ImageFit::Cover)] + pub fit: ImageFit, +} + +impl Layout for ImageElem { + #[tracing::instrument(name = "ImageElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let ext = Path::new(self.path().as_str()) + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default() + .to_lowercase(); + + let format = match ext.as_str() { + "png" => ImageFormat::Raster(RasterFormat::Png), + "jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg), + "gif" => ImageFormat::Raster(RasterFormat::Gif), + "svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg), + _ => bail!(self.span(), "unknown image format"), + }; + + let image = Image::with_fonts( + self.data(), + format, + vt.world, + families(styles).next().as_ref().map(|f| f.as_str()), + self.alt(styles), + ) + .at(self.span())?; + + let sizing = Axes::new(self.width(styles), self.height(styles)); + let region = sizing + .zip(regions.base()) + .map(|(s, r)| s.map(|v| v.resolve(styles).relative_to(r))) + .unwrap_or(regions.base()); + + let expand = sizing.as_ref().map(Smart::is_custom) | regions.expand; + let region_ratio = region.x / region.y; + + // Find out whether the image is wider or taller than the target size. + let pxw = image.width() as f64; + let pxh = image.height() as f64; + let px_ratio = pxw / pxh; + let wide = px_ratio > region_ratio; + + // The space into which the image will be placed according to its fit. + let target = if expand.x && expand.y { + region + } else if expand.x || (!expand.y && wide && region.x.is_finite()) { + Size::new(region.x, region.y.min(region.x.safe_div(px_ratio))) + } else if region.y.is_finite() { + Size::new(region.x.min(region.y * px_ratio), region.y) + } else { + Size::new(Abs::pt(pxw), Abs::pt(pxh)) + }; + + // Compute the actual size of the fitted image. + let fit = self.fit(styles); + let fitted = match fit { + ImageFit::Cover | ImageFit::Contain => { + if wide == (fit == ImageFit::Contain) { + Size::new(target.x, target.x / px_ratio) + } else { + Size::new(target.y * px_ratio, target.y) + } + } + ImageFit::Stretch => target, + }; + + // First, place the image in a frame of exactly its size and then resize + // the frame to the target size, center aligning the image in the + // process. + let mut frame = Frame::new(fitted); + frame.push(Point::zero(), FrameItem::Image(image, fitted, self.span())); + frame.resize(target, Align::CENTER_HORIZON); + + // Create a clipping group if only part of the image should be visible. + if fit == ImageFit::Cover && !target.fits(fitted) { + frame.clip(); + } + + // Apply metadata. + frame.meta(styles, false); + + Ok(Fragment::frame(frame)) + } +} + +impl LocalName for ImageElem { + fn local_name(&self, lang: Lang, _: Option<Region>) -> &'static str { + match lang { + Lang::ALBANIAN => "Figurë", + Lang::ARABIC => "شكل", + Lang::BOKMÅL => "Figur", + Lang::CHINESE => "图", + Lang::CZECH => "Obrázek", + Lang::DANISH => "Figur", + Lang::DUTCH => "Figuur", + Lang::FILIPINO => "Pigura", + Lang::FRENCH => "Figure", + Lang::GERMAN => "Abbildung", + Lang::ITALIAN => "Figura", + Lang::NYNORSK => "Figur", + Lang::POLISH => "Rysunek", + Lang::PORTUGUESE => "Figura", + Lang::RUSSIAN => "Рисунок", + Lang::SLOVENIAN => "Slika", + Lang::SPANISH => "Figura", + Lang::SWEDISH => "Figur", + Lang::TURKISH => "Şekil", + Lang::UKRAINIAN => "Рисунок", + Lang::VIETNAMESE => "Hình", + Lang::ENGLISH | _ => "Figure", + } + } +} + +impl Figurable for 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. This is the default. + Cover, + /// The image should be fully contained in the area. + Contain, + /// The image should be stretched so that it exactly fills the area, even if + /// this means that the image will be distorted. + Stretch, +} diff --git a/crates/typst-library/src/visualize/line.rs b/crates/typst-library/src/visualize/line.rs new file mode 100644 index 00000000..62a381a9 --- /dev/null +++ b/crates/typst-library/src/visualize/line.rs @@ -0,0 +1,118 @@ +use crate::prelude::*; + +/// A line from one point to another. +/// +/// ## Example { #example } +/// ```example +/// #set page(height: 100pt) +/// +/// #line(length: 100%) +/// #line(end: (50%, 50%)) +/// #line( +/// length: 4cm, +/// stroke: 2pt + maroon, +/// ) +/// ``` +/// +/// Display: Line +/// Category: visualize +#[element(Layout)] +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. This can be: + /// + /// - 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}`. + /// - A stroke described by a dictionary with any of the following keys: + /// - `paint`: The [color]($type/color) to use for the stroke. + /// - `thickness`: The stroke's thickness as a [length]($type/length). + /// - `cap`: How the line terminates. One of `{"butt"}`, `{"round"}`, or + /// `{"square"}`. + /// - `join`: How sharp turns of a contour are rendered. One of + /// `{"miter"}`, `{"round"}`, or `{"bevel"}`. Not applicable to lines + /// but to [polygons]($func/polygon) or [paths]($func/path). + /// - `miter-limit`: Number at which protruding sharp angles are rendered + /// with a bevel instead. The higher the number, the sharper an angle + /// can be before it is bevelled. Only applicable if `join` is + /// `{"miter"}`. Defaults to `{4.0}`. + /// - `dash`: The dash pattern to use. Can be any of the following: + /// - One of the predefined patterns `{"solid"}`, `{"dotted"}`, + /// `{"densely-dotted"}`, `{"loosely-dotted"}`, `{"dashed"}`, + /// `{"densely-dashed"}`, `{"loosely-dashed"}`, `{"dash-dotted"}`, + /// `{"densely-dash-dotted"}` or `{"loosely-dash-dotted"}` + /// - An [array]($type/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]($type/dictionary) with the keys `array` (same as + /// the array above), and `phase` (of type [length]($type/length)), + /// which defines where in the pattern to start drawing. + /// + /// ```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: PartialStroke, +} + +impl Layout for LineElem { + #[tracing::instrument(name = "LineElem::layout", skip_all)] + fn layout( + &self, + _: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let resolve = |axes: Axes<Rel<Abs>>| { + axes.zip(regions.base()).map(|(l, b)| l.relative_to(b)) + }; + + let start = resolve(self.start(styles)); + let delta = + self.end(styles).map(|end| resolve(end) - start).unwrap_or_else(|| { + let length = self.length(styles); + let angle = self.angle(styles); + let x = angle.cos() * length; + let y = angle.sin() * length; + resolve(Axes::new(x, y)) + }); + + let stroke = self.stroke(styles).unwrap_or_default(); + let size = start.max(start + delta).max(Size::zero()); + let target = regions.expand.select(regions.size, size); + + let mut frame = Frame::new(target); + let shape = Geometry::Line(delta.to_point()).stroked(stroke); + frame.push(start.to_point(), FrameItem::Shape(shape, self.span())); + Ok(Fragment::frame(frame)) + } +} diff --git a/crates/typst-library/src/visualize/mod.rs b/crates/typst-library/src/visualize/mod.rs new file mode 100644 index 00000000..ea873f44 --- /dev/null +++ b/crates/typst-library/src/visualize/mod.rs @@ -0,0 +1,45 @@ +//! Drawing and visualization. + +mod image; +mod line; +mod path; +mod polygon; +mod shape; + +pub use self::image::*; +pub use self::line::*; +pub use self::path::*; +pub use self::polygon::*; +pub use self::shape::*; + +use crate::prelude::*; + +/// Hook up all visualize definitions. +pub(super) fn define(global: &mut Scope) { + global.define("image", ImageElem::func()); + global.define("line", LineElem::func()); + global.define("rect", RectElem::func()); + global.define("square", SquareElem::func()); + global.define("ellipse", EllipseElem::func()); + global.define("circle", CircleElem::func()); + global.define("polygon", PolygonElem::func()); + global.define("path", PathElem::func()); + 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); +} diff --git a/crates/typst-library/src/visualize/path.rs b/crates/typst-library/src/visualize/path.rs new file mode 100644 index 00000000..d78abce1 --- /dev/null +++ b/crates/typst-library/src/visualize/path.rs @@ -0,0 +1,211 @@ +use kurbo::{CubicBez, ParamCurveExtrema}; +use typst::eval::Reflect; + +use crate::prelude::*; + +use PathVertex::{AllControlPoints, MirroredControlPoint, Vertex}; + +/// A path through a list of points, connected by Bezier curves. +/// +/// ## Example { #example } +/// ```example +/// #path( +/// fill: blue.lighten(80%), +/// stroke: blue, +/// closed: true, +/// (0pt, 50pt), +/// (100%, 50pt), +/// ((50%, 0pt), (40pt, 0pt)), +/// ) +/// ``` +/// +/// Display: Path +/// Category: visualize +#[element(Layout)] +pub struct PathElem { + /// How to fill the path. See the + /// [rectangle's documentation]($func/rect.fill) for more details. + /// + /// Currently all paths are filled according to the + /// [non-zero winding rule](https://en.wikipedia.org/wiki/Nonzero-rule). + pub fill: Option<Paint>, + + /// How to stroke the path. This can be: + /// + /// See the [line's documentation]($func/line.stroke) for more details. 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<PartialStroke>>, + + /// 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`]($func/line) or + /// [`polygon`]($func/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 Layout for PathElem { + #[tracing::instrument(name = "PathElem::layout", skip_all)] + fn layout( + &self, + _: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let resolve = |axes: Axes<Rel<Length>>| { + axes.resolve(styles) + .zip(regions.base()) + .map(|(l, b)| l.relative_to(b)) + .to_point() + }; + + let vertices: Vec<PathVertex> = self.vertices(); + let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect(); + + let mut size = Size::zero(); + if points.is_empty() { + return Ok(Fragment::frame(Frame::new(size))); + } + + // Only create a path if there are more than zero points. + // Construct a closed path given all points. + let mut path = Path::new(); + path.move_to(points[0]); + + let mut add_cubic = + |from_point: Point, to_point: Point, from: PathVertex, to: PathVertex| { + let from_control_point = resolve(from.control_point_from()) + from_point; + let to_control_point = resolve(to.control_point_to()) + to_point; + path.cubic_to(from_control_point, to_control_point, to_point); + + let p0 = kurbo::Point::new(from_point.x.to_raw(), from_point.y.to_raw()); + let p1 = kurbo::Point::new( + from_control_point.x.to_raw(), + from_control_point.y.to_raw(), + ); + let p2 = kurbo::Point::new( + to_control_point.x.to_raw(), + to_control_point.y.to_raw(), + ); + let p3 = kurbo::Point::new(to_point.x.to_raw(), to_point.y.to_raw()); + let extrema = CubicBez::new(p0, p1, p2, p3).bounding_box(); + size.x.set_max(Abs::raw(extrema.x1)); + size.y.set_max(Abs::raw(extrema.y1)); + }; + + for (vertex_window, point_window) in vertices.windows(2).zip(points.windows(2)) { + let from = vertex_window[0]; + let to = vertex_window[1]; + let from_point = point_window[0]; + let to_point = point_window[1]; + + add_cubic(from_point, to_point, from, to); + } + + if self.closed(styles) { + let from = *vertices.last().unwrap(); // We checked that we have at least one element. + let to = vertices[0]; + let from_point = *points.last().unwrap(); + let to_point = points[0]; + + add_cubic(from_point, to_point, from, to); + path.close_path(); + } + + // Prepare fill and stroke. + let fill = self.fill(styles); + let stroke = match self.stroke(styles) { + Smart::Auto if fill.is_none() => Some(Stroke::default()), + Smart::Auto => None, + Smart::Custom(stroke) => stroke.map(PartialStroke::unwrap_or_default), + }; + + let mut frame = Frame::new(size); + let shape = Shape { geometry: Geometry::Path(path), stroke, fill }; + frame.push(Point::zero(), FrameItem::Shape(shape, self.span())); + + Ok(Fragment::frame(frame)) + } +} + +#[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"), + } + }, +} diff --git a/crates/typst-library/src/visualize/polygon.rs b/crates/typst-library/src/visualize/polygon.rs new file mode 100644 index 00000000..d2d3132a --- /dev/null +++ b/crates/typst-library/src/visualize/polygon.rs @@ -0,0 +1,93 @@ +use crate::prelude::*; + +/// A closed polygon. +/// +/// The polygon is defined by its corner points and is closed automatically. +/// +/// ## Example { #example } +/// ```example +/// #polygon( +/// fill: blue.lighten(80%), +/// stroke: blue, +/// (20%, 0pt), +/// (60%, 0pt), +/// (80%, 2cm), +/// (0%, 2cm), +/// ) +/// ``` +/// +/// Display: Polygon +/// Category: visualize +#[element(Layout)] +pub struct PolygonElem { + /// How to fill the polygon. See the + /// [rectangle's documentation]($func/rect.fill) for more details. + /// + /// Currently all polygons are filled according to the + /// [non-zero winding rule](https://en.wikipedia.org/wiki/Nonzero-rule). + pub fill: Option<Paint>, + + /// How to stroke the polygon. This can be: + /// + /// See the [line's documentation]($func/line.stroke) for more details. 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<PartialStroke>>, + + /// The vertices of the polygon. Each point is specified as an array of two + /// [relative lengths]($type/relative-length). + #[variadic] + pub vertices: Vec<Axes<Rel<Length>>>, +} + +impl Layout for PolygonElem { + #[tracing::instrument(name = "PolygonElem::layout", skip_all)] + fn layout( + &self, + _: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let points: Vec<Point> = self + .vertices() + .iter() + .map(|c| { + c.resolve(styles) + .zip(regions.base()) + .map(|(l, b)| l.relative_to(b)) + .to_point() + }) + .collect(); + + let size = points.iter().fold(Point::zero(), |max, c| c.max(max)).to_size(); + let mut frame = Frame::new(size); + + // Only create a path if there are more than zero points. + if points.is_empty() { + return Ok(Fragment::frame(frame)); + } + + // Prepare fill and stroke. + let fill = self.fill(styles); + let stroke = match self.stroke(styles) { + Smart::Auto if fill.is_none() => Some(Stroke::default()), + Smart::Auto => None, + Smart::Custom(stroke) => stroke.map(PartialStroke::unwrap_or_default), + }; + + // Construct a closed path given all points. + let mut path = Path::new(); + path.move_to(points[0]); + for &point in &points[1..] { + path.line_to(point); + } + path.close_path(); + + let shape = Shape { geometry: Geometry::Path(path), stroke, fill }; + frame.push(Point::zero(), FrameItem::Shape(shape, self.span())); + + Ok(Fragment::frame(frame)) + } +} diff --git a/crates/typst-library/src/visualize/shape.rs b/crates/typst-library/src/visualize/shape.rs new file mode 100644 index 00000000..6129b70b --- /dev/null +++ b/crates/typst-library/src/visualize/shape.rs @@ -0,0 +1,569 @@ +use std::f64::consts::SQRT_2; + +use crate::prelude::*; + +/// A rectangle with optional content. +/// +/// ## Example { #example } +/// ```example +/// // Without content. +/// #rect(width: 35%, height: 30pt) +/// +/// // With content. +/// #rect[ +/// Automatically sized \ +/// to fit the content. +/// ] +/// ``` +/// +/// Display: Rectangle +/// Category: visualize +#[element(Layout)] +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: Smart<Rel<Length>>, + + /// 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 that can also be used for + /// [lines]($func/line.stroke). + /// - A dictionary describing the stroke for each side inidvidually. 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<PartialStroke>>>>, + + /// 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. + /// + /// _Note:_ When the rectangle contains text, its exact size depends on the + /// current [text edges]($func/text.top-edge). + /// + /// ```example + /// #rect(inset: 0pt)[Tight] + /// ``` + #[resolve] + #[fold] + #[default(Sides::splat(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]($func/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] + pub body: Option<Content>, +} + +impl Layout for RectElem { + #[tracing::instrument(name = "RectElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + layout( + vt, + styles, + regions, + ShapeKind::Rect, + &self.body(styles), + Axes::new(self.width(styles), self.height(styles)), + self.fill(styles), + self.stroke(styles), + self.inset(styles), + self.outset(styles), + self.radius(styles), + self.span(), + ) + } +} + +/// A square with optional content. +/// +/// ## Example { #example } +/// ```example +/// // Without content. +/// #square(size: 40pt) +/// +/// // With content. +/// #square[ +/// Automatically \ +/// sized to fit. +/// ] +/// ``` +/// +/// Display: Square +/// Category: visualize +#[element(Layout)] +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, + })] + pub height: Smart<Rel<Length>>, + + /// How to fill the square. See the + /// [rectangle's documentation]($func/rect.fill) for more details. + pub fill: Option<Paint>, + + /// How to stroke the square. See the [rectangle's + /// documentation]($func/rect.stroke) for more details. + #[resolve] + #[fold] + pub stroke: Smart<Sides<Option<Option<PartialStroke>>>>, + + /// How much to round the square's corners. See the [rectangle's + /// documentation]($func/rect.radius) for more details. + #[resolve] + #[fold] + pub radius: Corners<Option<Rel<Length>>>, + + /// How much to pad the square's content. See the [rectangle's + /// documentation]($func/rect.inset) for more details. + #[resolve] + #[fold] + #[default(Sides::splat(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 [rectangle's documentation]($func/rect.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] + pub body: Option<Content>, +} + +impl Layout for SquareElem { + #[tracing::instrument(name = "SquareElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + layout( + vt, + styles, + regions, + ShapeKind::Square, + &self.body(styles), + Axes::new(self.width(styles), self.height(styles)), + self.fill(styles), + self.stroke(styles), + self.inset(styles), + self.outset(styles), + self.radius(styles), + self.span(), + ) + } +} + +/// An ellipse with optional content. +/// +/// ## Example { #example } +/// ```example +/// // Without content. +/// #ellipse(width: 35%, height: 30pt) +/// +/// // With content. +/// #ellipse[ +/// #set align(center) +/// Automatically sized \ +/// to fit the content. +/// ] +/// ``` +/// +/// Display: Ellipse +/// Category: visualize +#[element(Layout)] +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: Smart<Rel<Length>>, + + /// How to fill the ellipse. See the + /// [rectangle's documentation]($func/rect.fill) for more details. + pub fill: Option<Paint>, + + /// How to stroke the ellipse. See the [rectangle's + /// documentation]($func/rect.stroke) for more details. + #[resolve] + #[fold] + pub stroke: Smart<Option<PartialStroke>>, + + /// How much to pad the ellipse's content. See the [rectangle's + /// documentation]($func/rect.inset) for more details. + #[resolve] + #[fold] + #[default(Sides::splat(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 [rectangle's documentation]($func/rect.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] + pub body: Option<Content>, +} + +impl Layout for EllipseElem { + #[tracing::instrument(name = "EllipseElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + layout( + vt, + styles, + regions, + ShapeKind::Ellipse, + &self.body(styles), + Axes::new(self.width(styles), self.height(styles)), + self.fill(styles), + self.stroke(styles).map(Sides::splat), + self.inset(styles), + self.outset(styles), + Corners::splat(Rel::zero()), + self.span(), + ) + } +} + +/// A circle with optional content. +/// +/// ## Example { #example } +/// ```example +/// // Without content. +/// #circle(radius: 25pt) +/// +/// // With content. +/// #circle[ +/// #set align(center + horizon) +/// Automatically \ +/// sized to fit. +/// ] +/// ``` +/// +/// Display: Circle +/// Category: visualize +#[element(Layout)] +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, + })] + pub height: Smart<Rel<Length>>, + + /// How to fill the circle. See the + /// [rectangle's documentation]($func/rect.fill) for more details. + pub fill: Option<Paint>, + + /// How to stroke the circle. See the [rectangle's + /// documentation]($func/rect.stroke) for more details. + #[resolve] + #[fold] + #[default(Smart::Auto)] + pub stroke: Smart<Option<PartialStroke>>, + + /// How much to pad the circle's content. See the [rectangle's + /// documentation]($func/rect.inset) for more details. + #[resolve] + #[fold] + #[default(Sides::splat(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 [rectangle's documentation]($func/rect.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] + pub body: Option<Content>, +} + +impl Layout for CircleElem { + #[tracing::instrument(name = "CircleElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + layout( + vt, + styles, + regions, + ShapeKind::Circle, + &self.body(styles), + Axes::new(self.width(styles), self.height(styles)), + self.fill(styles), + self.stroke(styles).map(Sides::splat), + self.inset(styles), + self.outset(styles), + Corners::splat(Rel::zero()), + self.span(), + ) + } +} + +/// Layout a shape. +#[tracing::instrument(name = "shape::layout", skip_all)] +#[allow(clippy::too_many_arguments)] +fn layout( + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + kind: ShapeKind, + body: &Option<Content>, + sizing: Axes<Smart<Rel<Length>>>, + fill: Option<Paint>, + stroke: Smart<Sides<Option<PartialStroke<Abs>>>>, + mut inset: Sides<Rel<Abs>>, + outset: Sides<Rel<Abs>>, + radius: Corners<Rel<Abs>>, + span: Span, +) -> SourceResult<Fragment> { + let resolved = sizing + .zip(regions.base()) + .map(|(s, r)| s.map(|v| v.resolve(styles).relative_to(r))); + + let mut frame; + if let Some(child) = body { + let region = resolved.unwrap_or(regions.base()); + if kind.is_round() { + inset = inset.map(|side| side + Ratio::new(0.5 - SQRT_2 / 4.0)); + } + + // Pad the child. + let child = child.clone().padded(inset.map(|side| side.map(Length::from))); + let expand = sizing.as_ref().map(Smart::is_custom); + let pod = Regions::one(region, expand); + frame = child.layout(vt, styles, pod)?.into_frame(); + + // Enforce correct size. + *frame.size_mut() = expand.select(region, frame.size()); + + // Relayout with full expansion into square region to make sure + // the result is really a square or circle. + if kind.is_quadratic() { + frame.set_size(Size::splat(frame.size().max_by_side())); + let length = frame.size().max_by_side().min(region.min_by_side()); + let pod = Regions::one(Size::splat(length), Axes::splat(true)); + frame = child.layout(vt, styles, pod)?.into_frame(); + } + + // Enforce correct size again. + *frame.size_mut() = expand.select(region, frame.size()); + if kind.is_quadratic() { + frame.set_size(Size::splat(frame.size().max_by_side())); + } + } else { + // The default size that a shape takes on if it has no child and + // enough space. + let default = Size::new(Abs::pt(45.0), Abs::pt(30.0)); + let mut size = resolved.unwrap_or(default.min(regions.base())); + if kind.is_quadratic() { + size = Size::splat(size.min_by_side()); + } + frame = Frame::new(size); + } + + // Prepare stroke. + let stroke = match stroke { + Smart::Auto if fill.is_none() => Sides::splat(Some(Stroke::default())), + Smart::Auto => Sides::splat(None), + Smart::Custom(strokes) => { + strokes.map(|s| s.map(PartialStroke::unwrap_or_default)) + } + }; + + // Add fill and/or stroke. + if fill.is_some() || stroke.iter().any(Option::is_some) { + if kind.is_round() { + let outset = outset.relative_to(frame.size()); + let size = frame.size() + outset.sum_by_axis(); + let pos = Point::new(-outset.left, -outset.top); + let shape = ellipse(size, fill, stroke.left); + frame.prepend(pos, FrameItem::Shape(shape, span)); + } else { + frame.fill_and_stroke(fill, stroke, outset, radius, span); + } + } + + // Apply metadata. + frame.meta(styles, false); + + Ok(Fragment::frame(frame)) +} + +/// A category of shape. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum ShapeKind { + /// A rectangle with equal side lengths. + Square, + /// A quadrilateral with four right angles. + Rect, + /// An ellipse with coinciding foci. + Circle, + /// A curve around two focal points. + Ellipse, +} + +impl ShapeKind { + /// Whether this shape kind is curvy. + fn is_round(self) -> bool { + matches!(self, Self::Circle | Self::Ellipse) + } + + /// Whether this shape kind has equal side length. + fn is_quadratic(self) -> bool { + matches!(self, Self::Square | Self::Circle) + } +} diff --git a/crates/typst-macros/Cargo.toml b/crates/typst-macros/Cargo.toml new file mode 100644 index 00000000..15f2b2bd --- /dev/null +++ b/crates/typst-macros/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "typst-macros" +description = "Proc-macros for Typst." +version.workspace = true +rust-version.workspace = true +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true + +[lib] +proc-macro = true +test = false +doctest = false +bench = false + +[dependencies] +heck = "0.4" +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["full", "extra-traits"] } diff --git a/crates/typst-macros/src/castable.rs b/crates/typst-macros/src/castable.rs new file mode 100644 index 00000000..05c1b4d1 --- /dev/null +++ b/crates/typst-macros/src/castable.rs @@ -0,0 +1,331 @@ +use super::*; + +/// Expand the `#[derive(Cast)]` macro. +pub fn derive_cast(item: &DeriveInput) -> Result<TokenStream> { + let ty = &item.ident; + + let syn::Data::Enum(data) = &item.data else { + bail!(item, "only enums are supported"); + }; + + let mut variants = vec![]; + for variant in &data.variants { + if let Some((_, expr)) = &variant.discriminant { + bail!(expr, "explicit discriminant is not allowed"); + } + + let string = if let Some(attr) = + variant.attrs.iter().find(|attr| attr.path().is_ident("string")) + { + attr.parse_args::<syn::LitStr>()?.value() + } else { + kebab_case(&variant.ident) + }; + + variants.push(Variant { + ident: variant.ident.clone(), + string, + docs: documentation(&variant.attrs), + }); + } + + let strs_to_variants = variants.iter().map(|Variant { ident, string, docs }| { + quote! { + #[doc = #docs] + #string => Self::#ident + } + }); + + let variants_to_strs = variants.iter().map(|Variant { ident, string, .. }| { + quote! { + #ty::#ident => #string + } + }); + + Ok(quote! { + ::typst::eval::cast! { + #ty, + self => ::typst::eval::IntoValue::into_value(match self { + #(#variants_to_strs),* + }), + #(#strs_to_variants),* + } + }) +} + +/// An enum variant in a `derive(Cast)`. +struct Variant { + ident: Ident, + string: String, + docs: String, +} + +/// Expand the `cast!` macro. +pub fn cast(stream: TokenStream) -> Result<TokenStream> { + let input: CastInput = syn::parse2(stream)?; + let ty = &input.ty; + let eval = quote! { ::typst::eval }; + + let castable_body = create_castable_body(&input); + let describe_body = create_describe_body(&input); + let into_value_body = create_into_value_body(&input); + let from_value_body = create_from_value_body(&input); + + let reflect = (!input.from_value.is_empty() || input.name.is_some()).then(|| { + quote! { + impl #eval::Reflect for #ty { + fn describe() -> #eval::CastInfo { + #describe_body + } + + fn castable(value: &#eval::Value) -> bool { + #castable_body + } + } + } + }); + + let into_value = (input.into_value.is_some() || input.name.is_some()).then(|| { + quote! { + impl #eval::IntoValue for #ty { + fn into_value(self) -> #eval::Value { + #into_value_body + } + } + } + }); + + let from_value = (!input.from_value.is_empty() || input.name.is_some()).then(|| { + quote! { + impl #eval::FromValue for #ty { + fn from_value(value: #eval::Value) -> ::typst::diag::StrResult<Self> { + #from_value_body + } + } + } + }); + + let ty = input.name.as_ref().map(|name| { + quote! { + impl #eval::Type for #ty { + const TYPE_NAME: &'static str = #name; + } + } + }); + + Ok(quote! { + #reflect + #into_value + #from_value + #ty + }) +} + +/// The input to `cast!`. +struct CastInput { + ty: syn::Type, + name: Option<syn::LitStr>, + into_value: Option<syn::Expr>, + from_value: Punctuated<Cast, Token![,]>, +} + +impl Parse for CastInput { + fn parse(input: ParseStream) -> Result<Self> { + let ty; + let mut name = None; + if input.peek(syn::Token![type]) { + let _: syn::Token![type] = input.parse()?; + ty = input.parse()?; + let _: syn::Token![:] = input.parse()?; + name = Some(input.parse()?); + } else { + ty = input.parse()?; + } + + let _: syn::Token![,] = input.parse()?; + + let mut into_value = None; + if input.peek(syn::Token![self]) { + let _: syn::Token![self] = input.parse()?; + let _: syn::Token![=>] = input.parse()?; + into_value = Some(input.parse()?); + let _: syn::Token![,] = input.parse()?; + } + + let from_value = Punctuated::parse_terminated(input)?; + Ok(Self { ty, name, into_value, from_value }) + } +} + +impl Parse for Cast { + fn parse(input: ParseStream) -> Result<Self> { + let attrs = input.call(syn::Attribute::parse_outer)?; + let pattern = input.parse()?; + let _: syn::Token![=>] = input.parse()?; + let expr = input.parse()?; + Ok(Self { attrs, pattern, expr }) + } +} + +impl Parse for Pattern { + fn parse(input: ParseStream) -> Result<Self> { + if input.peek(syn::LitStr) { + Ok(Pattern::Str(input.parse()?)) + } else { + let pat = syn::Pat::parse_single(input)?; + let _: syn::Token![:] = input.parse()?; + let ty = input.parse()?; + Ok(Pattern::Ty(pat, ty)) + } + } +} + +/// A single cast, e.g. `v: i64 => Self::Int(v)`. +struct Cast { + attrs: Vec<syn::Attribute>, + pattern: Pattern, + expr: syn::Expr, +} + +/// A pattern in a cast, e.g.`"ascender"` or `v: i64`. +enum Pattern { + Str(syn::LitStr), + Ty(syn::Pat, syn::Type), +} + +fn create_castable_body(input: &CastInput) -> TokenStream { + let mut strings = vec![]; + let mut casts = vec![]; + + for cast in &input.from_value { + match &cast.pattern { + Pattern::Str(lit) => { + strings.push(quote! { #lit => return true }); + } + Pattern::Ty(_, ty) => { + casts.push(quote! { + if <#ty as ::typst::eval::Reflect>::castable(value) { + return true; + } + }); + } + } + } + + let dynamic_check = input.name.is_some().then(|| { + quote! { + if let ::typst::eval::Value::Dyn(dynamic) = &value { + if dynamic.is::<Self>() { + return true; + } + } + } + }); + + let str_check = (!strings.is_empty()).then(|| { + quote! { + if let ::typst::eval::Value::Str(string) = &value { + match string.as_str() { + #(#strings,)* + _ => {} + } + } + } + }); + + quote! { + #dynamic_check + #str_check + #(#casts)* + false + } +} + +fn create_describe_body(input: &CastInput) -> TokenStream { + let mut infos = vec![]; + + for cast in &input.from_value { + let docs = documentation(&cast.attrs); + infos.push(match &cast.pattern { + Pattern::Str(lit) => { + quote! { + ::typst::eval::CastInfo::Value( + ::typst::eval::IntoValue::into_value(#lit), + #docs, + ) + } + } + Pattern::Ty(_, ty) => { + quote! { <#ty as ::typst::eval::Reflect>::describe() } + } + }); + } + + if let Some(name) = &input.name { + infos.push(quote! { + ::typst::eval::CastInfo::Type(#name) + }); + } + + quote! { + #(#infos)+* + } +} + +fn create_into_value_body(input: &CastInput) -> TokenStream { + if let Some(expr) = &input.into_value { + quote! { #expr } + } else { + quote! { ::typst::eval::Value::dynamic(self) } + } +} + +fn create_from_value_body(input: &CastInput) -> TokenStream { + let mut string_arms = vec![]; + let mut cast_checks = vec![]; + + for cast in &input.from_value { + let expr = &cast.expr; + match &cast.pattern { + Pattern::Str(lit) => { + string_arms.push(quote! { #lit => return Ok(#expr) }); + } + Pattern::Ty(binding, ty) => { + cast_checks.push(quote! { + if <#ty as ::typst::eval::Reflect>::castable(&value) { + let #binding = <#ty as ::typst::eval::FromValue>::from_value(value)?; + return Ok(#expr); + } + }); + } + } + } + + let dynamic_check = input.name.is_some().then(|| { + quote! { + if let ::typst::eval::Value::Dyn(dynamic) = &value { + if let Some(concrete) = dynamic.downcast::<Self>() { + return Ok(concrete.clone()); + } + } + } + }); + + let str_check = (!string_arms.is_empty()).then(|| { + quote! { + if let ::typst::eval::Value::Str(string) = &value { + match string.as_str() { + #(#string_arms,)* + _ => {} + } + } + } + }); + + quote! { + #dynamic_check + #str_check + #(#cast_checks)* + Err(<Self as ::typst::eval::Reflect>::error(&value)) + } +} diff --git a/crates/typst-macros/src/element.rs b/crates/typst-macros/src/element.rs new file mode 100644 index 00000000..6ce91fcb --- /dev/null +++ b/crates/typst-macros/src/element.rs @@ -0,0 +1,554 @@ +use super::*; + +/// Expand the `#[element]` macro. +pub fn element(stream: TokenStream, body: &syn::ItemStruct) -> Result<TokenStream> { + let element = prepare(stream, body)?; + Ok(create(&element)) +} + +struct Elem { + name: String, + display: String, + category: String, + keywords: Option<String>, + docs: String, + vis: syn::Visibility, + ident: Ident, + capable: Vec<Ident>, + fields: Vec<Field>, + scope: Option<BlockWithReturn>, +} + +struct Field { + name: String, + docs: String, + internal: bool, + external: bool, + positional: bool, + required: bool, + variadic: bool, + synthesized: bool, + fold: bool, + resolve: bool, + parse: Option<BlockWithReturn>, + default: syn::Expr, + vis: syn::Visibility, + ident: Ident, + ident_in: Ident, + with_ident: Ident, + push_ident: Ident, + set_ident: Ident, + ty: syn::Type, + output: syn::Type, +} + +impl Field { + fn inherent(&self) -> bool { + self.required || self.variadic + } + + fn settable(&self) -> bool { + !self.inherent() + } +} + +/// Preprocess the element's definition. +fn prepare(stream: TokenStream, body: &syn::ItemStruct) -> Result<Elem> { + let syn::Fields::Named(named) = &body.fields else { + bail!(body, "expected named fields"); + }; + + let mut fields = vec![]; + for field in &named.named { + let Some(ident) = field.ident.clone() else { + bail!(field, "expected named field"); + }; + + let mut attrs = field.attrs.clone(); + let variadic = has_attr(&mut attrs, "variadic"); + let required = has_attr(&mut attrs, "required") || variadic; + let positional = has_attr(&mut attrs, "positional") || required; + + if ident == "label" { + bail!(ident, "invalid field name"); + } + + let mut field = Field { + name: kebab_case(&ident), + docs: documentation(&attrs), + internal: has_attr(&mut attrs, "internal"), + external: has_attr(&mut attrs, "external"), + positional, + required, + variadic, + synthesized: has_attr(&mut attrs, "synthesized"), + fold: has_attr(&mut attrs, "fold"), + resolve: has_attr(&mut attrs, "resolve"), + parse: parse_attr(&mut attrs, "parse")?.flatten(), + default: parse_attr(&mut attrs, "default")? + .flatten() + .unwrap_or_else(|| parse_quote! { ::std::default::Default::default() }), + vis: field.vis.clone(), + ident: ident.clone(), + ident_in: Ident::new(&format!("{}_in", ident), ident.span()), + with_ident: Ident::new(&format!("with_{}", ident), ident.span()), + push_ident: Ident::new(&format!("push_{}", ident), ident.span()), + set_ident: Ident::new(&format!("set_{}", ident), ident.span()), + ty: field.ty.clone(), + output: field.ty.clone(), + }; + + if field.required && (field.fold || field.resolve) { + bail!(ident, "required fields cannot be folded or resolved"); + } + + if field.required && !field.positional { + bail!(ident, "only positional fields can be required"); + } + + if field.resolve { + let output = &field.output; + field.output = parse_quote! { <#output as ::typst::model::Resolve>::Output }; + } + if field.fold { + let output = &field.output; + field.output = parse_quote! { <#output as ::typst::model::Fold>::Output }; + } + + validate_attrs(&attrs)?; + fields.push(field); + } + + let capable = Punctuated::<Ident, Token![,]>::parse_terminated + .parse2(stream)? + .into_iter() + .collect(); + + let mut attrs = body.attrs.clone(); + let docs = documentation(&attrs); + let mut lines = docs.split('\n').collect(); + let keywords = meta_line(&mut lines, "Keywords").ok().map(Into::into); + let category = meta_line(&mut lines, "Category")?.into(); + let display = meta_line(&mut lines, "Display")?.into(); + let docs = lines.join("\n").trim().into(); + + let element = Elem { + name: body.ident.to_string().trim_end_matches("Elem").to_lowercase(), + display, + category, + keywords, + docs, + vis: body.vis.clone(), + ident: body.ident.clone(), + capable, + fields, + scope: parse_attr(&mut attrs, "scope")?.flatten(), + }; + + validate_attrs(&attrs)?; + Ok(element) +} + +/// Produce the element's definition. +fn create(element: &Elem) -> TokenStream { + let Elem { vis, ident, docs, .. } = element; + let all = element.fields.iter().filter(|field| !field.external); + let settable = all.clone().filter(|field| !field.synthesized && field.settable()); + + // Inherent methods and functions. + let new = create_new_func(element); + let field_methods = all.clone().map(create_field_method); + let field_in_methods = settable.clone().map(create_field_in_method); + let with_field_methods = all.clone().map(create_with_field_method); + let push_field_methods = all.map(create_push_field_method); + let field_style_methods = settable.map(create_set_field_method); + + // Trait implementations. + let element_impl = create_pack_impl(element); + let construct_impl = element + .capable + .iter() + .all(|capability| capability != "Construct") + .then(|| create_construct_impl(element)); + let set_impl = create_set_impl(element); + let locatable_impl = element + .capable + .iter() + .any(|capability| capability == "Locatable") + .then(|| quote! { impl ::typst::model::Locatable for #ident {} }); + + quote! { + #[doc = #docs] + #[derive(Debug, Clone, Hash)] + #[repr(transparent)] + #vis struct #ident(pub ::typst::model::Content); + + impl #ident { + #new + #(#field_methods)* + #(#field_in_methods)* + #(#with_field_methods)* + #(#push_field_methods)* + #(#field_style_methods)* + + /// The element's span. + pub fn span(&self) -> ::typst::syntax::Span { + self.0.span() + } + + /// Set the element's span. + pub fn spanned(self, span: ::typst::syntax::Span) -> Self { + Self(self.0.spanned(span)) + } + } + + #element_impl + #construct_impl + #set_impl + #locatable_impl + + impl ::typst::eval::IntoValue for #ident { + fn into_value(self) -> ::typst::eval::Value { + ::typst::eval::Value::Content(self.0) + } + } + } +} + +/// Create the `new` function for the element. +fn create_new_func(element: &Elem) -> TokenStream { + let relevant = element + .fields + .iter() + .filter(|field| !field.external && !field.synthesized && field.inherent()); + let params = relevant.clone().map(|Field { ident, ty, .. }| { + quote! { #ident: #ty } + }); + let builder_calls = relevant.map(|Field { ident, with_ident, .. }| { + quote! { .#with_ident(#ident) } + }); + quote! { + /// Create a new element. + pub fn new(#(#params),*) -> Self { + Self(::typst::model::Content::new( + <Self as ::typst::model::Element>::func() + )) + #(#builder_calls)* + } + } +} + +/// Create an accessor methods for a field. +fn create_field_method(field: &Field) -> TokenStream { + let Field { vis, docs, ident, name, output, .. } = field; + if field.inherent() || field.synthesized { + quote! { + #[doc = #docs] + #[track_caller] + #vis fn #ident(&self) -> #output { + self.0.expect_field(#name) + } + } + } else { + let access = create_style_chain_access(field, quote! { self.0.field(#name) }); + quote! { + #[doc = #docs] + #vis fn #ident(&self, styles: ::typst::model::StyleChain) -> #output { + #access + } + } + } +} + +/// Create a style chain access method for a field. +fn create_field_in_method(field: &Field) -> TokenStream { + let Field { vis, ident_in, name, output, .. } = field; + let doc = format!("Access the `{}` field in the given style chain.", name); + let access = create_style_chain_access(field, quote! { None }); + quote! { + #[doc = #doc] + #vis fn #ident_in(styles: ::typst::model::StyleChain) -> #output { + #access + } + } +} + +/// Create a style chain access method for a field. +fn create_style_chain_access(field: &Field, inherent: TokenStream) -> TokenStream { + let Field { name, ty, default, .. } = field; + let getter = match (field.fold, field.resolve) { + (false, false) => quote! { get }, + (false, true) => quote! { get_resolve }, + (true, false) => quote! { get_fold }, + (true, true) => quote! { get_resolve_fold }, + }; + + quote! { + styles.#getter::<#ty>( + <Self as ::typst::model::Element>::func(), + #name, + #inherent, + || #default, + ) + } +} + +/// Create a builder pattern method for a field. +fn create_with_field_method(field: &Field) -> TokenStream { + let Field { vis, ident, with_ident, name, ty, .. } = field; + let doc = format!("Set the [`{}`](Self::{}) field.", name, ident); + quote! { + #[doc = #doc] + #vis fn #with_ident(mut self, #ident: #ty) -> Self { + Self(self.0.with_field(#name, #ident)) + } + } +} + +/// Create a set-style method for a field. +fn create_push_field_method(field: &Field) -> TokenStream { + let Field { vis, ident, push_ident, name, ty, .. } = field; + let doc = format!("Push the [`{}`](Self::{}) field.", name, ident); + quote! { + #[doc = #doc] + #vis fn #push_ident(&mut self, #ident: #ty) { + self.0.push_field(#name, #ident); + } + } +} + +/// Create a setter method for a field. +fn create_set_field_method(field: &Field) -> TokenStream { + let Field { vis, ident, set_ident, name, ty, .. } = field; + let doc = format!("Create a style property for the `{}` field.", name); + quote! { + #[doc = #doc] + #vis fn #set_ident(#ident: #ty) -> ::typst::model::Style { + ::typst::model::Style::Property(::typst::model::Property::new( + <Self as ::typst::model::Element>::func(), + #name, + #ident, + )) + } + } +} + +/// Create the element's `Pack` implementation. +fn create_pack_impl(element: &Elem) -> TokenStream { + let Elem { ident, name, display, keywords, category, docs, .. } = element; + let vtable_func = create_vtable_func(element); + let infos = element + .fields + .iter() + .filter(|field| !field.internal && !field.synthesized) + .map(create_param_info); + let scope = create_scope_builder(element.scope.as_ref()); + let keywords = quote_option(keywords); + quote! { + impl ::typst::model::Element for #ident { + fn pack(self) -> ::typst::model::Content { + self.0 + } + + fn unpack(content: &::typst::model::Content) -> ::std::option::Option<&Self> { + // Safety: Elements are #[repr(transparent)]. + content.is::<Self>().then(|| unsafe { + ::std::mem::transmute(content) + }) + } + + fn func() -> ::typst::model::ElemFunc { + static NATIVE: ::typst::model::NativeElemFunc = ::typst::model::NativeElemFunc { + name: #name, + vtable: #vtable_func, + construct: <#ident as ::typst::model::Construct>::construct, + set: <#ident as ::typst::model::Set>::set, + info: ::typst::eval::Lazy::new(|| typst::eval::FuncInfo { + name: #name, + display: #display, + keywords: #keywords, + docs: #docs, + params: ::std::vec![#(#infos),*], + returns: ::typst::eval::CastInfo::Union(::std::vec![ + ::typst::eval::CastInfo::Type("content") + ]), + category: #category, + scope: #scope, + }), + }; + (&NATIVE).into() + } + } + } +} + +/// Create the element's casting vtable. +fn create_vtable_func(element: &Elem) -> TokenStream { + let ident = &element.ident; + let relevant = element.capable.iter().filter(|&ident| ident != "Construct"); + let checks = relevant.map(|capability| { + quote! { + if id == ::std::any::TypeId::of::<dyn #capability>() { + return Some(unsafe { + ::typst::util::fat::vtable(&null as &dyn #capability) + }); + } + } + }); + + quote! { + |id| { + let null = Self(::typst::model::Content::new( + <#ident as ::typst::model::Element>::func() + )); + #(#checks)* + None + } + } +} + +/// Create a parameter info for a field. +fn create_param_info(field: &Field) -> TokenStream { + let Field { + name, + docs, + positional, + variadic, + required, + default, + fold, + ty, + output, + .. + } = field; + let named = !positional; + let settable = field.settable(); + let default_ty = if *fold { &output } else { &ty }; + let default = quote_option(&settable.then(|| { + quote! { + || { + let typed: #default_ty = #default; + ::typst::eval::IntoValue::into_value(typed) + } + } + })); + let ty = if *variadic { + quote! { <#ty as ::typst::eval::Variadics>::Inner } + } else { + quote! { #ty } + }; + quote! { + ::typst::eval::ParamInfo { + name: #name, + docs: #docs, + cast: <#ty as ::typst::eval::Reflect>::describe(), + default: #default, + positional: #positional, + named: #named, + variadic: #variadic, + required: #required, + settable: #settable, + } + } +} + +/// Create the element's `Construct` implementation. +fn create_construct_impl(element: &Elem) -> TokenStream { + let ident = &element.ident; + let handlers = element + .fields + .iter() + .filter(|field| { + !field.external + && !field.synthesized + && (!field.internal || field.parse.is_some()) + }) + .map(|field| { + let push_ident = &field.push_ident; + let (prefix, value) = create_field_parser(field); + if field.settable() { + quote! { + #prefix + if let Some(value) = #value { + element.#push_ident(value); + } + } + } else { + quote! { + #prefix + element.#push_ident(#value); + } + } + }); + + quote! { + impl ::typst::model::Construct for #ident { + fn construct( + vm: &mut ::typst::eval::Vm, + args: &mut ::typst::eval::Args, + ) -> ::typst::diag::SourceResult<::typst::model::Content> { + let mut element = Self(::typst::model::Content::new( + <Self as ::typst::model::Element>::func() + )); + #(#handlers)* + Ok(element.0) + } + } + } +} + +/// Create the element's `Set` implementation. +fn create_set_impl(element: &Elem) -> TokenStream { + let ident = &element.ident; + let handlers = element + .fields + .iter() + .filter(|field| { + !field.external + && !field.synthesized + && field.settable() + && (!field.internal || field.parse.is_some()) + }) + .map(|field| { + let set_ident = &field.set_ident; + let (prefix, value) = create_field_parser(field); + quote! { + #prefix + if let Some(value) = #value { + styles.set(Self::#set_ident(value)); + } + } + }); + + quote! { + impl ::typst::model::Set for #ident { + fn set( + args: &mut ::typst::eval::Args, + ) -> ::typst::diag::SourceResult<::typst::model::Styles> { + let mut styles = ::typst::model::Styles::new(); + #(#handlers)* + Ok(styles) + } + } + } +} + +/// Create argument parsing code for a field. +fn create_field_parser(field: &Field) -> (TokenStream, TokenStream) { + if let Some(BlockWithReturn { prefix, expr }) = &field.parse { + return (quote! { #(#prefix);* }, quote! { #expr }); + } + + let name = &field.name; + let value = if field.variadic { + quote! { args.all()? } + } else if field.required { + quote! { args.expect(#name)? } + } else if field.positional { + quote! { args.find()? } + } else { + quote! { args.named(#name)? } + }; + + (quote! {}, value) +} diff --git a/crates/typst-macros/src/func.rs b/crates/typst-macros/src/func.rs new file mode 100644 index 00000000..4a68e846 --- /dev/null +++ b/crates/typst-macros/src/func.rs @@ -0,0 +1,268 @@ +use super::*; + +/// Expand the `#[func]` macro. +pub fn func(stream: TokenStream, item: &syn::ItemFn) -> Result<TokenStream> { + let func = prepare(stream, item)?; + Ok(create(&func, item)) +} + +struct Func { + name: String, + display: String, + category: String, + keywords: Option<String>, + docs: String, + vis: syn::Visibility, + ident: Ident, + ident_func: Ident, + parent: Option<syn::Type>, + vm: bool, + vt: bool, + args: bool, + span: bool, + params: Vec<Param>, + returns: syn::Type, + scope: Option<BlockWithReturn>, +} + +struct Param { + name: String, + docs: String, + external: bool, + named: bool, + variadic: bool, + default: Option<syn::Expr>, + ident: Ident, + ty: syn::Type, +} + +fn prepare(stream: TokenStream, item: &syn::ItemFn) -> Result<Func> { + let sig = &item.sig; + + let Parent(parent) = syn::parse2(stream)?; + + let mut vm = false; + let mut vt = false; + let mut args = false; + let mut span = false; + let mut params = vec![]; + for input in &sig.inputs { + let syn::FnArg::Typed(typed) = input else { + bail!(input, "self is not allowed here"); + }; + + let syn::Pat::Ident(syn::PatIdent { + by_ref: None, + mutability: None, + ident, + .. + }) = &*typed.pat else { + bail!(typed.pat, "expected identifier"); + }; + + match ident.to_string().as_str() { + "vm" => vm = true, + "vt" => vt = true, + "args" => args = true, + "span" => span = true, + _ => { + let mut attrs = typed.attrs.clone(); + params.push(Param { + name: kebab_case(ident), + docs: documentation(&attrs), + external: has_attr(&mut attrs, "external"), + named: has_attr(&mut attrs, "named"), + variadic: has_attr(&mut attrs, "variadic"), + default: parse_attr(&mut attrs, "default")?.map(|expr| { + expr.unwrap_or_else( + || parse_quote! { ::std::default::Default::default() }, + ) + }), + ident: ident.clone(), + ty: (*typed.ty).clone(), + }); + + validate_attrs(&attrs)?; + } + } + } + + let mut attrs = item.attrs.clone(); + let docs = documentation(&attrs); + let mut lines = docs.split('\n').collect(); + let keywords = meta_line(&mut lines, "Keywords").ok().map(Into::into); + let category = meta_line(&mut lines, "Category")?.into(); + let display = meta_line(&mut lines, "Display")?.into(); + let docs = lines.join("\n").trim().into(); + + let func = Func { + name: sig.ident.to_string().trim_end_matches('_').replace('_', "-"), + display, + category, + keywords, + docs, + vis: item.vis.clone(), + ident: sig.ident.clone(), + ident_func: Ident::new( + &format!("{}_func", sig.ident.to_string().trim_end_matches('_')), + sig.ident.span(), + ), + parent, + params, + returns: match &sig.output { + syn::ReturnType::Default => parse_quote! { () }, + syn::ReturnType::Type(_, ty) => ty.as_ref().clone(), + }, + scope: parse_attr(&mut attrs, "scope")?.flatten(), + vm, + vt, + args, + span, + }; + + Ok(func) +} + +fn create(func: &Func, item: &syn::ItemFn) -> TokenStream { + let Func { + name, + display, + category, + docs, + vis, + ident, + ident_func, + returns, + .. + } = func; + + let handlers = func + .params + .iter() + .filter(|param| !param.external) + .map(create_param_parser); + + let args = func + .params + .iter() + .filter(|param| !param.external) + .map(|param| ¶m.ident); + + let parent = func.parent.as_ref().map(|ty| quote! { #ty:: }); + let vm_ = func.vm.then(|| quote! { vm, }); + let vt_ = func.vt.then(|| quote! { &mut vm.vt, }); + let args_ = func.args.then(|| quote! { args.take(), }); + let span_ = func.span.then(|| quote! { args.span, }); + let wrapper = quote! { + |vm, args| { + let __typst_func = #parent #ident; + #(#handlers)* + let output = __typst_func(#(#args,)* #vm_ #vt_ #args_ #span_); + ::typst::eval::IntoResult::into_result(output, args.span) + } + }; + + let mut item = item.clone(); + item.attrs.clear(); + + let inputs = item.sig.inputs.iter().cloned().filter_map(|mut input| { + if let syn::FnArg::Typed(typed) = &mut input { + if typed.attrs.iter().any(|attr| attr.path().is_ident("external")) { + return None; + } + typed.attrs.clear(); + } + Some(input) + }); + + item.sig.inputs = parse_quote! { #(#inputs),* }; + + let keywords = quote_option(&func.keywords); + let params = func.params.iter().map(create_param_info); + let scope = create_scope_builder(func.scope.as_ref()); + + quote! { + #[doc(hidden)] + #vis fn #ident_func() -> &'static ::typst::eval::NativeFunc { + static FUNC: ::typst::eval::NativeFunc = ::typst::eval::NativeFunc { + func: #wrapper, + info: ::typst::eval::Lazy::new(|| typst::eval::FuncInfo { + name: #name, + display: #display, + keywords: #keywords, + category: #category, + docs: #docs, + params: ::std::vec![#(#params),*], + returns: <#returns as ::typst::eval::Reflect>::describe(), + scope: #scope, + }), + }; + &FUNC + } + + #[doc = #docs] + #item + } +} + +/// Create a parameter info for a field. +fn create_param_info(param: &Param) -> TokenStream { + let Param { name, docs, named, variadic, ty, default, .. } = param; + let positional = !named; + let required = default.is_none(); + let default = quote_option(&default.as_ref().map(|_default| { + quote! { + || { + let typed: #ty = #default; + ::typst::eval::IntoValue::into_value(typed) + } + } + })); + let ty = if *variadic { + quote! { <#ty as ::typst::eval::Variadics>::Inner } + } else { + quote! { #ty } + }; + quote! { + ::typst::eval::ParamInfo { + name: #name, + docs: #docs, + cast: <#ty as ::typst::eval::Reflect>::describe(), + default: #default, + positional: #positional, + named: #named, + variadic: #variadic, + required: #required, + settable: false, + } + } +} + +/// Create argument parsing code for a parameter. +fn create_param_parser(param: &Param) -> TokenStream { + let Param { name, ident, ty, .. } = param; + + let mut value = if param.variadic { + quote! { args.all()? } + } else if param.named { + quote! { args.named(#name)? } + } else if param.default.is_some() { + quote! { args.eat()? } + } else { + quote! { args.expect(#name)? } + }; + + if let Some(default) = ¶m.default { + value = quote! { #value.unwrap_or_else(|| #default) } + } + + quote! { let mut #ident: #ty = #value; } +} + +struct Parent(Option<syn::Type>); + +impl Parse for Parent { + fn parse(input: ParseStream) -> Result<Self> { + Ok(Self(if !input.is_empty() { Some(input.parse()?) } else { None })) + } +} diff --git a/crates/typst-macros/src/lib.rs b/crates/typst-macros/src/lib.rs new file mode 100644 index 00000000..49840ef2 --- /dev/null +++ b/crates/typst-macros/src/lib.rs @@ -0,0 +1,63 @@ +//! Procedural macros for Typst. + +extern crate proc_macro; + +#[macro_use] +mod util; +mod castable; +mod element; +mod func; +mod symbols; + +use proc_macro::TokenStream as BoundaryStream; +use proc_macro2::TokenStream; +use quote::quote; +use syn::ext::IdentExt; +use syn::parse::{Parse, ParseStream, Parser}; +use syn::punctuated::Punctuated; +use syn::{parse_quote, DeriveInput, Ident, Result, Token}; + +use self::util::*; + +/// Turns a function into a `NativeFunc`. +#[proc_macro_attribute] +pub fn func(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { + let item = syn::parse_macro_input!(item as syn::ItemFn); + func::func(stream.into(), &item) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +/// Turns a type into an `Element`. +#[proc_macro_attribute] +pub fn element(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { + let item = syn::parse_macro_input!(item as syn::ItemStruct); + element::element(stream.into(), &item) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +/// Implements `Reflect`, `FromValue`, and `IntoValue` for an enum. +#[proc_macro_derive(Cast, attributes(string))] +pub fn derive_cast(item: BoundaryStream) -> BoundaryStream { + let item = syn::parse_macro_input!(item as DeriveInput); + castable::derive_cast(&item) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +/// Implements `Reflect`, `FromValue`, and `IntoValue` for a type. +#[proc_macro] +pub fn cast(stream: BoundaryStream) -> BoundaryStream { + castable::cast(stream.into()) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +/// Defines a list of `Symbol`s. +#[proc_macro] +pub fn symbols(stream: BoundaryStream) -> BoundaryStream { + symbols::symbols(stream.into()) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} diff --git a/crates/typst-macros/src/symbols.rs b/crates/typst-macros/src/symbols.rs new file mode 100644 index 00000000..cdb7f5d7 --- /dev/null +++ b/crates/typst-macros/src/symbols.rs @@ -0,0 +1,78 @@ +use super::*; + +/// Expand the `symbols!` macro. +pub fn symbols(stream: TokenStream) -> Result<TokenStream> { + let list: Punctuated<Symbol, Token![,]> = + Punctuated::parse_terminated.parse2(stream)?; + let pairs = list.iter().map(|symbol| { + let name = symbol.name.to_string(); + let kind = match &symbol.kind { + Kind::Single(c) => quote! { typst::eval::Symbol::new(#c), }, + Kind::Multiple(variants) => { + let variants = variants.iter().map(|variant| { + let name = &variant.name; + let c = &variant.c; + quote! { (#name, #c) } + }); + quote! { + typst::eval::Symbol::list(&[#(#variants),*]) + } + } + }; + quote! { (#name, #kind) } + }); + Ok(quote! { &[#(#pairs),*] }) +} + +struct Symbol { + name: syn::Ident, + kind: Kind, +} + +enum Kind { + Single(syn::LitChar), + Multiple(Punctuated<Variant, Token![,]>), +} + +struct Variant { + name: String, + c: syn::LitChar, +} + +impl Parse for Symbol { + fn parse(input: ParseStream) -> Result<Self> { + let name = input.call(Ident::parse_any)?; + input.parse::<Token![:]>()?; + let kind = input.parse()?; + Ok(Self { name, kind }) + } +} + +impl Parse for Kind { + fn parse(input: ParseStream) -> Result<Self> { + if input.peek(syn::LitChar) { + Ok(Self::Single(input.parse()?)) + } else { + let content; + syn::bracketed!(content in input); + Ok(Self::Multiple(Punctuated::parse_terminated(&content)?)) + } + } +} + +impl Parse for Variant { + fn parse(input: ParseStream) -> Result<Self> { + let mut name = String::new(); + if input.peek(syn::Ident::peek_any) { + name.push_str(&input.call(Ident::parse_any)?.to_string()); + while input.peek(Token![.]) { + input.parse::<Token![.]>()?; + name.push('.'); + name.push_str(&input.call(Ident::parse_any)?.to_string()); + } + input.parse::<Token![:]>()?; + } + let c = input.parse()?; + Ok(Self { name, c }) + } +} diff --git a/crates/typst-macros/src/util.rs b/crates/typst-macros/src/util.rs new file mode 100644 index 00000000..389fed06 --- /dev/null +++ b/crates/typst-macros/src/util.rs @@ -0,0 +1,145 @@ +use heck::ToKebabCase; +use quote::ToTokens; + +use super::*; + +/// Return an error at the given item. +macro_rules! bail { + (callsite, $($tts:tt)*) => { + return Err(syn::Error::new( + proc_macro2::Span::call_site(), + format!("typst: {}", format!($($tts)*)) + )) + }; + ($item:expr, $($tts:tt)*) => { + return Err(syn::Error::new_spanned( + &$item, + format!("typst: {}", format!($($tts)*)) + )) + }; +} + +/// For parsing attributes of the form: +/// #[attr( +/// statement; +/// statement; +/// returned_expression +/// )] +pub struct BlockWithReturn { + pub prefix: Vec<syn::Stmt>, + pub expr: syn::Stmt, +} + +impl Parse for BlockWithReturn { + fn parse(input: ParseStream) -> Result<Self> { + let mut stmts = syn::Block::parse_within(input)?; + let Some(expr) = stmts.pop() else { + return Err(input.error("expected at least one expression")); + }; + Ok(Self { prefix: stmts, expr }) + } +} + +/// Whether an attribute list has a specified attribute. +pub fn has_attr(attrs: &mut Vec<syn::Attribute>, target: &str) -> bool { + take_attr(attrs, target).is_some() +} + +/// Whether an attribute list has a specified attribute. +pub fn parse_attr<T: Parse>( + attrs: &mut Vec<syn::Attribute>, + target: &str, +) -> Result<Option<Option<T>>> { + take_attr(attrs, target) + .map(|attr| { + Ok(match attr.meta { + syn::Meta::Path(_) => None, + syn::Meta::List(list) => Some(list.parse_args()?), + syn::Meta::NameValue(meta) => bail!(meta, "not valid here"), + }) + }) + .transpose() +} + +/// Whether an attribute list has a specified attribute. +pub fn take_attr( + attrs: &mut Vec<syn::Attribute>, + target: &str, +) -> Option<syn::Attribute> { + attrs + .iter() + .position(|attr| attr.path().is_ident(target)) + .map(|i| attrs.remove(i)) +} + +/// Ensure that no unrecognized attributes remain. +pub fn validate_attrs(attrs: &[syn::Attribute]) -> Result<()> { + for attr in attrs { + if !attr.path().is_ident("doc") && !attr.path().is_ident("derive") { + let ident = attr.path().get_ident().unwrap(); + bail!(ident, "unrecognized attribute: {ident}"); + } + } + Ok(()) +} + +/// Convert an identifier to a kebab-case string. +pub fn kebab_case(name: &Ident) -> String { + name.to_string().to_kebab_case() +} + +/// Extract documentation comments from an attribute list. +pub fn documentation(attrs: &[syn::Attribute]) -> String { + let mut doc = String::new(); + + // Parse doc comments. + for attr in attrs { + if let syn::Meta::NameValue(meta) = &attr.meta { + if meta.path.is_ident("doc") { + if let syn::Expr::Lit(lit) = &meta.value { + if let syn::Lit::Str(string) = &lit.lit { + let full = string.value(); + let line = full.strip_prefix(' ').unwrap_or(&full); + doc.push_str(line); + doc.push('\n'); + } + } + } + } + } + + doc.trim().into() +} + +/// Extract a line of metadata from documentation. +pub fn meta_line<'a>(lines: &mut Vec<&'a str>, key: &str) -> Result<&'a str> { + match lines.last().and_then(|line| line.strip_prefix(&format!("{key}:"))) { + Some(value) => { + lines.pop(); + Ok(value.trim()) + } + None => bail!(callsite, "missing metadata key: {key}"), + } +} + +/// Creates a block responsible for building a `Scope`. +pub fn create_scope_builder(scope_block: Option<&BlockWithReturn>) -> TokenStream { + if let Some(BlockWithReturn { prefix, expr }) = scope_block { + quote! { { + let mut scope = ::typst::eval::Scope::deduplicating(); + #(#prefix);* + #expr + } } + } else { + quote! { ::typst::eval::Scope::new() } + } +} + +/// Quotes an option literally. +pub fn quote_option<T: ToTokens>(option: &Option<T>) -> TokenStream { + if let Some(value) = option { + quote! { Some(#value) } + } else { + quote! { None } + } +} diff --git a/crates/typst/Cargo.toml b/crates/typst/Cargo.toml new file mode 100644 index 00000000..c9f3bb02 --- /dev/null +++ b/crates/typst/Cargo.toml @@ -0,0 +1,56 @@ +[package] +name = "typst" +description = "A new markup-based typesetting system that is powerful and easy to learn." +categories = ["compilers", "science"] +keywords = ["markup", "typesetting"] +version.workspace = true +rust-version.workspace = true +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true + +[lib] +doctest = false +bench = false + +[dependencies] +typst-macros = { path = "../typst-macros" } +bitflags = { version = "2", features = ["serde"] } +bytemuck = "1" +comemo = "0.3" +ecow = { version = "0.1.1", features = ["serde"] } +flate2 = "1" +fontdb = "0.13" +if_chain = "1" +image = { version = "0.24", default-features = false, features = ["png", "jpeg", "gif"] } +indexmap = "1.9.3" +log = "0.4" +miniz_oxide = "0.7" +once_cell = "1" +pdf-writer = "0.7.1" +pixglyph = "0.1" +regex = "1" +resvg = { version = "0.32", default-features = false } +roxmltree = "0.18" +rustybuzz = "0.7" +serde = { version = "1", features = ["derive"] } +siphasher = "0.3" +subsetter = "0.1.1" +svg2pdf = { git = "https://github.com/typst/svg2pdf" } +tiny-skia = "0.9.0" +toml = { version = "0.7.3", default-features = false, features = ["parse"] } +tracing = "0.1.37" +ttf-parser = "0.18.1" +unicode-general-category = "0.6" +unicode-ident = "1.0" +unicode-math-class = "0.1" +unicode-segmentation = "1" +unscanny = "0.1" +usvg = { version = "0.32", default-features = false, features = ["text"] } +xmp-writer = "0.1" +time = { version = "0.3.20", features = ["std", "formatting"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +stacker = "0.1.15" diff --git a/crates/typst/src/diag.rs b/crates/typst/src/diag.rs new file mode 100644 index 00000000..b5995be4 --- /dev/null +++ b/crates/typst/src/diag.rs @@ -0,0 +1,376 @@ +//! Diagnostics. + +use std::fmt::{self, Display, Formatter}; +use std::io; +use std::path::{Path, PathBuf}; +use std::str::Utf8Error; +use std::string::FromUtf8Error; + +use comemo::Tracked; + +use crate::file::PackageSpec; +use crate::syntax::{Span, Spanned}; +use crate::World; + +/// Early-return with a [`StrResult`] or [`SourceResult`]. +/// +/// If called with just a string and format args, returns with a +/// `StrResult`. If called with a span, a string and format args, returns +/// a `SourceResult`. +/// +/// ``` +/// bail!("bailing with a {}", "string result"); +/// bail!(span, "bailing with a {}", "source result"); +/// ``` +#[macro_export] +#[doc(hidden)] +macro_rules! __bail { + ($fmt:literal $(, $arg:expr)* $(,)?) => { + return Err($crate::diag::eco_format!($fmt, $($arg),*)) + }; + + ($error:expr) => { + return Err(Box::new(vec![$error])) + }; + + ($span:expr, $fmt:literal $(, $arg:expr)* $(,)?) => { + return Err(Box::new(vec![$crate::diag::SourceError::new( + $span, + $crate::diag::eco_format!($fmt, $($arg),*), + )])) + }; +} + +#[doc(inline)] +pub use crate::__bail as bail; + +/// Construct an [`EcoString`] or [`SourceError`]. +#[macro_export] +#[doc(hidden)] +macro_rules! __error { + ($fmt:literal $(, $arg:expr)* $(,)?) => { + $crate::diag::eco_format!($fmt, $($arg),*) + }; + + ($span:expr, $fmt:literal $(, $arg:expr)* $(,)?) => { + $crate::diag::SourceError::new( + $span, + $crate::diag::eco_format!($fmt, $($arg),*), + ) + }; +} + +#[doc(inline)] +pub use crate::__error as error; +#[doc(hidden)] +pub use ecow::{eco_format, EcoString}; + +/// A result that can carry multiple source errors. +pub type SourceResult<T> = Result<T, Box<Vec<SourceError>>>; + +/// An error in a source file. +/// +/// The contained spans will only be detached if any of the input source files +/// were detached. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct SourceError { + /// The span of the erroneous node in the source code. + pub span: Span, + /// A diagnostic message describing the problem. + pub message: EcoString, + /// The trace of function calls leading to the error. + pub trace: Vec<Spanned<Tracepoint>>, + /// Additonal hints to the user, indicating how this error could be avoided + /// or worked around. + pub hints: Vec<EcoString>, +} + +impl SourceError { + /// Create a new, bare error. + pub fn new(span: Span, message: impl Into<EcoString>) -> Self { + Self { + span, + trace: vec![], + message: message.into(), + hints: vec![], + } + } + + /// Adds user-facing hints to the error. + pub fn with_hints(mut self, hints: impl IntoIterator<Item = EcoString>) -> Self { + self.hints.extend(hints); + self + } +} + +/// A part of an error's [trace](SourceError::trace). +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum Tracepoint { + /// A function call. + Call(Option<EcoString>), + /// A show rule application. + Show(EcoString), + /// A module import. + Import, +} + +impl Display for Tracepoint { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Tracepoint::Call(Some(name)) => { + write!(f, "error occurred in this call of function `{}`", name) + } + Tracepoint::Call(None) => { + write!(f, "error occurred in this function call") + } + Tracepoint::Show(name) => { + write!(f, "error occurred while applying show rule to this {name}") + } + Tracepoint::Import => { + write!(f, "error occurred while importing this module") + } + } + } +} + +/// Enrich a [`SourceResult`] with a tracepoint. +pub trait Trace<T> { + /// Add the tracepoint to all errors that lie outside the `span`. + fn trace<F>(self, world: Tracked<dyn World + '_>, make_point: F, span: Span) -> Self + where + F: Fn() -> Tracepoint; +} + +impl<T> Trace<T> for SourceResult<T> { + fn trace<F>(self, world: Tracked<dyn World + '_>, make_point: F, span: Span) -> Self + where + F: Fn() -> Tracepoint, + { + self.map_err(|mut errors| { + if span.is_detached() { + return errors; + } + + let trace_range = span.range(&*world); + for error in errors.iter_mut().filter(|e| !e.span.is_detached()) { + // Skip traces that surround the error. + if error.span.id() == span.id() { + let error_range = error.span.range(&*world); + if trace_range.start <= error_range.start + && trace_range.end >= error_range.end + { + continue; + } + } + + error.trace.push(Spanned::new(make_point(), span)); + } + errors + }) + } +} + +/// A result type with a string error message. +pub type StrResult<T> = Result<T, EcoString>; + +/// Convert a [`StrResult`] to a [`SourceResult`] by adding span information. +pub trait At<T> { + /// Add the span information. + fn at(self, span: Span) -> SourceResult<T>; +} + +impl<T, S> At<T> for Result<T, S> +where + S: Into<EcoString>, +{ + fn at(self, span: Span) -> SourceResult<T> { + self.map_err(|message| Box::new(vec![SourceError::new(span, message)])) + } +} + +/// A result type with a string error message and hints. +pub type HintedStrResult<T> = Result<T, HintedString>; + +/// A string message with hints. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct HintedString { + /// A diagnostic message describing the problem. + pub message: EcoString, + /// Additonal hints to the user, indicating how this error could be avoided + /// or worked around. + pub hints: Vec<EcoString>, +} + +impl<T> At<T> for Result<T, HintedString> { + fn at(self, span: Span) -> SourceResult<T> { + self.map_err(|diags| { + Box::new(vec![SourceError::new(span, diags.message).with_hints(diags.hints)]) + }) + } +} + +/// Enrich a [`StrResult`] or [`HintedStrResult`] with a hint. +pub trait Hint<T> { + /// Add the hint. + fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T>; +} + +impl<T> Hint<T> for StrResult<T> { + fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T> { + self.map_err(|message| HintedString { message, hints: vec![hint.into()] }) + } +} + +impl<T> Hint<T> for HintedStrResult<T> { + fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T> { + self.map_err(|mut error| { + error.hints.push(hint.into()); + error + }) + } +} + +/// A result type with a file-related error. +pub type FileResult<T> = Result<T, FileError>; + +/// An error that occurred while trying to load of a file. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum FileError { + /// A file was not found at this path. + NotFound(PathBuf), + /// A file could not be accessed. + AccessDenied, + /// A directory was found, but a file was expected. + IsDirectory, + /// The file is not a Typst source file, but should have been. + NotSource, + /// The file was not valid UTF-8, but should have been. + InvalidUtf8, + /// The package the file is part of could not be loaded. + Package(PackageError), + /// Another error. + Other, +} + +impl FileError { + /// Create a file error from an I/O error. + pub fn from_io(error: io::Error, path: &Path) -> Self { + match error.kind() { + io::ErrorKind::NotFound => Self::NotFound(path.into()), + io::ErrorKind::PermissionDenied => Self::AccessDenied, + io::ErrorKind::InvalidData + if error.to_string().contains("stream did not contain valid UTF-8") => + { + Self::InvalidUtf8 + } + _ => Self::Other, + } + } +} + +impl std::error::Error for FileError {} + +impl Display for FileError { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::NotFound(path) => { + write!(f, "file not found (searched at {})", path.display()) + } + Self::AccessDenied => f.pad("failed to load file (access denied)"), + Self::IsDirectory => f.pad("failed to load file (is a directory)"), + Self::NotSource => f.pad("not a typst source file"), + Self::InvalidUtf8 => f.pad("file is not valid utf-8"), + Self::Package(error) => error.fmt(f), + Self::Other => f.pad("failed to load file"), + } + } +} + +impl From<Utf8Error> for FileError { + fn from(_: Utf8Error) -> Self { + Self::InvalidUtf8 + } +} + +impl From<FromUtf8Error> for FileError { + fn from(_: FromUtf8Error) -> Self { + Self::InvalidUtf8 + } +} + +impl From<PackageError> for FileError { + fn from(error: PackageError) -> Self { + Self::Package(error) + } +} + +impl From<FileError> for EcoString { + fn from(error: FileError) -> Self { + eco_format!("{error}") + } +} + +/// A result type with a package-related error. +pub type PackageResult<T> = Result<T, PackageError>; + +/// An error that occured while trying to load a package. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum PackageError { + /// The specified package does not exist. + NotFound(PackageSpec), + /// Failed to retrieve the package through the network. + NetworkFailed, + /// The package archive was malformed. + MalformedArchive, + /// Another error. + Other, +} + +impl std::error::Error for PackageError {} + +impl Display for PackageError { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::NotFound(spec) => { + write!(f, "package not found (searched for {spec})",) + } + Self::NetworkFailed => f.pad("failed to load package (network failed)"), + Self::MalformedArchive => f.pad("failed to load package (archive malformed)"), + Self::Other => f.pad("failed to load package"), + } + } +} + +impl From<PackageError> for EcoString { + fn from(error: PackageError) -> Self { + eco_format!("{error}") + } +} +/// Format a user-facing error message for an XML-like file format. +pub fn format_xml_like_error(format: &str, error: roxmltree::Error) -> EcoString { + match error { + roxmltree::Error::UnexpectedCloseTag { expected, actual, pos } => { + eco_format!( + "failed to parse {format}: found closing tag '{actual}' \ + instead of '{expected}' in line {}", + pos.row + ) + } + roxmltree::Error::UnknownEntityReference(entity, pos) => { + eco_format!( + "failed to parse {format}: unknown entity '{entity}' in line {}", + pos.row + ) + } + roxmltree::Error::DuplicatedAttribute(attr, pos) => { + eco_format!( + "failed to parse {format}: duplicate attribute '{attr}' in line {}", + pos.row + ) + } + roxmltree::Error::NoRootNode => { + eco_format!("failed to parse {format}: missing root node") + } + _ => eco_format!("failed to parse {format}"), + } +} diff --git a/crates/typst/src/doc.rs b/crates/typst/src/doc.rs new file mode 100644 index 00000000..de16cece --- /dev/null +++ b/crates/typst/src/doc.rs @@ -0,0 +1,719 @@ +//! Finished documents. + +use std::fmt::{self, Debug, Formatter}; +use std::num::NonZeroUsize; +use std::ops::Range; +use std::str::FromStr; +use std::sync::Arc; + +use ecow::EcoString; + +use crate::eval::{cast, dict, Dict, Value}; +use crate::font::Font; +use crate::geom::{ + self, rounded_rect, Abs, Align, Axes, Color, Corners, Dir, Em, Geometry, Length, + Numeric, Paint, Point, Rel, RgbaColor, Shape, Sides, Size, Stroke, Transform, +}; +use crate::image::Image; +use crate::model::{Content, Location, MetaElem, StyleChain}; +use crate::syntax::Span; + +/// A finished document with metadata and page frames. +#[derive(Debug, Default, Clone, Hash)] +pub struct Document { + /// The page frames. + pub pages: Vec<Frame>, + /// The document's title. + pub title: Option<EcoString>, + /// The document's author. + pub author: Vec<EcoString>, +} + +/// A finished layout with items at fixed positions. +#[derive(Default, Clone, Hash)] +pub struct Frame { + /// The size of the frame. + size: Size, + /// The baseline of the frame measured from the top. If this is `None`, the + /// frame's implicit baseline is at the bottom. + baseline: Option<Abs>, + /// The items composing this layout. + items: Arc<Vec<(Point, FrameItem)>>, +} + +/// Constructor, accessors and setters. +impl Frame { + /// Create a new, empty frame. + /// + /// Panics the size is not finite. + #[track_caller] + pub fn new(size: Size) -> Self { + assert!(size.is_finite()); + Self { size, baseline: None, items: Arc::new(vec![]) } + } + + /// Whether the frame contains no items. + pub fn is_empty(&self) -> bool { + self.items.is_empty() + } + + /// The size of the frame. + pub fn size(&self) -> Size { + self.size + } + + /// The size of the frame, mutably. + pub fn size_mut(&mut self) -> &mut Size { + &mut self.size + } + + /// Set the size of the frame. + pub fn set_size(&mut self, size: Size) { + self.size = size; + } + + /// The width of the frame. + pub fn width(&self) -> Abs { + self.size.x + } + + /// The height of the frame. + pub fn height(&self) -> Abs { + self.size.y + } + + /// The vertical position of the frame's baseline. + pub fn baseline(&self) -> Abs { + self.baseline.unwrap_or(self.size.y) + } + + /// Whether the frame has a non-default baseline. + pub fn has_baseline(&self) -> bool { + self.baseline.is_some() + } + + /// Set the frame's baseline from the top. + pub fn set_baseline(&mut self, baseline: Abs) { + self.baseline = Some(baseline); + } + + /// The distance from the baseline to the top of the frame. + /// + /// This is the same as `baseline()`, but more in line with the terminology + /// used in math layout. + pub fn ascent(&self) -> Abs { + self.baseline() + } + + /// The distance from the baseline to the bottom of the frame. + pub fn descent(&self) -> Abs { + self.size.y - self.baseline() + } + + /// An iterator over the items inside this frame alongside their positions + /// relative to the top-left of the frame. + pub fn items(&self) -> std::slice::Iter<'_, (Point, FrameItem)> { + self.items.iter() + } +} + +/// Insert items and subframes. +impl Frame { + /// The layer the next item will be added on. This corresponds to the number + /// of items in the frame. + pub fn layer(&self) -> usize { + self.items.len() + } + + /// Add an item at a position in the foreground. + pub fn push(&mut self, pos: Point, item: FrameItem) { + Arc::make_mut(&mut self.items).push((pos, item)); + } + + /// Add a frame at a position in the foreground. + /// + /// Automatically decides whether to inline the frame or to include it as a + /// group based on the number of items in it. + pub fn push_frame(&mut self, pos: Point, frame: Frame) { + if self.should_inline(&frame) { + self.inline(self.layer(), pos, frame); + } else { + self.push(pos, FrameItem::Group(GroupItem::new(frame))); + } + } + + /// Insert an item at the given layer in the frame. + /// + /// This panics if the layer is greater than the number of layers present. + #[track_caller] + pub fn insert(&mut self, layer: usize, pos: Point, items: FrameItem) { + Arc::make_mut(&mut self.items).insert(layer, (pos, items)); + } + + /// Add an item at a position in the background. + pub fn prepend(&mut self, pos: Point, item: FrameItem) { + Arc::make_mut(&mut self.items).insert(0, (pos, item)); + } + + /// Add multiple items at a position in the background. + /// + /// The first item in the iterator will be the one that is most in the + /// background. + pub fn prepend_multiple<I>(&mut self, items: I) + where + I: IntoIterator<Item = (Point, FrameItem)>, + { + Arc::make_mut(&mut self.items).splice(0..0, items); + } + + /// Add a frame at a position in the background. + pub fn prepend_frame(&mut self, pos: Point, frame: Frame) { + if self.should_inline(&frame) { + self.inline(0, pos, frame); + } else { + self.prepend(pos, FrameItem::Group(GroupItem::new(frame))); + } + } + + /// Whether the given frame should be inlined. + fn should_inline(&self, frame: &Frame) -> bool { + self.items.is_empty() || frame.items.len() <= 5 + } + + /// Inline a frame at the given layer. + fn inline(&mut self, layer: usize, pos: Point, frame: Frame) { + // Try to just reuse the items. + if pos.is_zero() && self.items.is_empty() { + self.items = frame.items; + return; + } + + // Try to transfer the items without adjusting the position. + // Also try to reuse the items if the Arc isn't shared. + let range = layer..layer; + if pos.is_zero() { + let sink = Arc::make_mut(&mut self.items); + match Arc::try_unwrap(frame.items) { + Ok(items) => { + sink.splice(range, items); + } + Err(arc) => { + sink.splice(range, arc.iter().cloned()); + } + } + return; + } + + // We have to adjust the item positions. + // But still try to reuse the items if the Arc isn't shared. + let sink = Arc::make_mut(&mut self.items); + match Arc::try_unwrap(frame.items) { + Ok(items) => { + sink.splice(range, items.into_iter().map(|(p, e)| (p + pos, e))); + } + Err(arc) => { + sink.splice(range, arc.iter().cloned().map(|(p, e)| (p + pos, e))); + } + } + } +} + +/// Modify the frame. +impl Frame { + /// Remove all items from the frame. + pub fn clear(&mut self) { + if Arc::strong_count(&self.items) == 1 { + Arc::make_mut(&mut self.items).clear(); + } else { + self.items = Arc::new(vec![]); + } + } + + /// Resize the frame to a new size, distributing new space according to the + /// given alignments. + pub fn resize(&mut self, target: Size, aligns: Axes<Align>) { + if self.size != target { + let offset = Point::new( + aligns.x.position(target.x - self.size.x), + aligns.y.position(target.y - self.size.y), + ); + self.size = target; + self.translate(offset); + } + } + + /// Move the baseline and contents of the frame by an offset. + pub fn translate(&mut self, offset: Point) { + if !offset.is_zero() { + if let Some(baseline) = &mut self.baseline { + *baseline += offset.y; + } + for (point, _) in Arc::make_mut(&mut self.items) { + *point += offset; + } + } + } + + /// Attach the metadata from this style chain to the frame. + pub fn meta(&mut self, styles: StyleChain, force: bool) { + if force || !self.is_empty() { + self.meta_iter(MetaElem::data_in(styles)); + } + } + + /// Attach metadata from an iterator. + pub fn meta_iter(&mut self, iter: impl IntoIterator<Item = Meta>) { + let mut hide = false; + for meta in iter { + if matches!(meta, Meta::Hide) { + hide = true; + } else { + self.prepend(Point::zero(), FrameItem::Meta(meta, self.size)); + } + } + if hide { + Arc::make_mut(&mut self.items).retain(|(_, item)| { + matches!(item, FrameItem::Group(_) | FrameItem::Meta(Meta::Elem(_), _)) + }); + } + } + + /// Add a background fill. + pub fn fill(&mut self, fill: Paint) { + self.prepend( + Point::zero(), + FrameItem::Shape(Geometry::Rect(self.size()).filled(fill), Span::detached()), + ); + } + + /// Add a fill and stroke with optional radius and outset to the frame. + pub fn fill_and_stroke( + &mut self, + fill: Option<Paint>, + stroke: Sides<Option<Stroke>>, + outset: Sides<Rel<Abs>>, + radius: Corners<Rel<Abs>>, + span: Span, + ) { + let outset = outset.relative_to(self.size()); + let size = self.size() + outset.sum_by_axis(); + let pos = Point::new(-outset.left, -outset.top); + let radius = radius.map(|side| side.relative_to(size.x.min(size.y) / 2.0)); + self.prepend_multiple( + rounded_rect(size, radius, fill, stroke) + .into_iter() + .map(|x| (pos, FrameItem::Shape(x, span))), + ) + } + + /// Arbitrarily transform the contents of the frame. + pub fn transform(&mut self, transform: Transform) { + if !self.is_empty() { + self.group(|g| g.transform = transform); + } + } + + /// Clip the contents of a frame to its size. + pub fn clip(&mut self) { + if !self.is_empty() { + self.group(|g| g.clips = true); + } + } + + /// Wrap the frame's contents in a group and modify that group with `f`. + fn group<F>(&mut self, f: F) + where + F: FnOnce(&mut GroupItem), + { + let mut wrapper = Frame::new(self.size); + wrapper.baseline = self.baseline; + let mut group = GroupItem::new(std::mem::take(self)); + f(&mut group); + wrapper.push(Point::zero(), FrameItem::Group(group)); + *self = wrapper; + } +} + +/// Tools for debugging. +impl Frame { + /// Add a full size aqua background and a red baseline for debugging. + pub fn debug(mut self) -> Self { + self.insert( + 0, + Point::zero(), + FrameItem::Shape( + Geometry::Rect(self.size) + .filled(RgbaColor { a: 100, ..Color::TEAL.to_rgba() }.into()), + Span::detached(), + ), + ); + self.insert( + 1, + Point::with_y(self.baseline()), + FrameItem::Shape( + Geometry::Line(Point::with_x(self.size.x)).stroked(Stroke { + paint: Color::RED.into(), + thickness: Abs::pt(1.0), + ..Stroke::default() + }), + Span::detached(), + ), + ); + self + } + + /// Add a green marker at a position for debugging. + pub fn mark_point(&mut self, pos: Point) { + let radius = Abs::pt(2.0); + self.push( + pos - Point::splat(radius), + FrameItem::Shape( + geom::ellipse(Size::splat(2.0 * radius), Some(Color::GREEN.into()), None), + Span::detached(), + ), + ); + } + + /// Add a green marker line at a position for debugging. + pub fn mark_line(&mut self, y: Abs) { + self.push( + Point::with_y(y), + FrameItem::Shape( + Geometry::Line(Point::with_x(self.size.x)).stroked(Stroke { + paint: Color::GREEN.into(), + thickness: Abs::pt(1.0), + ..Stroke::default() + }), + Span::detached(), + ), + ); + } +} + +impl Debug for Frame { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Frame ")?; + f.debug_list() + .entries(self.items.iter().map(|(_, item)| item)) + .finish() + } +} + +/// The building block frames are composed of. +#[derive(Clone, Hash)] +pub enum FrameItem { + /// A subframe with optional transformation and clipping. + Group(GroupItem), + /// A run of shaped text. + Text(TextItem), + /// A geometric shape with optional fill and stroke. + Shape(Shape, Span), + /// An image and its size. + Image(Image, Size, Span), + /// Meta information and the region it applies to. + Meta(Meta, Size), +} + +impl Debug for FrameItem { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Group(group) => group.fmt(f), + Self::Text(text) => write!(f, "{text:?}"), + Self::Shape(shape, _) => write!(f, "{shape:?}"), + Self::Image(image, _, _) => write!(f, "{image:?}"), + Self::Meta(meta, _) => write!(f, "{meta:?}"), + } + } +} + +/// A subframe with optional transformation and clipping. +#[derive(Clone, Hash)] +pub struct GroupItem { + /// The group's frame. + pub frame: Frame, + /// A transformation to apply to the group. + pub transform: Transform, + /// Whether the frame should be a clipping boundary. + pub clips: bool, +} + +impl GroupItem { + /// Create a new group with default settings. + pub fn new(frame: Frame) -> Self { + Self { + frame, + transform: Transform::identity(), + clips: false, + } + } +} + +impl Debug for GroupItem { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Group ")?; + self.frame.fmt(f) + } +} + +/// A run of shaped text. +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct TextItem { + /// The font the glyphs are contained in. + pub font: Font, + /// The font size. + pub size: Abs, + /// Glyph color. + pub fill: Paint, + /// The natural language of the text. + pub lang: Lang, + /// The item's plain text. + pub text: EcoString, + /// The glyphs. + pub glyphs: Vec<Glyph>, +} + +impl TextItem { + /// The width of the text run. + pub fn width(&self) -> Abs { + self.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size) + } +} + +impl Debug for TextItem { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Text(")?; + self.text.fmt(f)?; + f.write_str(")") + } +} + +/// A glyph in a run of shaped text. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Glyph { + /// The glyph's index in the font. + pub id: u16, + /// The advance width of the glyph. + pub x_advance: Em, + /// The horizontal offset of the glyph. + pub x_offset: Em, + /// The range of the glyph in its item's text. + pub range: Range<u16>, + /// The source code location of the text. + pub span: (Span, u16), +} + +impl Glyph { + /// The range of the glyph in its item's text. + pub fn range(&self) -> Range<usize> { + usize::from(self.range.start)..usize::from(self.range.end) + } +} + +/// An identifier for a natural language. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Lang([u8; 3], u8); + +impl Lang { + pub const ALBANIAN: Self = Self(*b"sq ", 2); + pub const ARABIC: Self = Self(*b"ar ", 2); + pub const BOKMÅL: Self = Self(*b"nb ", 2); + pub const CHINESE: Self = Self(*b"zh ", 2); + pub const CZECH: Self = Self(*b"cs ", 2); + pub const DANISH: Self = Self(*b"da ", 2); + pub const DUTCH: Self = Self(*b"nl ", 2); + pub const ENGLISH: Self = Self(*b"en ", 2); + pub const FILIPINO: Self = Self(*b"tl ", 2); + pub const FRENCH: Self = Self(*b"fr ", 2); + pub const GERMAN: Self = Self(*b"de ", 2); + pub const ITALIAN: Self = Self(*b"it ", 2); + pub const JAPANESE: Self = Self(*b"ja ", 2); + pub const NYNORSK: Self = Self(*b"nn ", 2); + pub const POLISH: Self = Self(*b"pl ", 2); + pub const PORTUGUESE: Self = Self(*b"pt ", 2); + pub const RUSSIAN: Self = Self(*b"ru ", 2); + pub const SLOVENIAN: Self = Self(*b"sl ", 2); + pub const SPANISH: Self = Self(*b"es ", 2); + pub const SWEDISH: Self = Self(*b"sv ", 2); + pub const TURKISH: Self = Self(*b"tr ", 2); + pub const UKRAINIAN: Self = Self(*b"ua ", 2); + pub const VIETNAMESE: Self = Self(*b"vi ", 2); + + /// Return the language code as an all lowercase string slice. + pub fn as_str(&self) -> &str { + std::str::from_utf8(&self.0[..usize::from(self.1)]).unwrap_or_default() + } + + /// The default direction for the language. + pub fn dir(self) -> Dir { + match self.as_str() { + "ar" | "dv" | "fa" | "he" | "ks" | "pa" | "ps" | "sd" | "ug" | "ur" + | "yi" => Dir::RTL, + _ => Dir::LTR, + } + } +} + +impl FromStr for Lang { + type Err = &'static str; + + /// Construct a language from a two- or three-byte ISO 639-1/2/3 code. + fn from_str(iso: &str) -> Result<Self, Self::Err> { + let len = iso.len(); + if matches!(len, 2..=3) && iso.is_ascii() { + let mut bytes = [b' '; 3]; + bytes[..len].copy_from_slice(iso.as_bytes()); + bytes.make_ascii_lowercase(); + Ok(Self(bytes, len as u8)) + } else { + Err("expected two or three letter language code (ISO 639-1/2/3)") + } + } +} + +cast! { + Lang, + self => self.as_str().into_value(), + string: EcoString => Self::from_str(&string)?, +} + +/// An identifier for a region somewhere in the world. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Region([u8; 2]); + +impl Region { + /// Return the region code as an all uppercase string slice. + pub fn as_str(&self) -> &str { + std::str::from_utf8(&self.0).unwrap_or_default() + } +} + +impl PartialEq<&str> for Region { + fn eq(&self, other: &&str) -> bool { + self.as_str() == *other + } +} + +impl FromStr for Region { + type Err = &'static str; + + /// Construct a region from its two-byte ISO 3166-1 alpha-2 code. + fn from_str(iso: &str) -> Result<Self, Self::Err> { + if iso.len() == 2 && iso.is_ascii() { + let mut bytes: [u8; 2] = iso.as_bytes().try_into().unwrap(); + bytes.make_ascii_uppercase(); + Ok(Self(bytes)) + } else { + Err("expected two letter region code (ISO 3166-1 alpha-2)") + } + } +} + +cast! { + Region, + self => self.as_str().into_value(), + string: EcoString => Self::from_str(&string)?, +} + +/// Meta information that isn't visible or renderable. +#[derive(Clone, PartialEq, Hash)] +pub enum Meta { + /// An internal or external link to a destination. + Link(Destination), + /// An identifiable element that produces something within the area this + /// metadata is attached to. + Elem(Content), + /// The numbering of the current page. + PageNumbering(Value), + /// Indicates that content should be hidden. This variant doesn't appear + /// in the final frames as it is removed alongside the content that should + /// be hidden. + Hide, +} + +cast! { + type Meta: "meta", +} + +impl Debug for Meta { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Link(dest) => write!(f, "Link({dest:?})"), + Self::Elem(content) => write!(f, "Elem({:?})", content.func()), + Self::PageNumbering(value) => write!(f, "PageNumbering({value:?})"), + Self::Hide => f.pad("Hide"), + } + } +} + +/// A link destination. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum Destination { + /// A link to a URL. + Url(EcoString), + /// A link to a point on a page. + Position(Position), + /// An unresolved link to a location in the document. + Location(Location), +} + +cast! { + Destination, + self => match self { + Self::Url(v) => v.into_value(), + Self::Position(v) => v.into_value(), + Self::Location(v) => v.into_value(), + }, + v: EcoString => Self::Url(v), + v: Position => Self::Position(v), + v: Location => Self::Location(v), +} + +/// A physical position in a document. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Position { + /// The page, starting at 1. + pub page: NonZeroUsize, + /// The exact coordinates on the page (from the top left, as usual). + pub point: Point, +} + +cast! { + Position, + self => Value::Dict(self.into()), + mut dict: Dict => { + let page = dict.take("page")?.cast()?; + let x: Length = dict.take("x")?.cast()?; + let y: Length = dict.take("y")?.cast()?; + dict.finish(&["page", "x", "y"])?; + Self { page, point: Point::new(x.abs, y.abs) } + }, +} + +impl From<Position> for Dict { + fn from(pos: Position) -> Self { + dict! { + "page" => pos.page, + "x" => pos.point.x, + "y" => pos.point.y, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::option_eq; + + #[test] + fn test_region_option_eq() { + let region = Some(Region([b'U', b'S'])); + assert!(option_eq(region, "US")); + assert!(!option_eq(region, "AB")); + } + + #[test] + fn test_document_is_send() { + fn ensure_send<T: Send>() {} + ensure_send::<Document>(); + } +} diff --git a/crates/typst/src/eval/args.rs b/crates/typst/src/eval/args.rs new file mode 100644 index 00000000..da29eeaf --- /dev/null +++ b/crates/typst/src/eval/args.rs @@ -0,0 +1,216 @@ +use std::fmt::{self, Debug, Formatter}; + +use ecow::{eco_format, EcoVec}; + +use super::{Array, Dict, FromValue, IntoValue, Str, Value}; +use crate::diag::{bail, At, SourceResult}; +use crate::syntax::{Span, Spanned}; +use crate::util::pretty_array_like; + +/// Evaluated arguments to a function. +#[derive(Clone, PartialEq, Hash)] +pub struct Args { + /// The span of the whole argument list. + pub span: Span, + /// The positional and named arguments. + pub items: EcoVec<Arg>, +} + +/// An argument to a function call: `12` or `draw: false`. +#[derive(Clone, PartialEq, Hash)] +pub struct Arg { + /// The span of the whole argument. + pub span: Span, + /// The name of the argument (`None` for positional arguments). + pub name: Option<Str>, + /// The value of the argument. + pub value: Spanned<Value>, +} + +impl Args { + /// Create positional arguments from a span and values. + pub fn new<T: IntoValue>(span: Span, values: impl IntoIterator<Item = T>) -> Self { + let items = values + .into_iter() + .map(|value| Arg { + span, + name: None, + value: Spanned::new(value.into_value(), span), + }) + .collect(); + Self { span, items } + } + + /// Push a positional argument. + pub fn push(&mut self, span: Span, value: Value) { + self.items.push(Arg { + span: self.span, + name: None, + value: Spanned::new(value, span), + }) + } + + /// Consume and cast the first positional argument if there is one. + pub fn eat<T>(&mut self) -> SourceResult<Option<T>> + where + T: FromValue<Spanned<Value>>, + { + for (i, slot) in self.items.iter().enumerate() { + if slot.name.is_none() { + let value = self.items.remove(i).value; + let span = value.span; + return T::from_value(value).at(span).map(Some); + } + } + Ok(None) + } + + /// Consume n positional arguments if possible. + pub fn consume(&mut self, n: usize) -> SourceResult<Vec<Arg>> { + let mut list = vec![]; + + let mut i = 0; + while i < self.items.len() && list.len() < n { + if self.items[i].name.is_none() { + list.push(self.items.remove(i)); + } else { + i += 1; + } + } + + if list.len() < n { + bail!(self.span, "not enough arguments"); + } + + Ok(list) + } + + /// Consume and cast the first positional argument. + /// + /// Returns a `missing argument: {what}` error if no positional argument is + /// left. + pub fn expect<T>(&mut self, what: &str) -> SourceResult<T> + where + T: FromValue<Spanned<Value>>, + { + match self.eat()? { + Some(v) => Ok(v), + None => bail!(self.span, "missing argument: {what}"), + } + } + + /// Find and consume the first castable positional argument. + pub fn find<T>(&mut self) -> SourceResult<Option<T>> + where + T: FromValue<Spanned<Value>>, + { + for (i, slot) in self.items.iter().enumerate() { + if slot.name.is_none() && T::castable(&slot.value.v) { + let value = self.items.remove(i).value; + let span = value.span; + return T::from_value(value).at(span).map(Some); + } + } + Ok(None) + } + + /// Find and consume all castable positional arguments. + pub fn all<T>(&mut self) -> SourceResult<Vec<T>> + where + T: FromValue<Spanned<Value>>, + { + let mut list = vec![]; + while let Some(value) = self.find()? { + list.push(value); + } + Ok(list) + } + + /// Cast and remove the value for the given named argument, returning an + /// error if the conversion fails. + pub fn named<T>(&mut self, name: &str) -> SourceResult<Option<T>> + where + T: FromValue<Spanned<Value>>, + { + // We don't quit once we have a match because when multiple matches + // exist, we want to remove all of them and use the last one. + let mut i = 0; + let mut found = None; + while i < self.items.len() { + if self.items[i].name.as_deref() == Some(name) { + let value = self.items.remove(i).value; + let span = value.span; + found = Some(T::from_value(value).at(span)?); + } else { + i += 1; + } + } + Ok(found) + } + + /// Same as named, but with fallback to find. + pub fn named_or_find<T>(&mut self, name: &str) -> SourceResult<Option<T>> + where + T: FromValue<Spanned<Value>>, + { + match self.named(name)? { + Some(value) => Ok(Some(value)), + None => self.find(), + } + } + + /// Take out all arguments into a new instance. + pub fn take(&mut self) -> Self { + Self { + span: self.span, + items: std::mem::take(&mut self.items), + } + } + + /// Return an "unexpected argument" error if there is any remaining + /// argument. + pub fn finish(self) -> SourceResult<()> { + if let Some(arg) = self.items.first() { + match &arg.name { + Some(name) => bail!(arg.span, "unexpected argument: {name}"), + _ => bail!(arg.span, "unexpected argument"), + } + } + Ok(()) + } + + /// Extract the positional arguments as an array. + pub fn to_pos(&self) -> Array { + self.items + .iter() + .filter(|item| item.name.is_none()) + .map(|item| item.value.v.clone()) + .collect() + } + + /// Extract the named arguments as a dictionary. + pub fn to_named(&self) -> Dict { + self.items + .iter() + .filter_map(|item| item.name.clone().map(|name| (name, item.value.v.clone()))) + .collect() + } +} + +impl Debug for Args { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let pieces: Vec<_> = + self.items.iter().map(|arg| eco_format!("{arg:?}")).collect(); + f.write_str(&pretty_array_like(&pieces, false)) + } +} + +impl Debug for Arg { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if let Some(name) = &self.name { + f.write_str(name)?; + f.write_str(": ")?; + } + Debug::fmt(&self.value.v, f) + } +} diff --git a/crates/typst/src/eval/array.rs b/crates/typst/src/eval/array.rs new file mode 100644 index 00000000..a7a1387b --- /dev/null +++ b/crates/typst/src/eval/array.rs @@ -0,0 +1,508 @@ +use std::cmp::Ordering; +use std::fmt::{self, Debug, Formatter}; +use std::ops::{Add, AddAssign}; + +use ecow::{eco_format, EcoString, EcoVec}; + +use super::{ops, Args, CastInfo, FromValue, Func, IntoValue, Reflect, Value, Vm}; +use crate::diag::{At, SourceResult, StrResult}; +use crate::syntax::Span; +use crate::util::pretty_array_like; + +/// Create a new [`Array`] from values. +#[macro_export] +#[doc(hidden)] +macro_rules! __array { + ($value:expr; $count:expr) => { + $crate::eval::Array::from($crate::eval::eco_vec![ + $crate::eval::IntoValue::into_value($value); + $count + ]) + }; + + ($($value:expr),* $(,)?) => { + $crate::eval::Array::from($crate::eval::eco_vec![$( + $crate::eval::IntoValue::into_value($value) + ),*]) + }; +} + +#[doc(inline)] +pub use crate::__array as array; +use crate::eval::ops::{add, mul}; +#[doc(hidden)] +pub use ecow::eco_vec; + +/// A reference counted array with value semantics. +#[derive(Default, Clone, PartialEq, Hash)] +pub struct Array(EcoVec<Value>); + +impl Array { + /// Create a new, empty array. + pub fn new() -> Self { + Self::default() + } + + /// Return `true` if the length is 0. + pub fn is_empty(&self) -> bool { + self.0.len() == 0 + } + + /// The length of the array. + pub fn len(&self) -> usize { + self.0.len() + } + + /// The first value in the array. + pub fn first(&self) -> StrResult<&Value> { + self.0.first().ok_or_else(array_is_empty) + } + + /// Mutably borrow the first value in the array. + pub fn first_mut(&mut self) -> StrResult<&mut Value> { + self.0.make_mut().first_mut().ok_or_else(array_is_empty) + } + + /// The last value in the array. + pub fn last(&self) -> StrResult<&Value> { + self.0.last().ok_or_else(array_is_empty) + } + + /// Mutably borrow the last value in the array. + pub fn last_mut(&mut self) -> StrResult<&mut Value> { + self.0.make_mut().last_mut().ok_or_else(array_is_empty) + } + + /// Borrow the value at the given index. + pub fn at<'a>( + &'a self, + index: i64, + default: Option<&'a Value>, + ) -> StrResult<&'a Value> { + self.locate(index) + .and_then(|i| self.0.get(i)) + .or(default) + .ok_or_else(|| out_of_bounds_no_default(index, self.len())) + } + + /// Mutably borrow the value at the given index. + pub fn at_mut(&mut self, index: i64) -> StrResult<&mut Value> { + let len = self.len(); + self.locate(index) + .and_then(move |i| self.0.make_mut().get_mut(i)) + .ok_or_else(|| out_of_bounds_no_default(index, len)) + } + + /// Push a value to the end of the array. + pub fn push(&mut self, value: Value) { + self.0.push(value); + } + + /// Remove the last value in the array. + pub fn pop(&mut self) -> StrResult<Value> { + self.0.pop().ok_or_else(array_is_empty) + } + + /// Insert a value at the specified index. + pub fn insert(&mut self, index: i64, value: Value) -> StrResult<()> { + let len = self.len(); + let i = self + .locate(index) + .filter(|&i| i <= self.0.len()) + .ok_or_else(|| out_of_bounds(index, len))?; + + self.0.insert(i, value); + Ok(()) + } + + /// Remove and return the value at the specified index. + pub fn remove(&mut self, index: i64) -> StrResult<Value> { + let len = self.len(); + let i = self + .locate(index) + .filter(|&i| i < self.0.len()) + .ok_or_else(|| out_of_bounds(index, len))?; + + Ok(self.0.remove(i)) + } + + /// Extract a contiguous subregion of the array. + pub fn slice(&self, start: i64, end: Option<i64>) -> StrResult<Self> { + let len = self.len(); + let start = self + .locate(start) + .filter(|&start| start <= self.0.len()) + .ok_or_else(|| out_of_bounds(start, len))?; + + let end = end.unwrap_or(self.len() as i64); + let end = self + .locate(end) + .filter(|&end| end <= self.0.len()) + .ok_or_else(|| out_of_bounds(end, len))? + .max(start); + + Ok(self.0[start..end].into()) + } + + /// Whether the array contains a specific value. + pub fn contains(&self, value: &Value) -> bool { + self.0.contains(value) + } + + /// Return the first matching item. + pub fn find(&self, vm: &mut Vm, func: Func) -> SourceResult<Option<Value>> { + for item in self.iter() { + let args = Args::new(func.span(), [item.clone()]); + if func.call_vm(vm, args)?.cast::<bool>().at(func.span())? { + return Ok(Some(item.clone())); + } + } + Ok(None) + } + + /// Return the index of the first matching item. + pub fn position(&self, vm: &mut Vm, func: Func) -> SourceResult<Option<i64>> { + for (i, item) in self.iter().enumerate() { + let args = Args::new(func.span(), [item.clone()]); + if func.call_vm(vm, args)?.cast::<bool>().at(func.span())? { + return Ok(Some(i as i64)); + } + } + + Ok(None) + } + + /// Return a new array with only those items for which the function returns + /// true. + pub fn filter(&self, vm: &mut Vm, func: Func) -> SourceResult<Self> { + let mut kept = EcoVec::new(); + for item in self.iter() { + let args = Args::new(func.span(), [item.clone()]); + if func.call_vm(vm, args)?.cast::<bool>().at(func.span())? { + kept.push(item.clone()) + } + } + Ok(kept.into()) + } + + /// Transform each item in the array with a function. + pub fn map(&self, vm: &mut Vm, func: Func) -> SourceResult<Self> { + self.iter() + .map(|item| { + let args = Args::new(func.span(), [item.clone()]); + func.call_vm(vm, args) + }) + .collect() + } + + /// Fold all of the array's items into one with a function. + pub fn fold(&self, vm: &mut Vm, init: Value, func: Func) -> SourceResult<Value> { + let mut acc = init; + for item in self.iter() { + let args = Args::new(func.span(), [acc, item.clone()]); + acc = func.call_vm(vm, args)?; + } + Ok(acc) + } + + /// Calculates the sum of the array's items + pub fn sum(&self, default: Option<Value>, span: Span) -> SourceResult<Value> { + let mut acc = self + .first() + .map(|x| x.clone()) + .or_else(|_| { + default.ok_or_else(|| { + eco_format!("cannot calculate sum of empty array with no default") + }) + }) + .at(span)?; + for i in self.iter().skip(1) { + acc = add(acc, i.clone()).at(span)?; + } + Ok(acc) + } + + /// Calculates the product of the array's items + pub fn product(&self, default: Option<Value>, span: Span) -> SourceResult<Value> { + let mut acc = self + .first() + .map(|x| x.clone()) + .or_else(|_| { + default.ok_or_else(|| { + eco_format!("cannot calculate product of empty array with no default") + }) + }) + .at(span)?; + for i in self.iter().skip(1) { + acc = mul(acc, i.clone()).at(span)?; + } + Ok(acc) + } + + /// Whether any item matches. + pub fn any(&self, vm: &mut Vm, func: Func) -> SourceResult<bool> { + for item in self.iter() { + let args = Args::new(func.span(), [item.clone()]); + if func.call_vm(vm, args)?.cast::<bool>().at(func.span())? { + return Ok(true); + } + } + + Ok(false) + } + + /// Whether all items match. + pub fn all(&self, vm: &mut Vm, func: Func) -> SourceResult<bool> { + for item in self.iter() { + let args = Args::new(func.span(), [item.clone()]); + if !func.call_vm(vm, args)?.cast::<bool>().at(func.span())? { + return Ok(false); + } + } + + Ok(true) + } + + /// Return a new array with all items from this and nested arrays. + pub fn flatten(&self) -> Self { + let mut flat = EcoVec::with_capacity(self.0.len()); + for item in self.iter() { + if let Value::Array(nested) = item { + flat.extend(nested.flatten().into_iter()); + } else { + flat.push(item.clone()); + } + } + flat.into() + } + + /// Returns a new array with reversed order. + pub fn rev(&self) -> Self { + self.0.iter().cloned().rev().collect() + } + + /// Split all values in the array. + pub fn split(&self, at: Value) -> Array { + self.as_slice() + .split(|value| *value == at) + .map(|subslice| Value::Array(subslice.iter().cloned().collect())) + .collect() + } + + /// Join all values in the array, optionally with separator and last + /// separator (between the final two items). + pub fn join(&self, sep: Option<Value>, mut last: Option<Value>) -> StrResult<Value> { + let len = self.0.len(); + let sep = sep.unwrap_or(Value::None); + + let mut result = Value::None; + for (i, value) in self.iter().cloned().enumerate() { + if i > 0 { + if i + 1 == len && last.is_some() { + result = ops::join(result, last.take().unwrap())?; + } else { + result = ops::join(result, sep.clone())?; + } + } + + result = ops::join(result, value)?; + } + + Ok(result) + } + + /// Zips the array with another array. If the two arrays are of unequal length, it will only + /// zip up until the last element of the smaller array and the remaining elements will be + /// ignored. The return value is an array where each element is yet another array of size 2. + pub fn zip(&self, other: Array) -> Array { + self.iter() + .zip(other) + .map(|(first, second)| array![first.clone(), second].into_value()) + .collect() + } + + /// Return a sorted version of this array, optionally by a given key function. + /// + /// Returns an error if two values could not be compared or if the key function (if given) + /// yields an error. + pub fn sorted( + &self, + vm: &mut Vm, + span: Span, + key: Option<Func>, + ) -> SourceResult<Self> { + let mut result = Ok(()); + let mut vec = self.0.clone(); + let mut key_of = |x: Value| match &key { + // NOTE: We are relying on `comemo`'s memoization of function + // evaluation to not excessively reevaluate the `key`. + Some(f) => f.call_vm(vm, Args::new(f.span(), [x])), + None => Ok(x), + }; + vec.make_mut().sort_by(|a, b| { + // Until we get `try` blocks :) + match (key_of(a.clone()), key_of(b.clone())) { + (Ok(a), Ok(b)) => { + typst::eval::ops::compare(&a, &b).unwrap_or_else(|err| { + if result.is_ok() { + result = Err(err).at(span); + } + Ordering::Equal + }) + } + (Err(e), _) | (_, Err(e)) => { + if result.is_ok() { + result = Err(e); + } + Ordering::Equal + } + } + }); + result.map(|_| vec.into()) + } + + /// Repeat this array `n` times. + pub fn repeat(&self, n: i64) -> StrResult<Self> { + let count = usize::try_from(n) + .ok() + .and_then(|n| self.0.len().checked_mul(n)) + .ok_or_else(|| format!("cannot repeat this array {} times", n))?; + + Ok(self.iter().cloned().cycle().take(count).collect()) + } + + /// Extract a slice of the whole array. + pub fn as_slice(&self) -> &[Value] { + self.0.as_slice() + } + + /// Iterate over references to the contained values. + pub fn iter(&self) -> std::slice::Iter<Value> { + self.0.iter() + } + + /// Resolve an index. + fn locate(&self, index: i64) -> Option<usize> { + usize::try_from(if index >= 0 { + index + } else { + (self.len() as i64).checked_add(index)? + }) + .ok() + } + + /// Enumerate all items in the array. + pub fn enumerate(&self) -> Self { + self.iter() + .enumerate() + .map(|(i, value)| array![i, value.clone()].into_value()) + .collect() + } +} + +impl Debug for Array { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let pieces: Vec<_> = self.iter().map(|value| eco_format!("{value:?}")).collect(); + f.write_str(&pretty_array_like(&pieces, self.len() == 1)) + } +} + +impl Add for Array { + type Output = Self; + + fn add(mut self, rhs: Array) -> Self::Output { + self += rhs; + self + } +} + +impl AddAssign for Array { + fn add_assign(&mut self, rhs: Array) { + self.0.extend(rhs.0); + } +} + +impl Extend<Value> for Array { + fn extend<T: IntoIterator<Item = Value>>(&mut self, iter: T) { + self.0.extend(iter); + } +} + +impl FromIterator<Value> for Array { + fn from_iter<T: IntoIterator<Item = Value>>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + +impl IntoIterator for Array { + type Item = Value; + type IntoIter = ecow::vec::IntoIter<Value>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'a> IntoIterator for &'a Array { + type Item = &'a Value; + type IntoIter = std::slice::Iter<'a, Value>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl From<EcoVec<Value>> for Array { + fn from(v: EcoVec<Value>) -> Self { + Array(v) + } +} + +impl From<&[Value]> for Array { + fn from(v: &[Value]) -> Self { + Array(v.into()) + } +} + +impl<T> Reflect for Vec<T> { + fn describe() -> CastInfo { + Array::describe() + } + + fn castable(value: &Value) -> bool { + Array::castable(value) + } +} + +impl<T: IntoValue> IntoValue for Vec<T> { + fn into_value(self) -> Value { + Value::Array(self.into_iter().map(IntoValue::into_value).collect()) + } +} + +impl<T: FromValue> FromValue for Vec<T> { + fn from_value(value: Value) -> StrResult<Self> { + value.cast::<Array>()?.into_iter().map(Value::cast).collect() + } +} + +/// The error message when the array is empty. +#[cold] +fn array_is_empty() -> EcoString { + "array is empty".into() +} + +/// The out of bounds access error message. +#[cold] +fn out_of_bounds(index: i64, len: usize) -> EcoString { + eco_format!("array index out of bounds (index: {index}, len: {len})") +} + +/// The out of bounds access error message when no default value was given. +#[cold] +fn out_of_bounds_no_default(index: i64, len: usize) -> EcoString { + eco_format!( + "array index out of bounds (index: {index}, len: {len}) \ + and no default value was specified", + ) +} diff --git a/crates/typst/src/eval/auto.rs b/crates/typst/src/eval/auto.rs new file mode 100644 index 00000000..e73b3f33 --- /dev/null +++ b/crates/typst/src/eval/auto.rs @@ -0,0 +1,39 @@ +use std::fmt::{self, Debug, Formatter}; + +use super::{CastInfo, FromValue, IntoValue, Reflect, Value}; +use crate::diag::StrResult; + +/// A value that indicates a smart default. +#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct AutoValue; + +impl IntoValue for AutoValue { + fn into_value(self) -> Value { + Value::Auto + } +} + +impl FromValue for AutoValue { + fn from_value(value: Value) -> StrResult<Self> { + match value { + Value::Auto => Ok(Self), + _ => Err(Self::error(&value)), + } + } +} + +impl Reflect for AutoValue { + fn describe() -> CastInfo { + CastInfo::Type("auto") + } + + fn castable(value: &Value) -> bool { + matches!(value, Value::Auto) + } +} + +impl Debug for AutoValue { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("auto") + } +} diff --git a/crates/typst/src/eval/cast.rs b/crates/typst/src/eval/cast.rs new file mode 100644 index 00000000..917972ed --- /dev/null +++ b/crates/typst/src/eval/cast.rs @@ -0,0 +1,316 @@ +pub use typst_macros::{cast, Cast}; + +use std::fmt::Write; +use std::ops::Add; + +use ecow::EcoString; + +use super::Value; +use crate::diag::{At, SourceResult, StrResult}; +use crate::syntax::{Span, Spanned}; +use crate::util::separated_list; + +/// Determine details of a type. +/// +/// Type casting works as follows: +/// - [`Reflect for T`](Reflect) describes the possible Typst values for `T` +/// (for documentation and autocomplete). +/// - [`IntoValue for T`](IntoValue) is for conversion from `T -> Value` +/// (infallible) +/// - [`FromValue for T`](FromValue) is for conversion from `Value -> T` +/// (fallible). +/// +/// We can't use `TryFrom<Value>` due to conflicting impls. We could use +/// `From<T> for Value`, but that inverses the impl and leads to tons of +/// `.into()` all over the place that become hard to decipher. +pub trait Reflect { + /// Describe the acceptable values for this type. + fn describe() -> CastInfo; + + /// Whether the given value can be converted to `T`. + /// + /// This exists for performance. The check could also be done through the + /// [`CastInfo`], but it would be much more expensive (heap allocation + + /// dynamic checks instead of optimized machine code for each type). + fn castable(value: &Value) -> bool; + + /// Produce an error message for an inacceptable value. + /// + /// ``` + /// # use typst::eval::{Int, Reflect, Value}; + /// assert_eq!( + /// <Int as Reflect>::error(Value::None), + /// "expected integer, found none", + /// ); + /// ``` + fn error(found: &Value) -> EcoString { + Self::describe().error(found) + } +} + +impl Reflect for Value { + fn describe() -> CastInfo { + CastInfo::Any + } + + fn castable(_: &Value) -> bool { + true + } +} + +impl<T: Reflect> Reflect for Spanned<T> { + fn describe() -> CastInfo { + T::describe() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl<T: Reflect> Reflect for StrResult<T> { + fn describe() -> CastInfo { + T::describe() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl<T: Reflect> Reflect for SourceResult<T> { + fn describe() -> CastInfo { + T::describe() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl<T: Reflect> Reflect for &T { + fn describe() -> CastInfo { + T::describe() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl<T: Reflect> Reflect for &mut T { + fn describe() -> CastInfo { + T::describe() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +/// Cast a Rust type into a Typst [`Value`]. +/// +/// See also: [`Reflect`]. +pub trait IntoValue { + /// Cast this type into a value. + fn into_value(self) -> Value; +} + +impl IntoValue for Value { + fn into_value(self) -> Value { + self + } +} + +impl<T: IntoValue> IntoValue for Spanned<T> { + fn into_value(self) -> Value { + self.v.into_value() + } +} + +/// Cast a Rust type or result into a [`SourceResult<Value>`]. +/// +/// Converts `T`, [`StrResult<T>`], or [`SourceResult<T>`] into +/// [`SourceResult<Value>`] by `Ok`-wrapping or adding span information. +pub trait IntoResult { + /// Cast this type into a value. + fn into_result(self, span: Span) -> SourceResult<Value>; +} + +impl<T: IntoValue> IntoResult for T { + fn into_result(self, _: Span) -> SourceResult<Value> { + Ok(self.into_value()) + } +} + +impl<T: IntoValue> IntoResult for StrResult<T> { + fn into_result(self, span: Span) -> SourceResult<Value> { + self.map(IntoValue::into_value).at(span) + } +} + +impl<T: IntoValue> IntoResult for SourceResult<T> { + fn into_result(self, _: Span) -> SourceResult<Value> { + self.map(IntoValue::into_value) + } +} + +/// Try to cast a Typst [`Value`] into a Rust type. +/// +/// See also: [`Reflect`]. +pub trait FromValue<V = Value>: Sized + Reflect { + /// Try to cast the value into an instance of `Self`. + fn from_value(value: V) -> StrResult<Self>; +} + +impl FromValue for Value { + fn from_value(value: Value) -> StrResult<Self> { + Ok(value) + } +} + +impl<T: FromValue> FromValue<Spanned<Value>> for T { + fn from_value(value: Spanned<Value>) -> StrResult<Self> { + T::from_value(value.v) + } +} + +impl<T: FromValue> FromValue<Spanned<Value>> for Spanned<T> { + fn from_value(value: Spanned<Value>) -> StrResult<Self> { + let span = value.span; + T::from_value(value.v).map(|t| Spanned::new(t, span)) + } +} + +/// Describes a possible value for a cast. +#[derive(Debug, Clone, Hash, PartialEq, PartialOrd)] +pub enum CastInfo { + /// Any value is okay. + Any, + /// A specific value, plus short documentation for that value. + Value(Value, &'static str), + /// Any value of a type. + Type(&'static str), + /// Multiple alternatives. + Union(Vec<Self>), +} + +impl CastInfo { + /// Produce an error message describing what was expected and what was + /// found. + pub fn error(&self, found: &Value) -> EcoString { + fn accumulate( + info: &CastInfo, + found: &Value, + parts: &mut Vec<EcoString>, + matching_type: &mut bool, + ) { + match info { + CastInfo::Any => parts.push("anything".into()), + CastInfo::Value(value, _) => { + parts.push(value.repr().into()); + if value.type_name() == found.type_name() { + *matching_type = true; + } + } + CastInfo::Type(ty) => parts.push((*ty).into()), + CastInfo::Union(options) => { + for option in options { + accumulate(option, found, parts, matching_type); + } + } + } + } + + let mut matching_type = false; + let mut parts = vec![]; + accumulate(self, found, &mut parts, &mut matching_type); + + let mut msg = String::from("expected "); + if parts.is_empty() { + msg.push_str(" nothing"); + } + + msg.push_str(&separated_list(&parts, "or")); + + if !matching_type { + msg.push_str(", found "); + msg.push_str(found.type_name()); + } + if_chain::if_chain! { + if let Value::Int(i) = found; + if parts.iter().any(|p| p == "length"); + if !matching_type; + then { + write!(msg, ": a length needs a unit - did you mean {i}pt?").unwrap(); + } + }; + + msg.into() + } +} + +impl Add for CastInfo { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Self::Union(match (self, rhs) { + (Self::Union(mut lhs), Self::Union(rhs)) => { + for cast in rhs { + if !lhs.contains(&cast) { + lhs.push(cast); + } + } + lhs + } + (Self::Union(mut lhs), rhs) => { + if !lhs.contains(&rhs) { + lhs.push(rhs); + } + lhs + } + (lhs, Self::Union(mut rhs)) => { + if !rhs.contains(&lhs) { + rhs.insert(0, lhs); + } + rhs + } + (lhs, rhs) => vec![lhs, rhs], + }) + } +} + +/// A container for a variadic argument. +pub trait Variadics { + /// The contained type. + type Inner; +} + +impl<T> Variadics for Vec<T> { + type Inner = T; +} + +/// An uninhabitable type. +pub enum Never {} + +impl Reflect for Never { + fn describe() -> CastInfo { + CastInfo::Union(vec![]) + } + + fn castable(_: &Value) -> bool { + false + } +} + +impl IntoValue for Never { + fn into_value(self) -> Value { + match self {} + } +} + +impl FromValue for Never { + fn from_value(value: Value) -> StrResult<Self> { + Err(Self::error(&value)) + } +} diff --git a/crates/typst/src/eval/datetime.rs b/crates/typst/src/eval/datetime.rs new file mode 100644 index 00000000..f3c4a5a1 --- /dev/null +++ b/crates/typst/src/eval/datetime.rs @@ -0,0 +1,201 @@ +use std::fmt; +use std::fmt::{Debug, Formatter}; +use std::hash::Hash; + +use ecow::{eco_format, EcoString, EcoVec}; +use time::error::{Format, InvalidFormatDescription}; +use time::{format_description, PrimitiveDateTime}; + +use crate::eval::cast; +use crate::util::pretty_array_like; + +/// A datetime object that represents either a date, a time or a combination of +/// both. +#[derive(Clone, Copy, PartialEq, Hash)] +pub enum Datetime { + /// Representation as a date. + Date(time::Date), + /// Representation as a time. + Time(time::Time), + /// Representation as a combination of date and time. + Datetime(time::PrimitiveDateTime), +} + +impl Datetime { + /// Display the date and/or time in a certain format. + pub fn display(&self, pattern: Option<EcoString>) -> Result<EcoString, EcoString> { + let pattern = pattern.as_ref().map(EcoString::as_str).unwrap_or(match self { + Datetime::Date(_) => "[year]-[month]-[day]", + Datetime::Time(_) => "[hour]:[minute]:[second]", + Datetime::Datetime(_) => "[year]-[month]-[day] [hour]:[minute]:[second]", + }); + + let format = format_description::parse(pattern) + .map_err(format_time_invalid_format_description_error)?; + + let formatted_result = match self { + Datetime::Date(date) => date.format(&format), + Datetime::Time(time) => time.format(&format), + Datetime::Datetime(datetime) => datetime.format(&format), + } + .map(EcoString::from); + + formatted_result.map_err(format_time_format_error) + } + + /// Return the year of the datetime, if existing. + pub fn year(&self) -> Option<i32> { + match self { + Datetime::Date(date) => Some(date.year()), + Datetime::Time(_) => None, + Datetime::Datetime(datetime) => Some(datetime.year()), + } + } + + /// Return the month of the datetime, if existing. + pub fn month(&self) -> Option<u8> { + match self { + Datetime::Date(date) => Some(date.month().into()), + Datetime::Time(_) => None, + Datetime::Datetime(datetime) => Some(datetime.month().into()), + } + } + + /// Return the weekday of the datetime, if existing. + pub fn weekday(&self) -> Option<u8> { + match self { + Datetime::Date(date) => Some(date.weekday().number_from_monday()), + Datetime::Time(_) => None, + Datetime::Datetime(datetime) => Some(datetime.weekday().number_from_monday()), + } + } + + /// Return the day of the datetime, if existing. + pub fn day(&self) -> Option<u8> { + match self { + Datetime::Date(date) => Some(date.day()), + Datetime::Time(_) => None, + Datetime::Datetime(datetime) => Some(datetime.day()), + } + } + + /// Return the hour of the datetime, if existing. + pub fn hour(&self) -> Option<u8> { + match self { + Datetime::Date(_) => None, + Datetime::Time(time) => Some(time.hour()), + Datetime::Datetime(datetime) => Some(datetime.hour()), + } + } + + /// Return the minute of the datetime, if existing. + pub fn minute(&self) -> Option<u8> { + match self { + Datetime::Date(_) => None, + Datetime::Time(time) => Some(time.minute()), + Datetime::Datetime(datetime) => Some(datetime.minute()), + } + } + + /// Return the second of the datetime, if existing. + pub fn second(&self) -> Option<u8> { + match self { + Datetime::Date(_) => None, + Datetime::Time(time) => Some(time.second()), + Datetime::Datetime(datetime) => Some(datetime.second()), + } + } + + /// Create a datetime from year, month, and day. + pub fn from_ymd(year: i32, month: u8, day: u8) -> Option<Self> { + Some(Datetime::Date( + time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day) + .ok()?, + )) + } + + /// Create a datetime from hour, minute, and second. + pub fn from_hms(hour: u8, minute: u8, second: u8) -> Option<Self> { + Some(Datetime::Time(time::Time::from_hms(hour, minute, second).ok()?)) + } + + /// Create a datetime from day and time. + pub fn from_ymd_hms( + year: i32, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + ) -> Option<Self> { + let date = + time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day) + .ok()?; + let time = time::Time::from_hms(hour, minute, second).ok()?; + Some(Datetime::Datetime(PrimitiveDateTime::new(date, time))) + } +} + +impl Debug for Datetime { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let year = self.year().map(|y| eco_format!("year: {y}")); + let month = self.month().map(|m| eco_format!("month: {m}")); + let day = self.day().map(|d| eco_format!("day: {d}")); + let hour = self.hour().map(|h| eco_format!("hour: {h}")); + let minute = self.minute().map(|m| eco_format!("minute: {m}")); + let second = self.second().map(|s| eco_format!("second: {s}")); + let filtered = [year, month, day, hour, minute, second] + .into_iter() + .flatten() + .collect::<EcoVec<_>>(); + + write!(f, "datetime{}", &pretty_array_like(&filtered, false)) + } +} + +cast! { + type Datetime: "datetime", +} + +/// Format the `Format` error of the time crate in an appropriate way. +fn format_time_format_error(error: Format) -> EcoString { + match error { + Format::InvalidComponent(name) => eco_format!("invalid component '{}'", name), + _ => "failed to format datetime in the requested format".into(), + } +} + +/// Format the `InvalidFormatDescription` error of the time crate in an +/// appropriate way. +fn format_time_invalid_format_description_error( + error: InvalidFormatDescription, +) -> EcoString { + match error { + InvalidFormatDescription::UnclosedOpeningBracket { index, .. } => { + eco_format!("missing closing bracket for bracket at index {}", index) + } + InvalidFormatDescription::InvalidComponentName { name, index, .. } => { + eco_format!("invalid component name '{}' at index {}", name, index) + } + InvalidFormatDescription::InvalidModifier { value, index, .. } => { + eco_format!("invalid modifier '{}' at index {}", value, index) + } + InvalidFormatDescription::Expected { what, index, .. } => { + eco_format!("expected {} at index {}", what, index) + } + InvalidFormatDescription::MissingComponentName { index, .. } => { + eco_format!("expected component name at index {}", index) + } + InvalidFormatDescription::MissingRequiredModifier { name, index, .. } => { + eco_format!( + "missing required modifier {} for component at index {}", + name, + index + ) + } + InvalidFormatDescription::NotSupported { context, what, index, .. } => { + eco_format!("{} is not supported in {} at index {}", what, context, index) + } + _ => "failed to parse datetime format".into(), + } +} diff --git a/crates/typst/src/eval/dict.rs b/crates/typst/src/eval/dict.rs new file mode 100644 index 00000000..3e6233ae --- /dev/null +++ b/crates/typst/src/eval/dict.rs @@ -0,0 +1,235 @@ +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::ops::{Add, AddAssign}; +use std::sync::Arc; + +use ecow::{eco_format, EcoString}; + +use super::{array, Array, Str, Value}; +use crate::diag::StrResult; +use crate::syntax::is_ident; +use crate::util::{pretty_array_like, separated_list, ArcExt}; + +/// Create a new [`Dict`] from key-value pairs. +#[macro_export] +#[doc(hidden)] +macro_rules! __dict { + ($($key:expr => $value:expr),* $(,)?) => {{ + #[allow(unused_mut)] + let mut map = $crate::eval::IndexMap::new(); + $(map.insert($key.into(), $crate::eval::IntoValue::into_value($value));)* + $crate::eval::Dict::from(map) + }}; +} + +#[doc(inline)] +pub use crate::__dict as dict; + +#[doc(inline)] +pub use indexmap::IndexMap; + +/// A reference-counted dictionary with value semantics. +#[derive(Default, Clone, PartialEq)] +pub struct Dict(Arc<IndexMap<Str, Value>>); + +impl Dict { + /// Create a new, empty dictionary. + pub fn new() -> Self { + Self::default() + } + + /// Whether the dictionary is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// The number of pairs in the dictionary. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Borrow the value the given `key` maps to, + pub fn at<'a>( + &'a self, + key: &str, + default: Option<&'a Value>, + ) -> StrResult<&'a Value> { + self.0.get(key).or(default).ok_or_else(|| missing_key_no_default(key)) + } + + /// Mutably borrow the value the given `key` maps to. + pub fn at_mut(&mut self, key: &str) -> StrResult<&mut Value> { + Arc::make_mut(&mut self.0) + .get_mut(key) + .ok_or_else(|| missing_key_no_default(key)) + } + + /// Remove the value if the dictionary contains the given key. + pub fn take(&mut self, key: &str) -> StrResult<Value> { + Arc::make_mut(&mut self.0) + .remove(key) + .ok_or_else(|| eco_format!("missing key: {:?}", Str::from(key))) + } + + /// Whether the dictionary contains a specific key. + pub fn contains(&self, key: &str) -> bool { + self.0.contains_key(key) + } + + /// Insert a mapping from the given `key` to the given `value`. + pub fn insert(&mut self, key: Str, value: Value) { + Arc::make_mut(&mut self.0).insert(key, value); + } + + /// Remove a mapping by `key` and return the value. + pub fn remove(&mut self, key: &str) -> StrResult<Value> { + match Arc::make_mut(&mut self.0).shift_remove(key) { + Some(value) => Ok(value), + None => Err(missing_key(key)), + } + } + + /// Clear the dictionary. + pub fn clear(&mut self) { + if Arc::strong_count(&self.0) == 1 { + Arc::make_mut(&mut self.0).clear(); + } else { + *self = Self::new(); + } + } + + /// Return the keys of the dictionary as an array. + pub fn keys(&self) -> Array { + self.0.keys().cloned().map(Value::Str).collect() + } + + /// Return the values of the dictionary as an array. + pub fn values(&self) -> Array { + self.0.values().cloned().collect() + } + + /// Return the values of the dictionary as an array of pairs (arrays of + /// length two). + pub fn pairs(&self) -> Array { + self.0 + .iter() + .map(|(k, v)| Value::Array(array![k.clone(), v.clone()])) + .collect() + } + + /// Iterate over pairs of references to the contained keys and values. + pub fn iter(&self) -> indexmap::map::Iter<Str, Value> { + self.0.iter() + } + + /// Return an "unexpected key" error if there is any remaining pair. + pub fn finish(&self, expected: &[&str]) -> StrResult<()> { + if let Some((key, _)) = self.iter().next() { + let parts: Vec<_> = expected.iter().map(|s| eco_format!("\"{s}\"")).collect(); + let mut msg = format!("unexpected key {key:?}, valid keys are "); + msg.push_str(&separated_list(&parts, "and")); + return Err(msg.into()); + } + Ok(()) + } +} + +impl Debug for Dict { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if self.is_empty() { + return f.write_str("(:)"); + } + + let pieces: Vec<_> = self + .iter() + .map(|(key, value)| { + if is_ident(key) { + eco_format!("{key}: {value:?}") + } else { + eco_format!("{key:?}: {value:?}") + } + }) + .collect(); + + f.write_str(&pretty_array_like(&pieces, false)) + } +} + +impl Add for Dict { + type Output = Self; + + fn add(mut self, rhs: Dict) -> Self::Output { + self += rhs; + self + } +} + +impl AddAssign for Dict { + fn add_assign(&mut self, rhs: Dict) { + match Arc::try_unwrap(rhs.0) { + Ok(map) => self.extend(map), + Err(rc) => self.extend(rc.iter().map(|(k, v)| (k.clone(), v.clone()))), + } + } +} + +impl Hash for Dict { + fn hash<H: Hasher>(&self, state: &mut H) { + state.write_usize(self.0.len()); + for item in self { + item.hash(state); + } + } +} + +impl Extend<(Str, Value)> for Dict { + fn extend<T: IntoIterator<Item = (Str, Value)>>(&mut self, iter: T) { + Arc::make_mut(&mut self.0).extend(iter); + } +} + +impl FromIterator<(Str, Value)> for Dict { + fn from_iter<T: IntoIterator<Item = (Str, Value)>>(iter: T) -> Self { + Self(Arc::new(iter.into_iter().collect())) + } +} + +impl IntoIterator for Dict { + type Item = (Str, Value); + type IntoIter = indexmap::map::IntoIter<Str, Value>; + + fn into_iter(self) -> Self::IntoIter { + Arc::take(self.0).into_iter() + } +} + +impl<'a> IntoIterator for &'a Dict { + type Item = (&'a Str, &'a Value); + type IntoIter = indexmap::map::Iter<'a, Str, Value>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl From<IndexMap<Str, Value>> for Dict { + fn from(map: IndexMap<Str, Value>) -> Self { + Self(Arc::new(map)) + } +} + +/// The missing key access error message. +#[cold] +fn missing_key(key: &str) -> EcoString { + eco_format!("dictionary does not contain key {:?}", Str::from(key)) +} + +/// The missing key access error message when no default was fiven. +#[cold] +fn missing_key_no_default(key: &str) -> EcoString { + eco_format!( + "dictionary does not contain key {:?} \ + and no default value was specified", + Str::from(key) + ) +} diff --git a/crates/typst/src/eval/func.rs b/crates/typst/src/eval/func.rs new file mode 100644 index 00000000..22f948ce --- /dev/null +++ b/crates/typst/src/eval/func.rs @@ -0,0 +1,643 @@ +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +use comemo::{Prehashed, Tracked, TrackedMut}; +use ecow::eco_format; +use once_cell::sync::Lazy; + +use super::{ + cast, Args, CastInfo, Eval, FlowEvent, IntoValue, Route, Scope, Scopes, Tracer, + Value, Vm, +}; +use crate::diag::{bail, SourceResult, StrResult}; +use crate::file::FileId; +use crate::model::{DelayedErrors, ElemFunc, Introspector, Locator, Vt}; +use crate::syntax::ast::{self, AstNode, Expr, Ident}; +use crate::syntax::{Span, SyntaxNode}; +use crate::World; + +/// An evaluatable function. +#[derive(Clone, Hash)] +#[allow(clippy::derived_hash_with_manual_eq)] +pub struct Func { + /// The internal representation. + repr: Repr, + /// The span with which errors are reported when this function is called. + span: Span, +} + +/// The different kinds of function representations. +#[derive(Clone, PartialEq, Hash)] +enum Repr { + /// A native Rust function. + Native(&'static NativeFunc), + /// A function for an element. + Elem(ElemFunc), + /// A user-defined closure. + Closure(Arc<Prehashed<Closure>>), + /// A nested function with pre-applied arguments. + With(Arc<(Func, Args)>), +} + +impl Func { + /// The name of the function. + pub fn name(&self) -> Option<&str> { + match &self.repr { + Repr::Native(native) => Some(native.info.name), + Repr::Elem(func) => Some(func.info().name), + Repr::Closure(closure) => closure.name.as_deref(), + Repr::With(arc) => arc.0.name(), + } + } + + /// Extract details the function. + pub fn info(&self) -> Option<&FuncInfo> { + match &self.repr { + Repr::Native(native) => Some(&native.info), + Repr::Elem(func) => Some(func.info()), + Repr::Closure(_) => None, + Repr::With(arc) => arc.0.info(), + } + } + + /// The function's span. + pub fn span(&self) -> Span { + self.span + } + + /// Attach a span to this function if it doesn't already have one. + pub fn spanned(mut self, span: Span) -> Self { + if self.span.is_detached() { + self.span = span; + } + self + } + + /// Call the function with the given arguments. + pub fn call_vm(&self, vm: &mut Vm, mut args: Args) -> SourceResult<Value> { + let _span = tracing::info_span!( + "call", + name = self.name().unwrap_or("<anon>"), + file = 0, + ); + + match &self.repr { + Repr::Native(native) => { + let value = (native.func)(vm, &mut args)?; + args.finish()?; + Ok(value) + } + Repr::Elem(func) => { + let value = func.construct(vm, &mut args)?; + args.finish()?; + Ok(Value::Content(value)) + } + Repr::Closure(closure) => { + // Determine the route inside the closure. + let fresh = Route::new(closure.location); + let route = + if vm.location.is_detached() { fresh.track() } else { vm.route }; + + Closure::call( + self, + vm.world(), + route, + vm.vt.introspector, + vm.vt.locator.track(), + TrackedMut::reborrow_mut(&mut vm.vt.delayed), + TrackedMut::reborrow_mut(&mut vm.vt.tracer), + vm.depth + 1, + args, + ) + } + Repr::With(arc) => { + args.items = arc.1.items.iter().cloned().chain(args.items).collect(); + arc.0.call_vm(vm, args) + } + } + } + + /// Call the function with a Vt. + #[tracing::instrument(skip_all)] + pub fn call_vt<T: IntoValue>( + &self, + vt: &mut Vt, + args: impl IntoIterator<Item = T>, + ) -> SourceResult<Value> { + let route = Route::default(); + let scopes = Scopes::new(None); + let mut locator = Locator::chained(vt.locator.track()); + let vt = Vt { + world: vt.world, + introspector: vt.introspector, + locator: &mut locator, + delayed: TrackedMut::reborrow_mut(&mut vt.delayed), + tracer: TrackedMut::reborrow_mut(&mut vt.tracer), + }; + let mut vm = Vm::new(vt, route.track(), FileId::detached(), scopes); + let args = Args::new(self.span(), args); + self.call_vm(&mut vm, args) + } + + /// Apply the given arguments to the function. + pub fn with(self, args: Args) -> Self { + let span = self.span; + Self { repr: Repr::With(Arc::new((self, args))), span } + } + + /// Extract the element function, if it is one. + pub fn element(&self) -> Option<ElemFunc> { + match self.repr { + Repr::Elem(func) => Some(func), + _ => None, + } + } + + /// Get a field from this function's scope, if possible. + pub fn get(&self, field: &str) -> StrResult<&Value> { + match &self.repr { + Repr::Native(func) => func.info.scope.get(field).ok_or_else(|| { + eco_format!( + "function `{}` does not contain field `{}`", + func.info.name, + field + ) + }), + Repr::Elem(func) => func.info().scope.get(field).ok_or_else(|| { + eco_format!( + "function `{}` does not contain field `{}`", + func.name(), + field + ) + }), + Repr::Closure(_) => { + Err(eco_format!("cannot access fields on user-defined functions")) + } + Repr::With(arc) => arc.0.get(field), + } + } +} + +impl Debug for Func { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self.name() { + Some(name) => write!(f, "{name}"), + None => f.write_str("(..) => .."), + } + } +} + +impl PartialEq for Func { + fn eq(&self, other: &Self) -> bool { + self.repr == other.repr + } +} + +impl From<Repr> for Func { + fn from(repr: Repr) -> Self { + Self { repr, span: Span::detached() } + } +} + +impl From<ElemFunc> for Func { + fn from(func: ElemFunc) -> Self { + Repr::Elem(func).into() + } +} + +/// A Typst function defined by a native Rust function. +pub struct NativeFunc { + /// The function's implementation. + pub func: fn(&mut Vm, &mut Args) -> SourceResult<Value>, + /// Details about the function. + pub info: Lazy<FuncInfo>, +} + +impl PartialEq for NativeFunc { + fn eq(&self, other: &Self) -> bool { + self.func as usize == other.func as usize + } +} + +impl Eq for NativeFunc {} + +impl Hash for NativeFunc { + fn hash<H: Hasher>(&self, state: &mut H) { + (self.func as usize).hash(state); + } +} + +impl From<&'static NativeFunc> for Func { + fn from(native: &'static NativeFunc) -> Self { + Repr::Native(native).into() + } +} + +cast! { + &'static NativeFunc, + self => Value::Func(self.into()), +} + +/// Details about a function. +#[derive(Debug, Clone)] +pub struct FuncInfo { + /// The function's name. + pub name: &'static str, + /// The display name of the function. + pub display: &'static str, + /// A string of search keywords. + pub keywords: Option<&'static str>, + /// Which category the function is part of. + pub category: &'static str, + /// Documentation for the function. + pub docs: &'static str, + /// Details about the function's parameters. + pub params: Vec<ParamInfo>, + /// Valid values for the return value. + pub returns: CastInfo, + /// The function's own scope of fields and sub-functions. + pub scope: Scope, +} + +impl FuncInfo { + /// Get the parameter info for a parameter with the given name + pub fn param(&self, name: &str) -> Option<&ParamInfo> { + self.params.iter().find(|param| param.name == name) + } +} + +/// Describes a named parameter. +#[derive(Debug, Clone)] +pub struct ParamInfo { + /// The parameter's name. + pub name: &'static str, + /// Documentation for the parameter. + pub docs: &'static str, + /// Valid values for the parameter. + pub cast: CastInfo, + /// Creates an instance of the parameter's default value. + pub default: Option<fn() -> Value>, + /// Is the parameter positional? + pub positional: bool, + /// Is the parameter named? + /// + /// Can be true even if `positional` is true if the parameter can be given + /// in both variants. + pub named: bool, + /// Can the parameter be given any number of times? + pub variadic: bool, + /// Is the parameter required? + pub required: bool, + /// Is the parameter settable with a set rule? + pub settable: bool, +} + +/// A user-defined closure. +#[derive(Hash)] +pub(super) struct Closure { + /// The source file where the closure was defined. + pub location: FileId, + /// The name of the closure. + pub name: Option<Ident>, + /// Captured values from outer scopes. + pub captured: Scope, + /// The list of parameters. + pub params: Vec<Param>, + /// The expression the closure should evaluate to. + pub body: Expr, +} + +/// A closure parameter. +#[derive(Hash)] +pub enum Param { + /// A positional parameter: `x`. + Pos(ast::Pattern), + /// A named parameter with a default value: `draw: false`. + Named(Ident, Value), + /// An argument sink: `..args`. + Sink(Option<Ident>), +} + +impl Closure { + /// Call the function in the context with the arguments. + #[comemo::memoize] + #[tracing::instrument(skip_all)] + #[allow(clippy::too_many_arguments)] + fn call( + this: &Func, + world: Tracked<dyn World + '_>, + route: Tracked<Route>, + introspector: Tracked<Introspector>, + locator: Tracked<Locator>, + delayed: TrackedMut<DelayedErrors>, + tracer: TrackedMut<Tracer>, + depth: usize, + mut args: Args, + ) -> SourceResult<Value> { + let closure = match &this.repr { + Repr::Closure(closure) => closure, + _ => panic!("`this` must be a closure"), + }; + + // Don't leak the scopes from the call site. Instead, we use the scope + // of captured variables we collected earlier. + let mut scopes = Scopes::new(None); + scopes.top = closure.captured.clone(); + + // Prepare VT. + let mut locator = Locator::chained(locator); + let vt = Vt { + world, + introspector, + locator: &mut locator, + delayed, + tracer, + }; + + // Prepare VM. + let mut vm = Vm::new(vt, route, closure.location, scopes); + vm.depth = depth; + + // Provide the closure itself for recursive calls. + if let Some(name) = &closure.name { + vm.define(name.clone(), Value::Func(this.clone())); + } + + // Parse the arguments according to the parameter list. + let num_pos_params = + closure.params.iter().filter(|p| matches!(p, Param::Pos(_))).count(); + let num_pos_args = args.to_pos().len(); + let sink_size = num_pos_args.checked_sub(num_pos_params); + + let mut sink = None; + let mut sink_pos_values = None; + for p in &closure.params { + match p { + Param::Pos(pattern) => match pattern { + ast::Pattern::Normal(ast::Expr::Ident(ident)) => { + vm.define(ident.clone(), args.expect::<Value>(ident)?) + } + ast::Pattern::Normal(_) => unreachable!(), + _ => { + pattern.define( + &mut vm, + args.expect::<Value>("pattern parameter")?, + )?; + } + }, + Param::Sink(ident) => { + sink = ident.clone(); + if let Some(sink_size) = sink_size { + sink_pos_values = Some(args.consume(sink_size)?); + } + } + Param::Named(ident, default) => { + let value = + args.named::<Value>(ident)?.unwrap_or_else(|| default.clone()); + vm.define(ident.clone(), value); + } + } + } + + if let Some(sink) = sink { + let mut remaining_args = args.take(); + if let Some(sink_pos_values) = sink_pos_values { + remaining_args.items.extend(sink_pos_values); + } + vm.define(sink, remaining_args); + } + + // Ensure all arguments have been used. + args.finish()?; + + // Handle control flow. + let result = closure.body.eval(&mut vm); + match vm.flow { + Some(FlowEvent::Return(_, Some(explicit))) => return Ok(explicit), + Some(FlowEvent::Return(_, None)) => {} + Some(flow) => bail!(flow.forbidden()), + None => {} + } + + result + } +} + +impl From<Closure> for Func { + fn from(closure: Closure) -> Self { + Repr::Closure(Arc::new(Prehashed::new(closure))).into() + } +} + +cast! { + Closure, + self => Value::Func(self.into()), +} + +/// A visitor that determines which variables to capture for a closure. +pub(super) struct CapturesVisitor<'a> { + external: &'a Scopes<'a>, + internal: Scopes<'a>, + captures: Scope, +} + +impl<'a> CapturesVisitor<'a> { + /// Create a new visitor for the given external scopes. + pub fn new(external: &'a Scopes) -> Self { + Self { + external, + internal: Scopes::new(None), + captures: Scope::new(), + } + } + + /// Return the scope of captured variables. + pub fn finish(self) -> Scope { + self.captures + } + + /// Visit any node and collect all captured variables. + #[tracing::instrument(skip_all)] + pub fn visit(&mut self, node: &SyntaxNode) { + match node.cast() { + // Every identifier is a potential variable that we need to capture. + // Identifiers that shouldn't count as captures because they + // actually bind a new name are handled below (individually through + // the expressions that contain them). + Some(ast::Expr::Ident(ident)) => self.capture(ident), + Some(ast::Expr::MathIdent(ident)) => self.capture_in_math(ident), + + // Code and content blocks create a scope. + Some(ast::Expr::Code(_) | ast::Expr::Content(_)) => { + self.internal.enter(); + for child in node.children() { + self.visit(child); + } + self.internal.exit(); + } + + // A closure contains parameter bindings, which are bound before the + // body is evaluated. Care must be taken so that the default values + // of named parameters cannot access previous parameter bindings. + Some(ast::Expr::Closure(expr)) => { + for param in expr.params().children() { + if let ast::Param::Named(named) = param { + self.visit(named.expr().as_untyped()); + } + } + + self.internal.enter(); + if let Some(name) = expr.name() { + self.bind(name); + } + + for param in expr.params().children() { + match param { + ast::Param::Pos(pattern) => { + for ident in pattern.idents() { + self.bind(ident); + } + } + ast::Param::Named(named) => self.bind(named.name()), + ast::Param::Sink(spread) => { + self.bind(spread.name().unwrap_or_default()) + } + } + } + + self.visit(expr.body().as_untyped()); + self.internal.exit(); + } + + // A let expression contains a binding, but that binding is only + // active after the body is evaluated. + Some(ast::Expr::Let(expr)) => { + if let Some(init) = expr.init() { + self.visit(init.as_untyped()); + } + + for ident in expr.kind().idents() { + self.bind(ident); + } + } + + // A for loop contains one or two bindings in its pattern. These are + // active after the iterable is evaluated but before the body is + // evaluated. + Some(ast::Expr::For(expr)) => { + self.visit(expr.iter().as_untyped()); + self.internal.enter(); + + let pattern = expr.pattern(); + for ident in pattern.idents() { + self.bind(ident); + } + + self.visit(expr.body().as_untyped()); + self.internal.exit(); + } + + // An import contains items, but these are active only after the + // path is evaluated. + Some(ast::Expr::Import(expr)) => { + self.visit(expr.source().as_untyped()); + if let Some(ast::Imports::Items(items)) = expr.imports() { + for item in items { + self.bind(item); + } + } + } + + // Everything else is traversed from left to right. + _ => { + for child in node.children() { + self.visit(child); + } + } + } + } + + /// Bind a new internal variable. + fn bind(&mut self, ident: ast::Ident) { + self.internal.top.define(ident.take(), Value::None); + } + + /// Capture a variable if it isn't internal. + fn capture(&mut self, ident: ast::Ident) { + if self.internal.get(&ident).is_err() { + if let Ok(value) = self.external.get(&ident) { + self.captures.define_captured(ident.take(), value.clone()); + } + } + } + + /// Capture a variable in math mode if it isn't internal. + fn capture_in_math(&mut self, ident: ast::MathIdent) { + if self.internal.get(&ident).is_err() { + if let Ok(value) = self.external.get_in_math(&ident) { + self.captures.define_captured(ident.take(), value.clone()); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::syntax::parse; + + #[track_caller] + fn test(text: &str, result: &[&str]) { + let mut scopes = Scopes::new(None); + scopes.top.define("f", 0); + scopes.top.define("x", 0); + scopes.top.define("y", 0); + scopes.top.define("z", 0); + + let mut visitor = CapturesVisitor::new(&scopes); + let root = parse(text); + visitor.visit(&root); + + let captures = visitor.finish(); + let mut names: Vec<_> = captures.iter().map(|(k, _)| k).collect(); + names.sort(); + + assert_eq!(names, result); + } + + #[test] + fn test_captures() { + // Let binding and function definition. + test("#let x = x", &["x"]); + test("#let x; #(x + y)", &["y"]); + test("#let f(x, y) = x + y", &[]); + test("#let f(x, y) = f", &[]); + test("#let f = (x, y) => f", &["f"]); + + // Closure with different kinds of params. + test("#((x, y) => x + z)", &["z"]); + test("#((x: y, z) => x + z)", &["y"]); + test("#((..x) => x + y)", &["y"]); + test("#((x, y: x + z) => x + y)", &["x", "z"]); + test("#{x => x; x}", &["x"]); + + // Show rule. + test("#show y: x => x", &["y"]); + test("#show y: x => x + z", &["y", "z"]); + test("#show x: x => x", &["x"]); + + // For loop. + test("#for x in y { x + z }", &["y", "z"]); + test("#for (x, y) in y { x + y }", &["y"]); + test("#for x in y {} #x", &["x", "y"]); + + // Import. + test("#import z: x, y", &["z"]); + test("#import x + y: x, y, z", &["x", "y"]); + + // Blocks. + test("#{ let x = 1; { let y = 2; y }; x + y }", &["y"]); + test("#[#let x = 1]#x", &["x"]); + } +} diff --git a/crates/typst/src/eval/int.rs b/crates/typst/src/eval/int.rs new file mode 100644 index 00000000..4e081617 --- /dev/null +++ b/crates/typst/src/eval/int.rs @@ -0,0 +1,81 @@ +use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize}; + +use super::{cast, Value}; + +macro_rules! signed_int { + ($($ty:ty)*) => { + $(cast! { + $ty, + self => Value::Int(self as i64), + v: i64 => v.try_into().map_err(|_| "number too large")?, + })* + } +} + +macro_rules! unsigned_int { + ($($ty:ty)*) => { + $(cast! { + $ty, + self => Value::Int(self as i64), + v: i64 => v.try_into().map_err(|_| { + if v < 0 { + "number must be at least zero" + } else { + "number too large" + } + })?, + })* + } +} + +macro_rules! signed_nonzero { + ($($ty:ty)*) => { + $(cast! { + $ty, + self => Value::Int(self.get() as i64), + v: i64 => v + .try_into() + .ok() + .and_then($ty::new) + .ok_or_else(|| if v == 0 { + "number must not be zero" + } else { + "number too large" + })?, + })* + } +} + +macro_rules! unsigned_nonzero { + ($($ty:ty)*) => { + $(cast! { + $ty, + self => Value::Int(self.get() as i64), + v: i64 => v + .try_into() + .ok() + .and_then($ty::new) + .ok_or_else(|| if v <= 0 { + "number must be positive" + } else { + "number too large" + })?, + })* + } +} + +signed_int! { + i8 i16 i32 isize +} + +unsigned_int! { + u8 u16 u32 u64 usize +} + +signed_nonzero! { + NonZeroI64 NonZeroIsize +} + +unsigned_nonzero! { + NonZeroU64 NonZeroUsize +} diff --git a/crates/typst/src/eval/library.rs b/crates/typst/src/eval/library.rs new file mode 100644 index 00000000..1b05de83 --- /dev/null +++ b/crates/typst/src/eval/library.rs @@ -0,0 +1,182 @@ +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::num::NonZeroUsize; + +use comemo::Tracked; +use ecow::EcoString; +use std::sync::OnceLock; + +use super::{Args, Dynamic, Module, Value, Vm}; +use crate::diag::SourceResult; +use crate::doc::Document; +use crate::geom::{Abs, Dir}; +use crate::model::{Content, ElemFunc, Introspector, Label, StyleChain, Styles, Vt}; +use crate::syntax::Span; +use crate::util::hash128; + +/// Definition of Typst's standard library. +#[derive(Debug, Clone, Hash)] +pub struct Library { + /// The scope containing definitions that are available everywhere. + pub global: Module, + /// The scope containing definitions available in math mode. + pub math: Module, + /// The default properties for page size, font selection and so on. + pub styles: Styles, + /// Defines which standard library items fulfill which syntactical roles. + pub items: LangItems, +} + +/// Definition of library items the language is aware of. +#[derive(Clone)] +pub struct LangItems { + /// The root layout function. + pub layout: + fn(vt: &mut Vt, content: &Content, styles: StyleChain) -> SourceResult<Document>, + /// Access the em size. + pub em: fn(StyleChain) -> Abs, + /// Access the text direction. + pub dir: fn(StyleChain) -> Dir, + /// Whitespace. + pub space: fn() -> Content, + /// A forced line break: `\`. + pub linebreak: fn() -> Content, + /// Plain text without markup. + pub text: fn(text: EcoString) -> Content, + /// The text function. + pub text_func: ElemFunc, + /// Get the string if this is a text element. + pub text_str: fn(&Content) -> Option<EcoString>, + /// A smart quote: `'` or `"`. + pub smart_quote: fn(double: bool) -> Content, + /// A paragraph break. + pub parbreak: fn() -> Content, + /// Strong content: `*Strong*`. + pub strong: fn(body: Content) -> Content, + /// Emphasized content: `_Emphasized_`. + pub emph: fn(body: Content) -> Content, + /// Raw text with optional syntax highlighting: `` `...` ``. + pub raw: fn(text: EcoString, tag: Option<EcoString>, block: bool) -> Content, + /// The language names and tags supported by raw text. + pub raw_languages: fn() -> Vec<(&'static str, Vec<&'static str>)>, + /// A hyperlink: `https://typst.org`. + pub link: fn(url: EcoString) -> Content, + /// A reference: `@target`, `@target[..]`. + pub reference: fn(target: Label, supplement: Option<Content>) -> Content, + /// The keys contained in the bibliography and short descriptions of them. + #[allow(clippy::type_complexity)] + pub bibliography_keys: + fn(introspector: Tracked<Introspector>) -> Vec<(EcoString, Option<EcoString>)>, + /// A section heading: `= Introduction`. + pub heading: fn(level: NonZeroUsize, body: Content) -> Content, + /// The heading function. + pub heading_func: ElemFunc, + /// An item in a bullet list: `- ...`. + pub list_item: fn(body: Content) -> Content, + /// An item in an enumeration (numbered list): `+ ...` or `1. ...`. + pub enum_item: fn(number: Option<usize>, body: Content) -> Content, + /// An item in a term list: `/ Term: Details`. + pub term_item: fn(term: Content, description: Content) -> Content, + /// A mathematical equation: `$x$`, `$ x^2 $`. + pub equation: fn(body: Content, block: bool) -> Content, + /// An alignment point in math: `&`. + pub math_align_point: fn() -> Content, + /// Matched delimiters in math: `[x + y]`. + pub math_delimited: fn(open: Content, body: Content, close: Content) -> Content, + /// A base with optional attachments in math: `a_1^2`. + #[allow(clippy::type_complexity)] + pub math_attach: fn( + base: Content, + // Positioned smartly. + t: Option<Content>, + b: Option<Content>, + // Fixed positions. + tl: Option<Content>, + bl: Option<Content>, + tr: Option<Content>, + br: Option<Content>, + ) -> Content, + /// A base with an accent: `arrow(x)`. + pub math_accent: fn(base: Content, accent: char) -> Content, + /// A fraction in math: `x/2`. + pub math_frac: fn(num: Content, denom: Content) -> Content, + /// A root in math: `√x`, `∛x` or `∜x`. + pub math_root: fn(index: Option<Content>, radicand: Content) -> Content, + /// Dispatch a method on a library value. + pub library_method: fn( + vm: &mut Vm, + dynamic: &Dynamic, + method: &str, + args: Args, + span: Span, + ) -> SourceResult<Value>, +} + +impl Debug for LangItems { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("LangItems { .. }") + } +} + +impl Hash for LangItems { + fn hash<H: Hasher>(&self, state: &mut H) { + (self.layout as usize).hash(state); + (self.em as usize).hash(state); + (self.dir as usize).hash(state); + self.space.hash(state); + self.linebreak.hash(state); + self.text.hash(state); + self.text_func.hash(state); + (self.text_str as usize).hash(state); + self.smart_quote.hash(state); + self.parbreak.hash(state); + self.strong.hash(state); + self.emph.hash(state); + self.raw.hash(state); + self.raw_languages.hash(state); + self.link.hash(state); + self.reference.hash(state); + (self.bibliography_keys as usize).hash(state); + self.heading.hash(state); + self.heading_func.hash(state); + self.list_item.hash(state); + self.enum_item.hash(state); + self.term_item.hash(state); + self.equation.hash(state); + self.math_align_point.hash(state); + self.math_delimited.hash(state); + self.math_attach.hash(state); + self.math_accent.hash(state); + self.math_frac.hash(state); + self.math_root.hash(state); + (self.library_method as usize).hash(state); + } +} + +/// Global storage for lang items. +#[doc(hidden)] +pub static LANG_ITEMS: OnceLock<LangItems> = OnceLock::new(); + +/// Set the lang items. +/// +/// This is a hack :( +/// +/// Passing the lang items everywhere they are needed (especially text related +/// things) is very painful. By storing them globally, in theory, we break +/// incremental, but only when different sets of lang items are used in the same +/// program. For this reason, if this function is called multiple times, the +/// items must be the same (and this is enforced). +pub fn set_lang_items(items: LangItems) { + if let Err(items) = LANG_ITEMS.set(items) { + let first = hash128(LANG_ITEMS.get().unwrap()); + let second = hash128(&items); + assert_eq!(first, second, "set differing lang items"); + } +} + +/// Access a lang item. +macro_rules! item { + ($name:ident) => { + $crate::eval::LANG_ITEMS.get().unwrap().$name + }; +} diff --git a/crates/typst/src/eval/methods.rs b/crates/typst/src/eval/methods.rs new file mode 100644 index 00000000..62ac4095 --- /dev/null +++ b/crates/typst/src/eval/methods.rs @@ -0,0 +1,373 @@ +//! Methods on values. + +use ecow::EcoString; + +use super::{Args, IntoValue, Str, Value, Vm}; +use crate::diag::{At, SourceResult}; +use crate::eval::Datetime; +use crate::model::{Location, Selector}; +use crate::syntax::Span; + +/// Call a method on a value. +pub fn call( + vm: &mut Vm, + value: Value, + method: &str, + mut args: Args, + span: Span, +) -> SourceResult<Value> { + let name = value.type_name(); + let missing = || Err(missing_method(name, method)).at(span); + + let output = match value { + Value::Color(color) => match method { + "lighten" => color.lighten(args.expect("amount")?).into_value(), + "darken" => color.darken(args.expect("amount")?).into_value(), + "negate" => color.negate().into_value(), + _ => return missing(), + }, + + Value::Str(string) => match method { + "len" => string.len().into_value(), + "first" => string.first().at(span)?.into_value(), + "last" => string.last().at(span)?.into_value(), + "at" => { + let index = args.expect("index")?; + let default = args.named::<EcoString>("default")?; + string.at(index, default.as_deref()).at(span)?.into_value() + } + "slice" => { + let start = args.expect("start")?; + let mut end = args.eat()?; + if end.is_none() { + end = args.named("count")?.map(|c: i64| start + c); + } + string.slice(start, end).at(span)?.into_value() + } + "clusters" => string.clusters().into_value(), + "codepoints" => string.codepoints().into_value(), + "contains" => string.contains(args.expect("pattern")?).into_value(), + "starts-with" => string.starts_with(args.expect("pattern")?).into_value(), + "ends-with" => string.ends_with(args.expect("pattern")?).into_value(), + "find" => string.find(args.expect("pattern")?).into_value(), + "position" => string.position(args.expect("pattern")?).into_value(), + "match" => string.match_(args.expect("pattern")?).into_value(), + "matches" => string.matches(args.expect("pattern")?).into_value(), + "replace" => { + let pattern = args.expect("pattern")?; + let with = args.expect("string or function")?; + let count = args.named("count")?; + string.replace(vm, pattern, with, count)?.into_value() + } + "trim" => { + let pattern = args.eat()?; + let at = args.named("at")?; + let repeat = args.named("repeat")?.unwrap_or(true); + string.trim(pattern, at, repeat).into_value() + } + "split" => string.split(args.eat()?).into_value(), + _ => return missing(), + }, + + Value::Content(content) => match method { + "func" => content.func().into_value(), + "has" => content.has(&args.expect::<EcoString>("field")?).into_value(), + "at" => content + .at(&args.expect::<EcoString>("field")?, args.named("default")?) + .at(span)?, + "fields" => content.dict().into_value(), + "location" => content + .location() + .ok_or("this method can only be called on content returned by query(..)") + .at(span)? + .into_value(), + _ => return missing(), + }, + + Value::Array(array) => match method { + "len" => array.len().into_value(), + "first" => array.first().at(span)?.clone(), + "last" => array.last().at(span)?.clone(), + "at" => array + .at(args.expect("index")?, args.named("default")?.as_ref()) + .at(span)? + .clone(), + "slice" => { + let start = args.expect("start")?; + let mut end = args.eat()?; + if end.is_none() { + end = args.named("count")?.map(|c: i64| start + c); + } + array.slice(start, end).at(span)?.into_value() + } + "contains" => array.contains(&args.expect("value")?).into_value(), + "find" => array.find(vm, args.expect("function")?)?.into_value(), + "position" => array.position(vm, args.expect("function")?)?.into_value(), + "filter" => array.filter(vm, args.expect("function")?)?.into_value(), + "map" => array.map(vm, args.expect("function")?)?.into_value(), + "fold" => { + array.fold(vm, args.expect("initial value")?, args.expect("function")?)? + } + "sum" => array.sum(args.named("default")?, span)?, + "product" => array.product(args.named("default")?, span)?, + "any" => array.any(vm, args.expect("function")?)?.into_value(), + "all" => array.all(vm, args.expect("function")?)?.into_value(), + "flatten" => array.flatten().into_value(), + "rev" => array.rev().into_value(), + "split" => array.split(args.expect("separator")?).into_value(), + "join" => { + let sep = args.eat()?; + let last = args.named("last")?; + array.join(sep, last).at(span)? + } + "sorted" => array.sorted(vm, span, args.named("key")?)?.into_value(), + "zip" => array.zip(args.expect("other")?).into_value(), + "enumerate" => array.enumerate().into_value(), + _ => return missing(), + }, + + Value::Dict(dict) => match method { + "len" => dict.len().into_value(), + "at" => dict + .at(&args.expect::<Str>("key")?, args.named("default")?.as_ref()) + .at(span)? + .clone(), + "keys" => dict.keys().into_value(), + "values" => dict.values().into_value(), + "pairs" => dict.pairs().into_value(), + _ => return missing(), + }, + + Value::Func(func) => match method { + "with" => func.with(args.take()).into_value(), + "where" => { + let fields = args.to_named(); + args.items.retain(|arg| arg.name.is_none()); + func.element() + .ok_or("`where()` can only be called on element functions") + .at(span)? + .where_(fields) + .into_value() + } + _ => return missing(), + }, + + Value::Args(args) => match method { + "pos" => args.to_pos().into_value(), + "named" => args.to_named().into_value(), + _ => return missing(), + }, + + Value::Dyn(dynamic) => { + if let Some(location) = dynamic.downcast::<Location>() { + match method { + "page" => vm.vt.introspector.page(*location).into_value(), + "position" => vm.vt.introspector.position(*location).into_value(), + "page-numbering" => vm.vt.introspector.page_numbering(*location), + _ => return missing(), + } + } else if let Some(selector) = dynamic.downcast::<Selector>() { + match method { + "or" => selector.clone().or(args.all::<Selector>()?).into_value(), + "and" => selector.clone().and(args.all::<Selector>()?).into_value(), + "before" => { + let location = args.expect::<Selector>("selector")?; + let inclusive = + args.named_or_find::<bool>("inclusive")?.unwrap_or(true); + selector.clone().before(location, inclusive).into_value() + } + "after" => { + let location = args.expect::<Selector>("selector")?; + let inclusive = + args.named_or_find::<bool>("inclusive")?.unwrap_or(true); + selector.clone().after(location, inclusive).into_value() + } + _ => return missing(), + } + } else if let Some(&datetime) = dynamic.downcast::<Datetime>() { + match method { + "display" => { + datetime.display(args.eat()?).at(args.span)?.into_value() + } + "year" => datetime.year().into_value(), + "month" => datetime.month().into_value(), + "weekday" => datetime.weekday().into_value(), + "day" => datetime.day().into_value(), + "hour" => datetime.hour().into_value(), + "minute" => datetime.minute().into_value(), + "second" => datetime.second().into_value(), + _ => return missing(), + } + } else { + return (vm.items.library_method)(vm, &dynamic, method, args, span); + } + } + + _ => return missing(), + }; + + args.finish()?; + Ok(output) +} + +/// Call a mutating method on a value. +pub fn call_mut( + value: &mut Value, + method: &str, + mut args: Args, + span: Span, +) -> SourceResult<Value> { + let name = value.type_name(); + let missing = || Err(missing_method(name, method)).at(span); + let mut output = Value::None; + + match value { + Value::Array(array) => match method { + "push" => array.push(args.expect("value")?), + "pop" => output = array.pop().at(span)?, + "insert" => { + array.insert(args.expect("index")?, args.expect("value")?).at(span)? + } + "remove" => output = array.remove(args.expect("index")?).at(span)?, + _ => return missing(), + }, + + Value::Dict(dict) => match method { + "insert" => dict.insert(args.expect::<Str>("key")?, args.expect("value")?), + "remove" => { + output = dict.remove(&args.expect::<EcoString>("key")?).at(span)? + } + _ => return missing(), + }, + + _ => return missing(), + } + + args.finish()?; + Ok(output) +} + +/// Call an accessor method on a value. +pub fn call_access<'a>( + value: &'a mut Value, + method: &str, + mut args: Args, + span: Span, +) -> SourceResult<&'a mut Value> { + let name = value.type_name(); + let missing = || Err(missing_method(name, method)).at(span); + + let slot = match value { + Value::Array(array) => match method { + "first" => array.first_mut().at(span)?, + "last" => array.last_mut().at(span)?, + "at" => array.at_mut(args.expect("index")?).at(span)?, + _ => return missing(), + }, + Value::Dict(dict) => match method { + "at" => dict.at_mut(&args.expect::<Str>("key")?).at(span)?, + _ => return missing(), + }, + _ => return missing(), + }; + + args.finish()?; + Ok(slot) +} + +/// Whether a specific method is mutating. +pub fn is_mutating(method: &str) -> bool { + matches!(method, "push" | "pop" | "insert" | "remove") +} + +/// Whether a specific method is an accessor. +pub fn is_accessor(method: &str) -> bool { + matches!(method, "first" | "last" | "at") +} + +/// The missing method error message. +#[cold] +fn missing_method(type_name: &str, method: &str) -> String { + format!("type {type_name} has no method `{method}`") +} + +/// List the available methods for a type and whether they take arguments. +pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] { + match type_name { + "color" => &[("lighten", true), ("darken", true), ("negate", false)], + "string" => &[ + ("len", false), + ("at", true), + ("clusters", false), + ("codepoints", false), + ("contains", true), + ("ends-with", true), + ("find", true), + ("first", false), + ("last", false), + ("match", true), + ("matches", true), + ("position", true), + ("replace", true), + ("slice", true), + ("split", true), + ("starts-with", true), + ("trim", true), + ], + "content" => &[ + ("func", false), + ("has", true), + ("at", true), + ("fields", false), + ("location", false), + ], + "array" => &[ + ("all", true), + ("any", true), + ("at", true), + ("contains", true), + ("filter", true), + ("find", true), + ("first", false), + ("flatten", false), + ("fold", true), + ("insert", true), + ("split", true), + ("join", true), + ("last", false), + ("len", false), + ("map", true), + ("pop", false), + ("position", true), + ("push", true), + ("remove", true), + ("rev", false), + ("slice", true), + ("sorted", false), + ("enumerate", false), + ("zip", true), + ], + "dictionary" => &[ + ("at", true), + ("insert", true), + ("keys", false), + ("len", false), + ("pairs", false), + ("remove", true), + ("values", false), + ], + "function" => &[("where", true), ("with", true)], + "arguments" => &[("named", false), ("pos", false)], + "location" => &[("page", false), ("position", false), ("page-numbering", false)], + "selector" => &[("or", true), ("and", true), ("before", true), ("after", true)], + "counter" => &[ + ("display", true), + ("at", true), + ("final", true), + ("step", true), + ("update", true), + ], + "state" => &[("display", true), ("at", true), ("final", true), ("update", true)], + _ => &[], + } +} diff --git a/crates/typst/src/eval/mod.rs b/crates/typst/src/eval/mod.rs new file mode 100644 index 00000000..fe28e3f3 --- /dev/null +++ b/crates/typst/src/eval/mod.rs @@ -0,0 +1,1908 @@ +//! Evaluation of markup into modules. + +#[macro_use] +mod library; +#[macro_use] +mod cast; +#[macro_use] +mod array; +#[macro_use] +mod dict; +#[macro_use] +mod str; +#[macro_use] +mod value; +mod args; +mod auto; +mod datetime; +mod func; +mod int; +mod methods; +mod module; +mod none; +pub mod ops; +mod scope; +mod symbol; + +#[doc(hidden)] +pub use { + self::library::LANG_ITEMS, + ecow::{eco_format, eco_vec}, + indexmap::IndexMap, + once_cell::sync::Lazy, +}; + +#[doc(inline)] +pub use typst_macros::{func, symbols}; + +pub use self::args::{Arg, Args}; +pub use self::array::{array, Array}; +pub use self::auto::AutoValue; +pub use self::cast::{ + cast, Cast, CastInfo, FromValue, IntoResult, IntoValue, Never, Reflect, Variadics, +}; +pub use self::datetime::Datetime; +pub use self::dict::{dict, Dict}; +pub use self::func::{Func, FuncInfo, NativeFunc, Param, ParamInfo}; +pub use self::library::{set_lang_items, LangItems, Library}; +pub use self::methods::methods_on; +pub use self::module::Module; +pub use self::none::NoneValue; +pub use self::scope::{Scope, Scopes}; +pub use self::str::{format_str, Regex, Str}; +pub use self::symbol::Symbol; +pub use self::value::{Dynamic, Type, Value}; + +use std::collections::HashSet; +use std::mem; +use std::path::Path; + +use comemo::{Track, Tracked, TrackedMut, Validate}; +use ecow::{EcoString, EcoVec}; +use unicode_segmentation::UnicodeSegmentation; + +use self::func::{CapturesVisitor, Closure}; +use crate::diag::{ + bail, error, At, SourceError, SourceResult, StrResult, Trace, Tracepoint, +}; +use crate::file::{FileId, PackageManifest, PackageSpec}; +use crate::model::{ + Content, DelayedErrors, Introspector, Label, Locator, Recipe, ShowableSelector, + Styles, Transform, Unlabellable, Vt, +}; +use crate::syntax::ast::{self, AstNode}; +use crate::syntax::{parse_code, Source, Span, Spanned, SyntaxKind, SyntaxNode}; +use crate::World; + +const MAX_ITERATIONS: usize = 10_000; +const MAX_CALL_DEPTH: usize = 64; + +/// Evaluate a source file and return the resulting module. +#[comemo::memoize] +#[tracing::instrument(skip(world, route, tracer, source))] +pub fn eval( + world: Tracked<dyn World + '_>, + route: Tracked<Route>, + tracer: TrackedMut<Tracer>, + source: &Source, +) -> SourceResult<Module> { + // Prevent cyclic evaluation. + let id = source.id(); + if route.contains(id) { + panic!("Tried to cyclicly evaluate {}", id.path().display()); + } + + // Hook up the lang items. + let library = world.library(); + set_lang_items(library.items.clone()); + + // Prepare VT. + let mut locator = Locator::default(); + let introspector = Introspector::default(); + let mut delayed = DelayedErrors::default(); + let vt = Vt { + world, + introspector: introspector.track(), + locator: &mut locator, + delayed: delayed.track_mut(), + tracer, + }; + + // Prepare VM. + let route = Route::insert(route, id); + let scopes = Scopes::new(Some(library)); + let mut vm = Vm::new(vt, route.track(), id, scopes); + let root = match source.root().cast::<ast::Markup>() { + Some(markup) if vm.traced.is_some() => markup, + _ => source.ast()?, + }; + + // Evaluate the module. + let result = root.eval(&mut vm); + + // Handle control flow. + if let Some(flow) = vm.flow { + bail!(flow.forbidden()); + } + + // Assemble the module. + let name = id.path().file_stem().unwrap_or_default().to_string_lossy(); + Ok(Module::new(name).with_scope(vm.scopes.top).with_content(result?)) +} + +/// Evaluate a string as code and return the resulting value. +/// +/// Everything in the output is associated with the given `span`. +#[comemo::memoize] +pub fn eval_string( + world: Tracked<dyn World + '_>, + code: &str, + span: Span, +) -> SourceResult<Value> { + let mut root = parse_code(code); + root.synthesize(span); + + let errors = root.errors(); + if !errors.is_empty() { + return Err(Box::new(errors)); + } + + // Prepare VT. + let mut tracer = Tracer::default(); + let mut locator = Locator::default(); + let mut delayed = DelayedErrors::default(); + let introspector = Introspector::default(); + let vt = Vt { + world, + introspector: introspector.track(), + locator: &mut locator, + delayed: delayed.track_mut(), + tracer: tracer.track_mut(), + }; + + // Prepare VM. + let route = Route::default(); + let id = FileId::detached(); + let scopes = Scopes::new(Some(world.library())); + let mut vm = Vm::new(vt, route.track(), id, scopes); + + // Evaluate the code. + let code = root.cast::<ast::Code>().unwrap(); + let result = code.eval(&mut vm); + + // Handle control flow. + if let Some(flow) = vm.flow { + bail!(flow.forbidden()); + } + + result +} + +/// A virtual machine. +/// +/// Holds the state needed to [evaluate](eval) Typst sources. A new +/// virtual machine is created for each module evaluation and function call. +pub struct Vm<'a> { + /// The underlying virtual typesetter. + pub vt: Vt<'a>, + /// The language items. + items: LangItems, + /// The route of source ids the VM took to reach its current location. + route: Tracked<'a, Route<'a>>, + /// The current location. + location: FileId, + /// A control flow event that is currently happening. + flow: Option<FlowEvent>, + /// The stack of scopes. + scopes: Scopes<'a>, + /// The current call depth. + depth: usize, + /// A span that is currently traced. + traced: Option<Span>, +} + +impl<'a> Vm<'a> { + /// Create a new virtual machine. + fn new( + vt: Vt<'a>, + route: Tracked<'a, Route>, + location: FileId, + scopes: Scopes<'a>, + ) -> Self { + let traced = vt.tracer.span(location); + let items = vt.world.library().items.clone(); + Self { + vt, + items, + route, + location, + flow: None, + scopes, + depth: 0, + traced, + } + } + + /// Access the underlying world. + pub fn world(&self) -> Tracked<'a, dyn World + 'a> { + self.vt.world + } + + /// The location to which paths are relative currently. + pub fn location(&self) -> FileId { + self.location + } + + /// Define a variable in the current scope. + #[tracing::instrument(skip_all)] + pub fn define(&mut self, var: ast::Ident, value: impl IntoValue) { + let value = value.into_value(); + if self.traced == Some(var.span()) { + self.vt.tracer.trace(value.clone()); + } + self.scopes.top.define(var.take(), value); + } +} + +/// A control flow event that occurred during evaluation. +#[derive(Debug, Clone, PartialEq)] +pub enum FlowEvent { + /// Stop iteration in a loop. + Break(Span), + /// Skip the remainder of the current iteration in a loop. + Continue(Span), + /// Stop execution of a function early, optionally returning an explicit + /// value. + Return(Span, Option<Value>), +} + +impl FlowEvent { + /// Return an error stating that this control flow is forbidden. + pub fn forbidden(&self) -> SourceError { + match *self { + Self::Break(span) => { + error!(span, "cannot break outside of loop") + } + Self::Continue(span) => { + error!(span, "cannot continue outside of loop") + } + Self::Return(span, _) => { + error!(span, "cannot return outside of function") + } + } + } +} + +/// A route of source ids. +#[derive(Default)] +pub struct Route<'a> { + // We need to override the constraint's lifetime here so that `Tracked` is + // covariant over the constraint. If it becomes invariant, we're in for a + // world of lifetime pain. + outer: Option<Tracked<'a, Self, <Route<'static> as Validate>::Constraint>>, + id: Option<FileId>, +} + +impl<'a> Route<'a> { + /// Create a new route with just one entry. + pub fn new(id: FileId) -> Self { + Self { id: Some(id), outer: None } + } + + /// Insert a new id into the route. + /// + /// You must guarantee that `outer` lives longer than the resulting + /// route is ever used. + pub fn insert(outer: Tracked<'a, Self>, id: FileId) -> Self { + Route { outer: Some(outer), id: Some(id) } + } + + /// Start tracking this locator. + /// + /// In comparison to [`Track::track`], this method skips this chain link + /// if it does not contribute anything. + pub fn track(&self) -> Tracked<'_, Self> { + match self.outer { + Some(outer) if self.id.is_none() => outer, + _ => Track::track(self), + } + } +} + +#[comemo::track] +impl<'a> Route<'a> { + /// Whether the given id is part of the route. + fn contains(&self, id: FileId) -> bool { + self.id == Some(id) || self.outer.map_or(false, |outer| outer.contains(id)) + } +} + +/// Traces which values existed for an expression at a span. +#[derive(Default, Clone)] +pub struct Tracer { + span: Option<Span>, + values: Vec<Value>, +} + +impl Tracer { + /// The maximum number of traced items. + pub const MAX: usize = 10; + + /// Create a new tracer, possibly with a span under inspection. + pub fn new(span: Option<Span>) -> Self { + Self { span, values: vec![] } + } + + /// Get the traced values. + pub fn finish(self) -> Vec<Value> { + self.values + } +} + +#[comemo::track] +impl Tracer { + /// The traced span if it is part of the given source file. + fn span(&self, id: FileId) -> Option<Span> { + if self.span.map(Span::id) == Some(id) { + self.span + } else { + None + } + } + + /// Trace a value for the span. + fn trace(&mut self, v: Value) { + if self.values.len() < Self::MAX { + self.values.push(v); + } + } +} + +/// Evaluate an expression. +pub(super) trait Eval { + /// The output of evaluating the expression. + type Output; + + /// Evaluate the expression to the output value. + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output>; +} + +impl Eval for ast::Markup { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + eval_markup(vm, &mut self.exprs()) + } +} + +/// Evaluate a stream of markup. +fn eval_markup( + vm: &mut Vm, + exprs: &mut impl Iterator<Item = ast::Expr>, +) -> SourceResult<Content> { + let flow = vm.flow.take(); + let mut seq = Vec::with_capacity(exprs.size_hint().1.unwrap_or_default()); + + while let Some(expr) = exprs.next() { + match expr { + ast::Expr::Set(set) => { + let styles = set.eval(vm)?; + if vm.flow.is_some() { + break; + } + + seq.push(eval_markup(vm, exprs)?.styled_with_map(styles)) + } + ast::Expr::Show(show) => { + let recipe = show.eval(vm)?; + if vm.flow.is_some() { + break; + } + + let tail = eval_markup(vm, exprs)?; + seq.push(tail.styled_with_recipe(vm, recipe)?) + } + expr => match expr.eval(vm)? { + Value::Label(label) => { + if let Some(elem) = + seq.iter_mut().rev().find(|node| !node.can::<dyn Unlabellable>()) + { + *elem = mem::take(elem).labelled(label); + } + } + value => seq.push(value.display().spanned(expr.span())), + }, + } + + if vm.flow.is_some() { + break; + } + } + + if flow.is_some() { + vm.flow = flow; + } + + Ok(Content::sequence(seq)) +} + +impl Eval for ast::Expr { + type Output = Value; + + #[tracing::instrument(name = "Expr::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let span = self.span(); + let forbidden = |name| { + error!(span, "{} is only allowed directly in code and content blocks", name) + }; + + let v = match self { + Self::Text(v) => v.eval(vm).map(Value::Content), + Self::Space(v) => v.eval(vm).map(Value::Content), + Self::Linebreak(v) => v.eval(vm).map(Value::Content), + Self::Parbreak(v) => v.eval(vm).map(Value::Content), + Self::Escape(v) => v.eval(vm), + Self::Shorthand(v) => v.eval(vm), + Self::SmartQuote(v) => v.eval(vm).map(Value::Content), + Self::Strong(v) => v.eval(vm).map(Value::Content), + Self::Emph(v) => v.eval(vm).map(Value::Content), + Self::Raw(v) => v.eval(vm).map(Value::Content), + Self::Link(v) => v.eval(vm).map(Value::Content), + Self::Label(v) => v.eval(vm), + Self::Ref(v) => v.eval(vm).map(Value::Content), + Self::Heading(v) => v.eval(vm).map(Value::Content), + Self::List(v) => v.eval(vm).map(Value::Content), + Self::Enum(v) => v.eval(vm).map(Value::Content), + Self::Term(v) => v.eval(vm).map(Value::Content), + Self::Equation(v) => v.eval(vm).map(Value::Content), + Self::Math(v) => v.eval(vm).map(Value::Content), + Self::MathIdent(v) => v.eval(vm), + Self::MathAlignPoint(v) => v.eval(vm).map(Value::Content), + Self::MathDelimited(v) => v.eval(vm).map(Value::Content), + Self::MathAttach(v) => v.eval(vm).map(Value::Content), + Self::MathFrac(v) => v.eval(vm).map(Value::Content), + Self::MathRoot(v) => v.eval(vm).map(Value::Content), + Self::Ident(v) => v.eval(vm), + Self::None(v) => v.eval(vm), + Self::Auto(v) => v.eval(vm), + Self::Bool(v) => v.eval(vm), + Self::Int(v) => v.eval(vm), + Self::Float(v) => v.eval(vm), + Self::Numeric(v) => v.eval(vm), + Self::Str(v) => v.eval(vm), + Self::Code(v) => v.eval(vm), + Self::Content(v) => v.eval(vm).map(Value::Content), + Self::Array(v) => v.eval(vm).map(Value::Array), + Self::Dict(v) => v.eval(vm).map(Value::Dict), + Self::Parenthesized(v) => v.eval(vm), + Self::FieldAccess(v) => v.eval(vm), + Self::FuncCall(v) => v.eval(vm), + Self::Closure(v) => v.eval(vm), + Self::Unary(v) => v.eval(vm), + Self::Binary(v) => v.eval(vm), + Self::Let(v) => v.eval(vm), + Self::DestructAssign(v) => v.eval(vm), + Self::Set(_) => bail!(forbidden("set")), + Self::Show(_) => bail!(forbidden("show")), + Self::Conditional(v) => v.eval(vm), + Self::While(v) => v.eval(vm), + Self::For(v) => v.eval(vm), + Self::Import(v) => v.eval(vm), + Self::Include(v) => v.eval(vm).map(Value::Content), + Self::Break(v) => v.eval(vm), + Self::Continue(v) => v.eval(vm), + Self::Return(v) => v.eval(vm), + }? + .spanned(span); + + if vm.traced == Some(span) { + vm.vt.tracer.trace(v.clone()); + } + + Ok(v) + } +} + +impl ast::Expr { + fn eval_display(&self, vm: &mut Vm) -> SourceResult<Content> { + Ok(self.eval(vm)?.display().spanned(self.span())) + } +} + +impl Eval for ast::Text { + type Output = Content; + + #[tracing::instrument(name = "Text::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok((vm.items.text)(self.get().clone())) + } +} + +impl Eval for ast::Space { + type Output = Content; + + #[tracing::instrument(name = "Space::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok((vm.items.space)()) + } +} + +impl Eval for ast::Linebreak { + type Output = Content; + + #[tracing::instrument(name = "Linebreak::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok((vm.items.linebreak)()) + } +} + +impl Eval for ast::Parbreak { + type Output = Content; + + #[tracing::instrument(name = "Parbreak::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok((vm.items.parbreak)()) + } +} + +impl Eval for ast::Escape { + type Output = Value; + + #[tracing::instrument(name = "Escape::eval", skip_all)] + fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Symbol(Symbol::new(self.get()))) + } +} + +impl Eval for ast::Shorthand { + type Output = Value; + + #[tracing::instrument(name = "Shorthand::eval", skip_all)] + fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Symbol(Symbol::new(self.get()))) + } +} + +impl Eval for ast::SmartQuote { + type Output = Content; + + #[tracing::instrument(name = "SmartQuote::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok((vm.items.smart_quote)(self.double())) + } +} + +impl Eval for ast::Strong { + type Output = Content; + + #[tracing::instrument(name = "Strong::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok((vm.items.strong)(self.body().eval(vm)?)) + } +} + +impl Eval for ast::Emph { + type Output = Content; + + #[tracing::instrument(name = "Emph::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok((vm.items.emph)(self.body().eval(vm)?)) + } +} + +impl Eval for ast::Raw { + type Output = Content; + + #[tracing::instrument(name = "Raw::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let text = self.text(); + let lang = self.lang().map(Into::into); + let block = self.block(); + Ok((vm.items.raw)(text, lang, block)) + } +} + +impl Eval for ast::Link { + type Output = Content; + + #[tracing::instrument(name = "Link::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok((vm.items.link)(self.get().clone())) + } +} + +impl Eval for ast::Label { + type Output = Value; + + #[tracing::instrument(name = "Label::eval", skip_all)] + fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Label(Label(self.get().into()))) + } +} + +impl Eval for ast::Ref { + type Output = Content; + + #[tracing::instrument(name = "Ref::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let label = Label(self.target().into()); + let supplement = self.supplement().map(|block| block.eval(vm)).transpose()?; + Ok((vm.items.reference)(label, supplement)) + } +} + +impl Eval for ast::Heading { + type Output = Content; + + #[tracing::instrument(name = "Heading::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let level = self.level(); + let body = self.body().eval(vm)?; + Ok((vm.items.heading)(level, body)) + } +} + +impl Eval for ast::ListItem { + type Output = Content; + + #[tracing::instrument(name = "ListItem::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok((vm.items.list_item)(self.body().eval(vm)?)) + } +} + +impl Eval for ast::EnumItem { + type Output = Content; + + #[tracing::instrument(name = "EnumItem::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let number = self.number(); + let body = self.body().eval(vm)?; + Ok((vm.items.enum_item)(number, body)) + } +} + +impl Eval for ast::TermItem { + type Output = Content; + + #[tracing::instrument(name = "TermItem::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let term = self.term().eval(vm)?; + let description = self.description().eval(vm)?; + Ok((vm.items.term_item)(term, description)) + } +} + +impl Eval for ast::Equation { + type Output = Content; + + #[tracing::instrument(name = "Equation::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let body = self.body().eval(vm)?; + let block = self.block(); + Ok((vm.items.equation)(body, block)) + } +} + +impl Eval for ast::Math { + type Output = Content; + + #[tracing::instrument(name = "Math::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok(Content::sequence( + self.exprs() + .map(|expr| expr.eval_display(vm)) + .collect::<SourceResult<Vec<_>>>()?, + )) + } +} + +impl Eval for ast::MathIdent { + type Output = Value; + + #[tracing::instrument(name = "MathIdent::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + vm.scopes.get_in_math(self).cloned().at(self.span()) + } +} + +impl Eval for ast::MathAlignPoint { + type Output = Content; + + #[tracing::instrument(name = "MathAlignPoint::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok((vm.items.math_align_point)()) + } +} + +impl Eval for ast::MathDelimited { + type Output = Content; + + #[tracing::instrument(name = "MathDelimited::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let open = self.open().eval_display(vm)?; + let body = self.body().eval(vm)?; + let close = self.close().eval_display(vm)?; + Ok((vm.items.math_delimited)(open, body, close)) + } +} + +impl Eval for ast::MathAttach { + type Output = Content; + + #[tracing::instrument(name = "MathAttach::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let base = self.base().eval_display(vm)?; + let top = self.top().map(|expr| expr.eval_display(vm)).transpose()?; + let bottom = self.bottom().map(|expr| expr.eval_display(vm)).transpose()?; + Ok((vm.items.math_attach)(base, top, bottom, None, None, None, None)) + } +} + +impl Eval for ast::MathFrac { + type Output = Content; + + #[tracing::instrument(name = "MathFrac::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let num = self.num().eval_display(vm)?; + let denom = self.denom().eval_display(vm)?; + Ok((vm.items.math_frac)(num, denom)) + } +} + +impl Eval for ast::MathRoot { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let index = self.index().map(|i| (vm.items.text)(eco_format!("{i}"))); + let radicand = self.radicand().eval_display(vm)?; + Ok((vm.items.math_root)(index, radicand)) + } +} + +impl Eval for ast::Ident { + type Output = Value; + + #[tracing::instrument(name = "Ident::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + vm.scopes.get(self).cloned().at(self.span()) + } +} + +impl Eval for ast::None { + type Output = Value; + + #[tracing::instrument(name = "None::eval", skip_all)] + fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::None) + } +} + +impl Eval for ast::Auto { + type Output = Value; + + #[tracing::instrument(name = "Auto::eval", skip_all)] + fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Auto) + } +} + +impl Eval for ast::Bool { + type Output = Value; + + #[tracing::instrument(name = "Bool::eval", skip_all)] + fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Bool(self.get())) + } +} + +impl Eval for ast::Int { + type Output = Value; + + #[tracing::instrument(name = "Int::eval", skip_all)] + fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Int(self.get())) + } +} + +impl Eval for ast::Float { + type Output = Value; + + #[tracing::instrument(name = "Float::eval", skip_all)] + fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Float(self.get())) + } +} + +impl Eval for ast::Numeric { + type Output = Value; + + #[tracing::instrument(name = "Numeric::eval", skip_all)] + fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::numeric(self.get())) + } +} + +impl Eval for ast::Str { + type Output = Value; + + #[tracing::instrument(name = "Str::eval", skip_all)] + fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Str(self.get().into())) + } +} + +impl Eval for ast::CodeBlock { + type Output = Value; + + #[tracing::instrument(name = "CodeBlock::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + vm.scopes.enter(); + let output = self.body().eval(vm)?; + vm.scopes.exit(); + Ok(output) + } +} + +impl Eval for ast::Code { + type Output = Value; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + eval_code(vm, &mut self.exprs()) + } +} + +/// Evaluate a stream of expressions. +fn eval_code( + vm: &mut Vm, + exprs: &mut impl Iterator<Item = ast::Expr>, +) -> SourceResult<Value> { + let flow = vm.flow.take(); + let mut output = Value::None; + + while let Some(expr) = exprs.next() { + let span = expr.span(); + let value = match expr { + ast::Expr::Set(set) => { + let styles = set.eval(vm)?; + if vm.flow.is_some() { + break; + } + + let tail = eval_code(vm, exprs)?.display(); + Value::Content(tail.styled_with_map(styles)) + } + ast::Expr::Show(show) => { + let recipe = show.eval(vm)?; + if vm.flow.is_some() { + break; + } + + let tail = eval_code(vm, exprs)?.display(); + Value::Content(tail.styled_with_recipe(vm, recipe)?) + } + _ => expr.eval(vm)?, + }; + + output = ops::join(output, value).at(span)?; + + if vm.flow.is_some() { + break; + } + } + + if flow.is_some() { + vm.flow = flow; + } + + Ok(output) +} + +impl Eval for ast::ContentBlock { + type Output = Content; + + #[tracing::instrument(name = "ContentBlock::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + vm.scopes.enter(); + let content = self.body().eval(vm)?; + vm.scopes.exit(); + Ok(content) + } +} + +impl Eval for ast::Parenthesized { + type Output = Value; + + #[tracing::instrument(name = "Parenthesized::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + self.expr().eval(vm) + } +} + +impl Eval for ast::Array { + type Output = Array; + + #[tracing::instrument(skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let items = self.items(); + + let mut vec = EcoVec::with_capacity(items.size_hint().0); + for item in items { + match item { + ast::ArrayItem::Pos(expr) => vec.push(expr.eval(vm)?), + ast::ArrayItem::Spread(expr) => match expr.eval(vm)? { + Value::None => {} + Value::Array(array) => vec.extend(array.into_iter()), + v => bail!(expr.span(), "cannot spread {} into array", v.type_name()), + }, + } + } + + Ok(vec.into()) + } +} + +impl Eval for ast::Dict { + type Output = Dict; + + #[tracing::instrument(skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let mut map = indexmap::IndexMap::new(); + + for item in self.items() { + match item { + ast::DictItem::Named(named) => { + map.insert(named.name().take().into(), named.expr().eval(vm)?); + } + ast::DictItem::Keyed(keyed) => { + map.insert(keyed.key().get().into(), keyed.expr().eval(vm)?); + } + ast::DictItem::Spread(expr) => match expr.eval(vm)? { + Value::None => {} + Value::Dict(dict) => map.extend(dict.into_iter()), + v => bail!( + expr.span(), + "cannot spread {} into dictionary", + v.type_name() + ), + }, + } + } + + Ok(map.into()) + } +} + +impl Eval for ast::Unary { + type Output = Value; + + #[tracing::instrument(name = "Unary::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let value = self.expr().eval(vm)?; + let result = match self.op() { + ast::UnOp::Pos => ops::pos(value), + ast::UnOp::Neg => ops::neg(value), + ast::UnOp::Not => ops::not(value), + }; + result.at(self.span()) + } +} + +impl Eval for ast::Binary { + type Output = Value; + + #[tracing::instrument(name = "Binary::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + match self.op() { + ast::BinOp::Add => self.apply(vm, ops::add), + ast::BinOp::Sub => self.apply(vm, ops::sub), + ast::BinOp::Mul => self.apply(vm, ops::mul), + ast::BinOp::Div => self.apply(vm, ops::div), + ast::BinOp::And => self.apply(vm, ops::and), + ast::BinOp::Or => self.apply(vm, ops::or), + ast::BinOp::Eq => self.apply(vm, ops::eq), + ast::BinOp::Neq => self.apply(vm, ops::neq), + ast::BinOp::Lt => self.apply(vm, ops::lt), + ast::BinOp::Leq => self.apply(vm, ops::leq), + ast::BinOp::Gt => self.apply(vm, ops::gt), + ast::BinOp::Geq => self.apply(vm, ops::geq), + ast::BinOp::In => self.apply(vm, ops::in_), + ast::BinOp::NotIn => self.apply(vm, ops::not_in), + ast::BinOp::Assign => self.assign(vm, |_, b| Ok(b)), + ast::BinOp::AddAssign => self.assign(vm, ops::add), + ast::BinOp::SubAssign => self.assign(vm, ops::sub), + ast::BinOp::MulAssign => self.assign(vm, ops::mul), + ast::BinOp::DivAssign => self.assign(vm, ops::div), + } + } +} + +impl ast::Binary { + /// Apply a basic binary operation. + fn apply( + &self, + vm: &mut Vm, + op: fn(Value, Value) -> StrResult<Value>, + ) -> SourceResult<Value> { + let lhs = self.lhs().eval(vm)?; + + // Short-circuit boolean operations. + if (self.op() == ast::BinOp::And && lhs == Value::Bool(false)) + || (self.op() == ast::BinOp::Or && lhs == Value::Bool(true)) + { + return Ok(lhs); + } + + let rhs = self.rhs().eval(vm)?; + op(lhs, rhs).at(self.span()) + } + + /// Apply an assignment operation. + fn assign( + &self, + vm: &mut Vm, + op: fn(Value, Value) -> StrResult<Value>, + ) -> SourceResult<Value> { + let rhs = self.rhs().eval(vm)?; + let lhs = self.lhs(); + + // An assignment to a dictionary field is different from a normal access + // since it can create the field instead of just modifying it. + if self.op() == ast::BinOp::Assign { + if let ast::Expr::FieldAccess(access) = &lhs { + let dict = access.access_dict(vm)?; + dict.insert(access.field().take().into(), rhs); + return Ok(Value::None); + } + } + + let location = self.lhs().access(vm)?; + let lhs = std::mem::take(&mut *location); + *location = op(lhs, rhs).at(self.span())?; + Ok(Value::None) + } +} + +impl Eval for ast::FieldAccess { + type Output = Value; + + #[tracing::instrument(name = "FieldAccess::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let value = self.target().eval(vm)?; + let field = self.field(); + value.field(&field).at(field.span()) + } +} + +impl Eval for ast::FuncCall { + type Output = Value; + + #[tracing::instrument(name = "FuncCall::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let span = self.span(); + if vm.depth >= MAX_CALL_DEPTH { + bail!(span, "maximum function call depth exceeded"); + } + + let callee = self.callee(); + let in_math = in_math(&callee); + let callee_span = callee.span(); + let args = self.args(); + + // Try to evaluate as a method call. This is possible if the callee is a + // field access and does not evaluate to a module. + let (callee, mut args) = if let ast::Expr::FieldAccess(access) = callee { + let target = access.target(); + let field = access.field(); + let field_span = field.span(); + let field = field.take(); + let point = || Tracepoint::Call(Some(field.clone())); + if methods::is_mutating(&field) { + let args = args.eval(vm)?; + let target = target.access(vm)?; + + // Prioritize a function's own methods (with, where) over its + // fields. This is fine as we define each field of a function, + // if it has any. + // ('methods_on' will be empty for Symbol and Module - their + // method calls always refer to their fields.) + if !matches!(target, Value::Symbol(_) | Value::Module(_) | Value::Func(_)) + || methods_on(target.type_name()).iter().any(|(m, _)| m == &field) + { + return methods::call_mut(target, &field, args, span).trace( + vm.world(), + point, + span, + ); + } + (target.field(&field).at(field_span)?, args) + } else { + let target = target.eval(vm)?; + let args = args.eval(vm)?; + + if !matches!(target, Value::Symbol(_) | Value::Module(_) | Value::Func(_)) + || methods_on(target.type_name()).iter().any(|(m, _)| m == &field) + { + return methods::call(vm, target, &field, args, span).trace( + vm.world(), + point, + span, + ); + } + (target.field(&field).at(field_span)?, args) + } + } else { + (callee.eval(vm)?, args.eval(vm)?) + }; + + // Handle math special cases for non-functions: + // Combining accent symbols apply themselves while everything else + // simply displays the arguments verbatim. + if in_math && !matches!(callee, Value::Func(_)) { + if let Value::Symbol(sym) = &callee { + let c = sym.get(); + if let Some(accent) = Symbol::combining_accent(c) { + let base = args.expect("base")?; + args.finish()?; + return Ok(Value::Content((vm.items.math_accent)(base, accent))); + } + } + let mut body = Content::empty(); + for (i, arg) in args.all::<Content>()?.into_iter().enumerate() { + if i > 0 { + body += (vm.items.text)(','.into()); + } + body += arg; + } + return Ok(Value::Content( + callee.display().spanned(callee_span) + + (vm.items.math_delimited)( + (vm.items.text)('('.into()), + body, + (vm.items.text)(')'.into()), + ), + )); + } + + let callee = callee.cast::<Func>().at(callee_span)?; + let point = || Tracepoint::Call(callee.name().map(Into::into)); + let f = || callee.call_vm(vm, args).trace(vm.world(), point, span); + + // Stacker is broken on WASM. + #[cfg(target_arch = "wasm32")] + return f(); + + #[cfg(not(target_arch = "wasm32"))] + stacker::maybe_grow(32 * 1024, 2 * 1024 * 1024, f) + } +} + +fn in_math(expr: &ast::Expr) -> bool { + match expr { + ast::Expr::MathIdent(_) => true, + ast::Expr::FieldAccess(access) => in_math(&access.target()), + _ => false, + } +} + +impl Eval for ast::Args { + type Output = Args; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let mut items = EcoVec::new(); + + for arg in self.items() { + let span = arg.span(); + match arg { + ast::Arg::Pos(expr) => { + items.push(Arg { + span, + name: None, + value: Spanned::new(expr.eval(vm)?, expr.span()), + }); + } + ast::Arg::Named(named) => { + items.push(Arg { + span, + name: Some(named.name().take().into()), + value: Spanned::new(named.expr().eval(vm)?, named.expr().span()), + }); + } + ast::Arg::Spread(expr) => match expr.eval(vm)? { + Value::None => {} + Value::Array(array) => { + items.extend(array.into_iter().map(|value| Arg { + span, + name: None, + value: Spanned::new(value, span), + })); + } + Value::Dict(dict) => { + items.extend(dict.into_iter().map(|(key, value)| Arg { + span, + name: Some(key), + value: Spanned::new(value, span), + })); + } + Value::Args(args) => items.extend(args.items), + v => bail!(expr.span(), "cannot spread {}", v.type_name()), + }, + } + } + + Ok(Args { span: self.span(), items }) + } +} + +impl Eval for ast::Closure { + type Output = Value; + + #[tracing::instrument(name = "Closure::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + // The closure's name is defined by its let binding if there's one. + let name = self.name(); + + // Collect captured variables. + let captured = { + let mut visitor = CapturesVisitor::new(&vm.scopes); + visitor.visit(self.as_untyped()); + visitor.finish() + }; + + // Collect parameters and an optional sink parameter. + let mut params = Vec::new(); + for param in self.params().children() { + match param { + ast::Param::Pos(pattern) => params.push(Param::Pos(pattern)), + ast::Param::Named(named) => { + params.push(Param::Named(named.name(), named.expr().eval(vm)?)); + } + ast::Param::Sink(spread) => params.push(Param::Sink(spread.name())), + } + } + + // Define the closure. + let closure = Closure { + location: vm.location, + name, + captured, + params, + body: self.body(), + }; + + Ok(Value::Func(Func::from(closure).spanned(self.params().span()))) + } +} + +impl ast::Pattern { + fn destruct_array<F>( + &self, + vm: &mut Vm, + value: Array, + f: F, + destruct: &ast::Destructuring, + ) -> SourceResult<Value> + where + F: Fn(&mut Vm, ast::Expr, Value) -> SourceResult<Value>, + { + let mut i = 0; + let len = value.as_slice().len(); + for p in destruct.bindings() { + match p { + ast::DestructuringKind::Normal(expr) => { + let Ok(v) = value.at(i as i64, None) else { + bail!(expr.span(), "not enough elements to destructure"); + }; + f(vm, expr, v.clone())?; + i += 1; + } + ast::DestructuringKind::Sink(spread) => { + let sink_size = (1 + len).checked_sub(destruct.bindings().count()); + let sink = sink_size.and_then(|s| value.as_slice().get(i..i + s)); + if let (Some(sink_size), Some(sink)) = (sink_size, sink) { + if let Some(expr) = spread.expr() { + f(vm, expr, Value::Array(sink.into()))?; + } + i += sink_size; + } else { + bail!(self.span(), "not enough elements to destructure") + } + } + ast::DestructuringKind::Named(named) => { + bail!(named.span(), "cannot destructure named elements from an array") + } + ast::DestructuringKind::Placeholder(underscore) => { + if i < len { + i += 1 + } else { + bail!(underscore.span(), "not enough elements to destructure") + } + } + } + } + if i < len { + bail!(self.span(), "too many elements to destructure"); + } + + Ok(Value::None) + } + + fn destruct_dict<F>( + &self, + vm: &mut Vm, + dict: Dict, + f: F, + destruct: &ast::Destructuring, + ) -> SourceResult<Value> + where + F: Fn(&mut Vm, ast::Expr, Value) -> SourceResult<Value>, + { + let mut sink = None; + let mut used = HashSet::new(); + for p in destruct.bindings() { + match p { + ast::DestructuringKind::Normal(ast::Expr::Ident(ident)) => { + let v = dict + .at(&ident, None) + .map_err(|_| "destructuring key not found in dictionary") + .at(ident.span())?; + f(vm, ast::Expr::Ident(ident.clone()), v.clone())?; + used.insert(ident.take()); + } + ast::DestructuringKind::Sink(spread) => sink = spread.expr(), + ast::DestructuringKind::Named(named) => { + let name = named.name(); + let v = dict + .at(&name, None) + .map_err(|_| "destructuring key not found in dictionary") + .at(name.span())?; + f(vm, named.expr(), v.clone())?; + used.insert(name.take()); + } + ast::DestructuringKind::Placeholder(_) => {} + ast::DestructuringKind::Normal(expr) => { + bail!(expr.span(), "expected key, found expression"); + } + } + } + + if let Some(expr) = sink { + let mut sink = Dict::new(); + for (key, value) in dict { + if !used.contains(key.as_str()) { + sink.insert(key, value); + } + } + f(vm, expr, Value::Dict(sink))?; + } + + Ok(Value::None) + } + + /// Destruct the given value into the pattern and apply the function to each binding. + #[tracing::instrument(skip_all)] + fn apply<T>(&self, vm: &mut Vm, value: Value, f: T) -> SourceResult<Value> + where + T: Fn(&mut Vm, ast::Expr, Value) -> SourceResult<Value>, + { + match self { + ast::Pattern::Normal(expr) => { + f(vm, expr.clone(), value)?; + Ok(Value::None) + } + ast::Pattern::Placeholder(_) => Ok(Value::None), + ast::Pattern::Destructuring(destruct) => match value { + Value::Array(value) => self.destruct_array(vm, value, f, destruct), + Value::Dict(value) => self.destruct_dict(vm, value, f, destruct), + _ => bail!(self.span(), "cannot destructure {}", value.type_name()), + }, + } + } + + /// Destruct the value into the pattern by binding. + pub fn define(&self, vm: &mut Vm, value: Value) -> SourceResult<Value> { + self.apply(vm, value, |vm, expr, value| match expr { + ast::Expr::Ident(ident) => { + vm.define(ident, value); + Ok(Value::None) + } + _ => bail!(expr.span(), "nested patterns are currently not supported"), + }) + } + + /// Destruct the value into the pattern by assignment. + pub fn assign(&self, vm: &mut Vm, value: Value) -> SourceResult<Value> { + self.apply(vm, value, |vm, expr, value| { + let location = expr.access(vm)?; + *location = value; + Ok(Value::None) + }) + } +} + +impl Eval for ast::LetBinding { + type Output = Value; + + #[tracing::instrument(name = "LetBinding::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let value = match self.init() { + Some(expr) => expr.eval(vm)?, + None => Value::None, + }; + + match self.kind() { + ast::LetBindingKind::Normal(pattern) => pattern.define(vm, value), + ast::LetBindingKind::Closure(ident) => { + vm.define(ident, value); + Ok(Value::None) + } + } + } +} + +impl Eval for ast::DestructAssignment { + type Output = Value; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let value = self.value().eval(vm)?; + self.pattern().assign(vm, value)?; + Ok(Value::None) + } +} + +impl Eval for ast::SetRule { + type Output = Styles; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + if let Some(condition) = self.condition() { + if !condition.eval(vm)?.cast::<bool>().at(condition.span())? { + return Ok(Styles::new()); + } + } + + let target = self.target(); + let target = target + .eval(vm)? + .cast::<Func>() + .and_then(|func| { + func.element().ok_or_else(|| { + "only element functions can be used in set rules".into() + }) + }) + .at(target.span())?; + let args = self.args().eval(vm)?; + Ok(target.set(args)?.spanned(self.span())) + } +} + +impl Eval for ast::ShowRule { + type Output = Recipe; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let selector = self + .selector() + .map(|sel| sel.eval(vm)?.cast::<ShowableSelector>().at(sel.span())) + .transpose()? + .map(|selector| selector.0); + + let transform = self.transform(); + let span = transform.span(); + + let transform = match transform { + ast::Expr::Set(set) => Transform::Style(set.eval(vm)?), + expr => expr.eval(vm)?.cast::<Transform>().at(span)?, + }; + + Ok(Recipe { span, selector, transform }) + } +} + +impl Eval for ast::Conditional { + type Output = Value; + + #[tracing::instrument(name = "Conditional::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let condition = self.condition(); + if condition.eval(vm)?.cast::<bool>().at(condition.span())? { + self.if_body().eval(vm) + } else if let Some(else_body) = self.else_body() { + else_body.eval(vm) + } else { + Ok(Value::None) + } + } +} + +impl Eval for ast::WhileLoop { + type Output = Value; + + #[tracing::instrument(name = "WhileLoop::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let flow = vm.flow.take(); + let mut output = Value::None; + let mut i = 0; + + let condition = self.condition(); + let body = self.body(); + + while condition.eval(vm)?.cast::<bool>().at(condition.span())? { + if i == 0 + && is_invariant(condition.as_untyped()) + && !can_diverge(body.as_untyped()) + { + bail!(condition.span(), "condition is always true"); + } else if i >= MAX_ITERATIONS { + bail!(self.span(), "loop seems to be infinite"); + } + + let value = body.eval(vm)?; + output = ops::join(output, value).at(body.span())?; + + match vm.flow { + Some(FlowEvent::Break(_)) => { + vm.flow = None; + break; + } + Some(FlowEvent::Continue(_)) => vm.flow = None, + Some(FlowEvent::Return(..)) => break, + None => {} + } + + i += 1; + } + + if flow.is_some() { + vm.flow = flow; + } + + Ok(output) + } +} + +/// Whether the expression always evaluates to the same value. +fn is_invariant(expr: &SyntaxNode) -> bool { + match expr.cast() { + Some(ast::Expr::Ident(_)) => false, + Some(ast::Expr::MathIdent(_)) => false, + Some(ast::Expr::FieldAccess(access)) => { + is_invariant(access.target().as_untyped()) + } + Some(ast::Expr::FuncCall(call)) => { + is_invariant(call.callee().as_untyped()) + && is_invariant(call.args().as_untyped()) + } + _ => expr.children().all(is_invariant), + } +} + +/// Whether the expression contains a break or return. +fn can_diverge(expr: &SyntaxNode) -> bool { + matches!(expr.kind(), SyntaxKind::Break | SyntaxKind::Return) + || expr.children().any(can_diverge) +} + +impl Eval for ast::ForLoop { + type Output = Value; + + #[tracing::instrument(name = "ForLoop::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let flow = vm.flow.take(); + let mut output = Value::None; + + macro_rules! iter { + (for $pat:ident in $iter:expr) => {{ + vm.scopes.enter(); + + #[allow(unused_parens)] + for value in $iter { + $pat.define(vm, value.into_value())?; + + let body = self.body(); + let value = body.eval(vm)?; + output = ops::join(output, value).at(body.span())?; + + match vm.flow { + Some(FlowEvent::Break(_)) => { + vm.flow = None; + break; + } + Some(FlowEvent::Continue(_)) => vm.flow = None, + Some(FlowEvent::Return(..)) => break, + None => {} + } + } + + vm.scopes.exit(); + }}; + } + + let iter = self.iter().eval(vm)?; + let pattern = self.pattern(); + + match (&pattern, iter.clone()) { + (ast::Pattern::Normal(_), Value::Str(string)) => { + // Iterate over graphemes of string. + iter!(for pattern in string.as_str().graphemes(true)); + } + (_, Value::Dict(dict)) => { + // Iterate over pairs of dict. + iter!(for pattern in dict.pairs()); + } + (_, Value::Array(array)) => { + // Iterate over values of array. + iter!(for pattern in array); + } + (ast::Pattern::Normal(_), _) => { + bail!(self.iter().span(), "cannot loop over {}", iter.type_name()); + } + (_, _) => { + bail!(pattern.span(), "cannot destructure values of {}", iter.type_name()) + } + } + + if flow.is_some() { + vm.flow = flow; + } + + Ok(output) + } +} + +/// Applies imports from `import` to the current scope. +fn apply_imports<V: IntoValue>( + imports: Option<ast::Imports>, + vm: &mut Vm, + source_value: V, + name: impl Fn(&V) -> EcoString, + scope: impl Fn(&V) -> &Scope, +) -> SourceResult<()> { + match imports { + None => { + vm.scopes.top.define(name(&source_value), source_value); + } + Some(ast::Imports::Wildcard) => { + for (var, value) in scope(&source_value).iter() { + vm.scopes.top.define(var.clone(), value.clone()); + } + } + Some(ast::Imports::Items(idents)) => { + let mut errors = vec![]; + let scope = scope(&source_value); + for ident in idents { + if let Some(value) = scope.get(&ident) { + vm.define(ident, value.clone()); + } else { + errors.push(error!(ident.span(), "unresolved import")); + } + } + if !errors.is_empty() { + return Err(Box::new(errors)); + } + } + } + + Ok(()) +} + +impl Eval for ast::ModuleImport { + type Output = Value; + + #[tracing::instrument(name = "ModuleImport::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let span = self.source().span(); + let source = self.source().eval(vm)?; + if let Value::Func(func) = source { + if func.info().is_none() { + bail!(span, "cannot import from user-defined functions"); + } + apply_imports( + self.imports(), + vm, + func, + |func| func.info().unwrap().name.into(), + |func| &func.info().unwrap().scope, + )?; + } else { + let module = import(vm, source, span, true)?; + apply_imports( + self.imports(), + vm, + module, + |module| module.name().clone(), + |module| module.scope(), + )?; + } + + Ok(Value::None) + } +} + +impl Eval for ast::ModuleInclude { + type Output = Content; + + #[tracing::instrument(name = "ModuleInclude::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let span = self.source().span(); + let source = self.source().eval(vm)?; + let module = import(vm, source, span, false)?; + Ok(module.content()) + } +} + +/// Process an import of a module relative to the current location. +fn import( + vm: &mut Vm, + source: Value, + span: Span, + accept_functions: bool, +) -> SourceResult<Module> { + let path = match source { + Value::Str(path) => path, + Value::Module(module) => return Ok(module), + v => { + if accept_functions { + bail!(span, "expected path, module or function, found {}", v.type_name()) + } else { + bail!(span, "expected path or module, found {}", v.type_name()) + } + } + }; + + // Handle package and file imports. + let path = path.as_str(); + if path.starts_with('@') { + let spec = path.parse::<PackageSpec>().at(span)?; + import_package(vm, spec, span) + } else { + import_file(vm, path, span) + } +} + +/// Import an external package. +fn import_package(vm: &mut Vm, spec: PackageSpec, span: Span) -> SourceResult<Module> { + // Evaluate the manifest. + let manifest_id = FileId::new(Some(spec.clone()), Path::new("/typst.toml")); + let bytes = vm.world().file(manifest_id).at(span)?; + let manifest = PackageManifest::parse(&bytes).at(span)?; + manifest.validate(&spec).at(span)?; + + // Evaluate the entry point. + let entrypoint_id = manifest_id.join(&manifest.package.entrypoint).at(span)?; + let source = vm.world().source(entrypoint_id).at(span)?; + let point = || Tracepoint::Import; + Ok(eval(vm.world(), vm.route, TrackedMut::reborrow_mut(&mut vm.vt.tracer), &source) + .trace(vm.world(), point, span)? + .with_name(manifest.package.name)) +} + +/// Import a file from a path. +fn import_file(vm: &mut Vm, path: &str, span: Span) -> SourceResult<Module> { + // Load the source file. + let world = vm.world(); + let id = vm.location().join(path).at(span)?; + let source = world.source(id).at(span)?; + + // Prevent cyclic importing. + if vm.route.contains(source.id()) { + bail!(span, "cyclic import"); + } + + // Evaluate the file. + let point = || Tracepoint::Import; + eval(world, vm.route, TrackedMut::reborrow_mut(&mut vm.vt.tracer), &source) + .trace(world, point, span) +} + +impl Eval for ast::LoopBreak { + type Output = Value; + + #[tracing::instrument(name = "LoopBreak::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + if vm.flow.is_none() { + vm.flow = Some(FlowEvent::Break(self.span())); + } + Ok(Value::None) + } +} + +impl Eval for ast::LoopContinue { + type Output = Value; + + #[tracing::instrument(name = "LoopContinue::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + if vm.flow.is_none() { + vm.flow = Some(FlowEvent::Continue(self.span())); + } + Ok(Value::None) + } +} + +impl Eval for ast::FuncReturn { + type Output = Value; + + #[tracing::instrument(name = "FuncReturn::eval", skip_all)] + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let value = self.body().map(|body| body.eval(vm)).transpose()?; + if vm.flow.is_none() { + vm.flow = Some(FlowEvent::Return(self.span(), value)); + } + Ok(Value::None) + } +} + +/// Access an expression mutably. +trait Access { + /// Access the value. + fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value>; +} + +impl Access for ast::Expr { + fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { + match self { + Self::Ident(v) => v.access(vm), + Self::Parenthesized(v) => v.access(vm), + Self::FieldAccess(v) => v.access(vm), + Self::FuncCall(v) => v.access(vm), + _ => { + let _ = self.eval(vm)?; + bail!(self.span(), "cannot mutate a temporary value"); + } + } + } +} + +impl Access for ast::Ident { + fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { + let span = self.span(); + let value = vm.scopes.get_mut(self).at(span)?; + if vm.traced == Some(span) { + vm.vt.tracer.trace(value.clone()); + } + Ok(value) + } +} + +impl Access for ast::Parenthesized { + fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { + self.expr().access(vm) + } +} + +impl Access for ast::FieldAccess { + fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { + self.access_dict(vm)?.at_mut(&self.field().take()).at(self.span()) + } +} + +impl ast::FieldAccess { + fn access_dict<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Dict> { + match self.target().access(vm)? { + Value::Dict(dict) => Ok(dict), + value => bail!( + self.target().span(), + "expected dictionary, found {}", + value.type_name(), + ), + } + } +} + +impl Access for ast::FuncCall { + fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { + if let ast::Expr::FieldAccess(access) = self.callee() { + let method = access.field().take(); + if methods::is_accessor(&method) { + let span = self.span(); + let world = vm.world(); + let args = self.args().eval(vm)?; + let value = access.target().access(vm)?; + let result = methods::call_access(value, &method, args, span); + let point = || Tracepoint::Call(Some(method.clone())); + return result.trace(world, point, span); + } + } + + let _ = self.eval(vm)?; + bail!(self.span(), "cannot mutate a temporary value"); + } +} diff --git a/crates/typst/src/eval/module.rs b/crates/typst/src/eval/module.rs new file mode 100644 index 00000000..0bc6bf38 --- /dev/null +++ b/crates/typst/src/eval/module.rs @@ -0,0 +1,98 @@ +use std::fmt::{self, Debug, Formatter}; +use std::sync::Arc; + +use ecow::{eco_format, EcoString}; + +use super::{Content, Scope, Value}; +use crate::diag::StrResult; + +/// An evaluated module, ready for importing or typesetting. +/// +/// Values of this type are cheap to clone and hash. +#[derive(Clone, Hash)] +#[allow(clippy::derived_hash_with_manual_eq)] +pub struct Module { + /// The module's name. + name: EcoString, + /// The reference-counted inner fields. + inner: Arc<Repr>, +} + +/// The internal representation. +#[derive(Clone, Hash)] +struct Repr { + /// The top-level definitions that were bound in this module. + scope: Scope, + /// The module's layoutable contents. + content: Content, +} + +impl Module { + /// Create a new module. + pub fn new(name: impl Into<EcoString>) -> Self { + Self { + name: name.into(), + inner: Arc::new(Repr { scope: Scope::new(), content: Content::empty() }), + } + } + + /// Update the module's name. + pub fn with_name(mut self, name: impl Into<EcoString>) -> Self { + self.name = name.into(); + self + } + + /// Update the module's scope. + pub fn with_scope(mut self, scope: Scope) -> Self { + Arc::make_mut(&mut self.inner).scope = scope; + self + } + + /// Update the module's content. + pub fn with_content(mut self, content: Content) -> Self { + Arc::make_mut(&mut self.inner).content = content; + self + } + + /// Get the module's name. + pub fn name(&self) -> &EcoString { + &self.name + } + + /// Access the module's scope. + pub fn scope(&self) -> &Scope { + &self.inner.scope + } + + /// Access the module's scope, mutably. + pub fn scope_mut(&mut self) -> &mut Scope { + &mut Arc::make_mut(&mut self.inner).scope + } + + /// Try to access a definition in the module. + pub fn get(&self, name: &str) -> StrResult<&Value> { + self.scope().get(name).ok_or_else(|| { + eco_format!("module `{}` does not contain `{name}`", self.name()) + }) + } + + /// Extract the module's content. + pub fn content(self) -> Content { + match Arc::try_unwrap(self.inner) { + Ok(repr) => repr.content, + Err(arc) => arc.content.clone(), + } + } +} + +impl Debug for Module { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "<module {}>", self.name()) + } +} + +impl PartialEq for Module { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && Arc::ptr_eq(&self.inner, &other.inner) + } +} diff --git a/crates/typst/src/eval/none.rs b/crates/typst/src/eval/none.rs new file mode 100644 index 00000000..ab7644a7 --- /dev/null +++ b/crates/typst/src/eval/none.rs @@ -0,0 +1,74 @@ +use std::fmt::{self, Debug, Formatter}; + +use super::{cast, CastInfo, FromValue, IntoValue, Reflect, Value}; +use crate::diag::StrResult; + +/// A value that indicates the absence of any other value. +#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct NoneValue; + +impl Reflect for NoneValue { + fn describe() -> CastInfo { + CastInfo::Type("none") + } + + fn castable(value: &Value) -> bool { + matches!(value, Value::None) + } +} + +impl IntoValue for NoneValue { + fn into_value(self) -> Value { + Value::None + } +} + +impl FromValue for NoneValue { + fn from_value(value: Value) -> StrResult<Self> { + match value { + Value::None => Ok(Self), + _ => Err(Self::error(&value)), + } + } +} + +impl Debug for NoneValue { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("none") + } +} + +cast! { + (), + self => Value::None, + _: NoneValue => (), +} + +impl<T: Reflect> Reflect for Option<T> { + fn describe() -> CastInfo { + T::describe() + NoneValue::describe() + } + + fn castable(value: &Value) -> bool { + NoneValue::castable(value) || T::castable(value) + } +} + +impl<T: IntoValue> IntoValue for Option<T> { + fn into_value(self) -> Value { + match self { + Some(v) => v.into_value(), + None => Value::None, + } + } +} + +impl<T: FromValue> FromValue for Option<T> { + fn from_value(value: Value) -> StrResult<Self> { + match value { + Value::None => Ok(None), + v if T::castable(&v) => Ok(Some(T::from_value(v)?)), + _ => Err(Self::error(&value)), + } + } +} diff --git a/crates/typst/src/eval/ops.rs b/crates/typst/src/eval/ops.rs new file mode 100644 index 00000000..0880a87e --- /dev/null +++ b/crates/typst/src/eval/ops.rs @@ -0,0 +1,429 @@ +//! Operations on values. + +use std::cmp::Ordering; +use std::fmt::Debug; + +use ecow::eco_format; + +use super::{format_str, Regex, Value}; +use crate::diag::{bail, StrResult}; +use crate::geom::{Axes, Axis, GenAlign, Length, Numeric, PartialStroke, Rel, Smart}; +use Value::*; + +/// Bail with a type mismatch error. +macro_rules! mismatch { + ($fmt:expr, $($value:expr),* $(,)?) => { + return Err(eco_format!($fmt, $($value.type_name()),*)) + }; +} + +/// Join a value with another value. +pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> { + Ok(match (lhs, rhs) { + (a, None) => a, + (None, b) => b, + (Symbol(a), Symbol(b)) => Str(format_str!("{a}{b}")), + (Str(a), Str(b)) => Str(a + b), + (Str(a), Symbol(b)) => Str(format_str!("{a}{b}")), + (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")), + (Content(a), Content(b)) => Content(a + b), + (Content(a), Symbol(b)) => Content(a + item!(text)(b.get().into())), + (Content(a), Str(b)) => Content(a + item!(text)(b.into())), + (Str(a), Content(b)) => Content(item!(text)(a.into()) + b), + (Symbol(a), Content(b)) => Content(item!(text)(a.get().into()) + b), + (Array(a), Array(b)) => Array(a + b), + (Dict(a), Dict(b)) => Dict(a + b), + (a, b) => mismatch!("cannot join {} with {}", a, b), + }) +} + +/// Apply the unary plus operator to a value. +pub fn pos(value: Value) -> StrResult<Value> { + Ok(match value { + Int(v) => Int(v), + Float(v) => Float(v), + Length(v) => Length(v), + Angle(v) => Angle(v), + Ratio(v) => Ratio(v), + Relative(v) => Relative(v), + Fraction(v) => Fraction(v), + v => mismatch!("cannot apply '+' to {}", v), + }) +} + +/// Compute the negation of a value. +pub fn neg(value: Value) -> StrResult<Value> { + Ok(match value { + Int(v) => Int(v.checked_neg().ok_or("value is too large")?), + Float(v) => Float(-v), + Length(v) => Length(-v), + Angle(v) => Angle(-v), + Ratio(v) => Ratio(-v), + Relative(v) => Relative(-v), + Fraction(v) => Fraction(-v), + v => mismatch!("cannot apply '-' to {}", v), + }) +} + +/// Compute the sum of two values. +pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> { + Ok(match (lhs, rhs) { + (a, None) => a, + (None, b) => b, + + (Int(a), Int(b)) => Int(a.checked_add(b).ok_or("value is too large")?), + (Int(a), Float(b)) => Float(a as f64 + b), + (Float(a), Int(b)) => Float(a + b as f64), + (Float(a), Float(b)) => Float(a + b), + + (Angle(a), Angle(b)) => Angle(a + b), + + (Length(a), Length(b)) => Length(a + b), + (Length(a), Ratio(b)) => Relative(b + a), + (Length(a), Relative(b)) => Relative(b + a), + + (Ratio(a), Length(b)) => Relative(a + b), + (Ratio(a), Ratio(b)) => Ratio(a + b), + (Ratio(a), Relative(b)) => Relative(b + a), + + (Relative(a), Length(b)) => Relative(a + b), + (Relative(a), Ratio(b)) => Relative(a + b), + (Relative(a), Relative(b)) => Relative(a + b), + + (Fraction(a), Fraction(b)) => Fraction(a + b), + + (Symbol(a), Symbol(b)) => Str(format_str!("{a}{b}")), + (Str(a), Str(b)) => Str(a + b), + (Str(a), Symbol(b)) => Str(format_str!("{a}{b}")), + (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")), + (Content(a), Content(b)) => Content(a + b), + (Content(a), Symbol(b)) => Content(a + item!(text)(b.get().into())), + (Content(a), Str(b)) => Content(a + item!(text)(b.into())), + (Str(a), Content(b)) => Content(item!(text)(a.into()) + b), + (Symbol(a), Content(b)) => Content(item!(text)(a.get().into()) + b), + + (Array(a), Array(b)) => Array(a + b), + (Dict(a), Dict(b)) => Dict(a + b), + + (Color(color), Length(thickness)) | (Length(thickness), Color(color)) => { + Value::dynamic(PartialStroke { + paint: Smart::Custom(color.into()), + thickness: Smart::Custom(thickness), + ..PartialStroke::default() + }) + } + + (Dyn(a), Dyn(b)) => { + // 1D alignments can be summed into 2D alignments. + if let (Some(&a), Some(&b)) = + (a.downcast::<GenAlign>(), b.downcast::<GenAlign>()) + { + if a.axis() == b.axis() { + return Err(eco_format!("cannot add two {:?} alignments", a.axis())); + } + + return Ok(Value::dynamic(match a.axis() { + Axis::X => Axes { x: a, y: b }, + Axis::Y => Axes { x: b, y: a }, + })); + }; + + mismatch!("cannot add {} and {}", a, b); + } + + (a, b) => mismatch!("cannot add {} and {}", a, b), + }) +} + +/// Compute the difference of two values. +pub fn sub(lhs: Value, rhs: Value) -> StrResult<Value> { + Ok(match (lhs, rhs) { + (Int(a), Int(b)) => Int(a.checked_sub(b).ok_or("value is too large")?), + (Int(a), Float(b)) => Float(a as f64 - b), + (Float(a), Int(b)) => Float(a - b as f64), + (Float(a), Float(b)) => Float(a - b), + + (Angle(a), Angle(b)) => Angle(a - b), + + (Length(a), Length(b)) => Length(a - b), + (Length(a), Ratio(b)) => Relative(-b + a), + (Length(a), Relative(b)) => Relative(-b + a), + + (Ratio(a), Length(b)) => Relative(a + -b), + (Ratio(a), Ratio(b)) => Ratio(a - b), + (Ratio(a), Relative(b)) => Relative(-b + a), + + (Relative(a), Length(b)) => Relative(a + -b), + (Relative(a), Ratio(b)) => Relative(a + -b), + (Relative(a), Relative(b)) => Relative(a - b), + + (Fraction(a), Fraction(b)) => Fraction(a - b), + + (a, b) => mismatch!("cannot subtract {1} from {0}", a, b), + }) +} + +/// Compute the product of two values. +pub fn mul(lhs: Value, rhs: Value) -> StrResult<Value> { + Ok(match (lhs, rhs) { + (Int(a), Int(b)) => Int(a.checked_mul(b).ok_or("value is too large")?), + (Int(a), Float(b)) => Float(a as f64 * b), + (Float(a), Int(b)) => Float(a * b as f64), + (Float(a), Float(b)) => Float(a * b), + + (Length(a), Int(b)) => Length(a * b as f64), + (Length(a), Float(b)) => Length(a * b), + (Length(a), Ratio(b)) => Length(a * b.get()), + (Int(a), Length(b)) => Length(b * a as f64), + (Float(a), Length(b)) => Length(b * a), + (Ratio(a), Length(b)) => Length(b * a.get()), + + (Angle(a), Int(b)) => Angle(a * b as f64), + (Angle(a), Float(b)) => Angle(a * b), + (Angle(a), Ratio(b)) => Angle(a * b.get()), + (Int(a), Angle(b)) => Angle(a as f64 * b), + (Float(a), Angle(b)) => Angle(a * b), + (Ratio(a), Angle(b)) => Angle(a.get() * b), + + (Ratio(a), Ratio(b)) => Ratio(a * b), + (Ratio(a), Int(b)) => Ratio(a * b as f64), + (Ratio(a), Float(b)) => Ratio(a * b), + (Int(a), Ratio(b)) => Ratio(a as f64 * b), + (Float(a), Ratio(b)) => Ratio(a * b), + + (Relative(a), Int(b)) => Relative(a * b as f64), + (Relative(a), Float(b)) => Relative(a * b), + (Relative(a), Ratio(b)) => Relative(a * b.get()), + (Int(a), Relative(b)) => Relative(a as f64 * b), + (Float(a), Relative(b)) => Relative(a * b), + (Ratio(a), Relative(b)) => Relative(a.get() * b), + + (Fraction(a), Int(b)) => Fraction(a * b as f64), + (Fraction(a), Float(b)) => Fraction(a * b), + (Fraction(a), Ratio(b)) => Fraction(a * b.get()), + (Int(a), Fraction(b)) => Fraction(a as f64 * b), + (Float(a), Fraction(b)) => Fraction(a * b), + (Ratio(a), Fraction(b)) => Fraction(a.get() * b), + + (Str(a), Int(b)) => Str(a.repeat(b)?), + (Int(a), Str(b)) => Str(b.repeat(a)?), + (Array(a), Int(b)) => Array(a.repeat(b)?), + (Int(a), Array(b)) => Array(b.repeat(a)?), + (Content(a), b @ Int(_)) => Content(a.repeat(b.cast()?)), + (a @ Int(_), Content(b)) => Content(b.repeat(a.cast()?)), + + (a, b) => mismatch!("cannot multiply {} with {}", a, b), + }) +} + +/// Compute the quotient of two values. +pub fn div(lhs: Value, rhs: Value) -> StrResult<Value> { + if is_zero(&rhs) { + bail!("cannot divide by zero"); + } + + Ok(match (lhs, rhs) { + (Int(a), Int(b)) => Float(a as f64 / b as f64), + (Int(a), Float(b)) => Float(a as f64 / b), + (Float(a), Int(b)) => Float(a / b as f64), + (Float(a), Float(b)) => Float(a / b), + + (Length(a), Int(b)) => Length(a / b as f64), + (Length(a), Float(b)) => Length(a / b), + (Length(a), Length(b)) => Float(try_div_length(a, b)?), + (Length(a), Relative(b)) if b.rel.is_zero() => Float(try_div_length(a, b.abs)?), + + (Angle(a), Int(b)) => Angle(a / b as f64), + (Angle(a), Float(b)) => Angle(a / b), + (Angle(a), Angle(b)) => Float(a / b), + + (Ratio(a), Int(b)) => Ratio(a / b as f64), + (Ratio(a), Float(b)) => Ratio(a / b), + (Ratio(a), Ratio(b)) => Float(a / b), + (Ratio(a), Relative(b)) if b.abs.is_zero() => Float(a / b.rel), + + (Relative(a), Int(b)) => Relative(a / b as f64), + (Relative(a), Float(b)) => Relative(a / b), + (Relative(a), Length(b)) if a.rel.is_zero() => Float(try_div_length(a.abs, b)?), + (Relative(a), Ratio(b)) if a.abs.is_zero() => Float(a.rel / b), + (Relative(a), Relative(b)) => Float(try_div_relative(a, b)?), + + (Fraction(a), Int(b)) => Fraction(a / b as f64), + (Fraction(a), Float(b)) => Fraction(a / b), + (Fraction(a), Fraction(b)) => Float(a / b), + + (a, b) => mismatch!("cannot divide {} by {}", a, b), + }) +} + +/// Whether a value is a numeric zero. +fn is_zero(v: &Value) -> bool { + match *v { + Int(v) => v == 0, + Float(v) => v == 0.0, + Length(v) => v.is_zero(), + Angle(v) => v.is_zero(), + Ratio(v) => v.is_zero(), + Relative(v) => v.is_zero(), + Fraction(v) => v.is_zero(), + _ => false, + } +} + +/// Try to divide two lengths. +fn try_div_length(a: Length, b: Length) -> StrResult<f64> { + a.try_div(b).ok_or_else(|| "cannot divide these two lengths".into()) +} + +/// Try to divide two relative lengths. +fn try_div_relative(a: Rel<Length>, b: Rel<Length>) -> StrResult<f64> { + a.try_div(b) + .ok_or_else(|| "cannot divide these two relative lengths".into()) +} + +/// Compute the logical "not" of a value. +pub fn not(value: Value) -> StrResult<Value> { + match value { + Bool(b) => Ok(Bool(!b)), + v => mismatch!("cannot apply 'not' to {}", v), + } +} + +/// Compute the logical "and" of two values. +pub fn and(lhs: Value, rhs: Value) -> StrResult<Value> { + match (lhs, rhs) { + (Bool(a), Bool(b)) => Ok(Bool(a && b)), + (a, b) => mismatch!("cannot apply 'and' to {} and {}", a, b), + } +} + +/// Compute the logical "or" of two values. +pub fn or(lhs: Value, rhs: Value) -> StrResult<Value> { + match (lhs, rhs) { + (Bool(a), Bool(b)) => Ok(Bool(a || b)), + (a, b) => mismatch!("cannot apply 'or' to {} and {}", a, b), + } +} + +/// Compute whether two values are equal. +pub fn eq(lhs: Value, rhs: Value) -> StrResult<Value> { + Ok(Bool(equal(&lhs, &rhs))) +} + +/// Compute whether two values are unequal. +pub fn neq(lhs: Value, rhs: Value) -> StrResult<Value> { + Ok(Bool(!equal(&lhs, &rhs))) +} + +macro_rules! comparison { + ($name:ident, $op:tt, $($pat:tt)*) => { + /// Compute how a value compares with another value. + pub fn $name(lhs: Value, rhs: Value) -> StrResult<Value> { + let ordering = compare(&lhs, &rhs)?; + Ok(Bool(matches!(ordering, $($pat)*))) + } + }; +} + +comparison!(lt, "<", Ordering::Less); +comparison!(leq, "<=", Ordering::Less | Ordering::Equal); +comparison!(gt, ">", Ordering::Greater); +comparison!(geq, ">=", Ordering::Greater | Ordering::Equal); + +/// Determine whether two values are equal. +pub fn equal(lhs: &Value, rhs: &Value) -> bool { + match (lhs, rhs) { + // Compare reflexively. + (None, None) => true, + (Auto, Auto) => true, + (Bool(a), Bool(b)) => a == b, + (Int(a), Int(b)) => a == b, + (Float(a), Float(b)) => a == b, + (Length(a), Length(b)) => a == b, + (Angle(a), Angle(b)) => a == b, + (Ratio(a), Ratio(b)) => a == b, + (Relative(a), Relative(b)) => a == b, + (Fraction(a), Fraction(b)) => a == b, + (Color(a), Color(b)) => a == b, + (Symbol(a), Symbol(b)) => a == b, + (Str(a), Str(b)) => a == b, + (Label(a), Label(b)) => a == b, + (Content(a), Content(b)) => a == b, + (Array(a), Array(b)) => a == b, + (Dict(a), Dict(b)) => a == b, + (Func(a), Func(b)) => a == b, + (Args(a), Args(b)) => a == b, + (Module(a), Module(b)) => a == b, + (Dyn(a), Dyn(b)) => a == b, + + // Some technically different things should compare equal. + (&Int(a), &Float(b)) => a as f64 == b, + (&Float(a), &Int(b)) => a == b as f64, + (&Length(a), &Relative(b)) => a == b.abs && b.rel.is_zero(), + (&Ratio(a), &Relative(b)) => a == b.rel && b.abs.is_zero(), + (&Relative(a), &Length(b)) => a.abs == b && a.rel.is_zero(), + (&Relative(a), &Ratio(b)) => a.rel == b && a.abs.is_zero(), + + _ => false, + } +} + +/// Compare two values. +pub fn compare(lhs: &Value, rhs: &Value) -> StrResult<Ordering> { + Ok(match (lhs, rhs) { + (Bool(a), Bool(b)) => a.cmp(b), + (Int(a), Int(b)) => a.cmp(b), + (Float(a), Float(b)) => try_cmp_values(a, b)?, + (Length(a), Length(b)) => try_cmp_values(a, b)?, + (Angle(a), Angle(b)) => a.cmp(b), + (Ratio(a), Ratio(b)) => a.cmp(b), + (Relative(a), Relative(b)) => try_cmp_values(a, b)?, + (Fraction(a), Fraction(b)) => a.cmp(b), + (Str(a), Str(b)) => a.cmp(b), + + // Some technically different things should be comparable. + (Int(a), Float(b)) => try_cmp_values(&(*a as f64), b)?, + (Float(a), Int(b)) => try_cmp_values(a, &(*b as f64))?, + (Length(a), Relative(b)) if b.rel.is_zero() => try_cmp_values(a, &b.abs)?, + (Ratio(a), Relative(b)) if b.abs.is_zero() => a.cmp(&b.rel), + (Relative(a), Length(b)) if a.rel.is_zero() => try_cmp_values(&a.abs, b)?, + (Relative(a), Ratio(b)) if a.abs.is_zero() => a.rel.cmp(b), + + _ => mismatch!("cannot compare {} and {}", lhs, rhs), + }) +} + +/// Try to compare two values. +fn try_cmp_values<T: PartialOrd + Debug>(a: &T, b: &T) -> StrResult<Ordering> { + a.partial_cmp(b) + .ok_or_else(|| eco_format!("cannot compare {:?} with {:?}", a, b)) +} + +/// Test whether one value is "in" another one. +pub fn in_(lhs: Value, rhs: Value) -> StrResult<Value> { + if let Some(b) = contains(&lhs, &rhs) { + Ok(Bool(b)) + } else { + mismatch!("cannot apply 'in' to {} and {}", lhs, rhs) + } +} + +/// Test whether one value is "not in" another one. +pub fn not_in(lhs: Value, rhs: Value) -> StrResult<Value> { + if let Some(b) = contains(&lhs, &rhs) { + Ok(Bool(!b)) + } else { + mismatch!("cannot apply 'not in' to {} and {}", lhs, rhs) + } +} + +/// Test for containment. +pub fn contains(lhs: &Value, rhs: &Value) -> Option<bool> { + match (lhs, rhs) { + (Str(a), Str(b)) => Some(b.as_str().contains(a.as_str())), + (Dyn(a), Str(b)) => a.downcast::<Regex>().map(|regex| regex.is_match(b)), + (Str(a), Dict(b)) => Some(b.contains(a)), + (a, Array(b)) => Some(b.contains(a)), + _ => Option::None, + } +} diff --git a/crates/typst/src/eval/scope.rs b/crates/typst/src/eval/scope.rs new file mode 100644 index 00000000..f3e13715 --- /dev/null +++ b/crates/typst/src/eval/scope.rs @@ -0,0 +1,178 @@ +use std::collections::BTreeMap; +use std::fmt::{self, Debug, Formatter}; +use std::hash::Hash; + +use ecow::{eco_format, EcoString}; + +use super::{IntoValue, Library, Value}; +use crate::diag::{bail, StrResult}; + +/// A stack of scopes. +#[derive(Debug, Default, Clone)] +pub struct Scopes<'a> { + /// The active scope. + pub top: Scope, + /// The stack of lower scopes. + pub scopes: Vec<Scope>, + /// The standard library. + pub base: Option<&'a Library>, +} + +impl<'a> Scopes<'a> { + /// Create a new, empty hierarchy of scopes. + pub fn new(base: Option<&'a Library>) -> Self { + Self { top: Scope::new(), scopes: vec![], base } + } + + /// Enter a new scope. + pub fn enter(&mut self) { + self.scopes.push(std::mem::take(&mut self.top)); + } + + /// Exit the topmost scope. + /// + /// This panics if no scope was entered. + pub fn exit(&mut self) { + self.top = self.scopes.pop().expect("no pushed scope"); + } + + /// Try to access a variable immutably. + pub fn get(&self, var: &str) -> StrResult<&Value> { + std::iter::once(&self.top) + .chain(self.scopes.iter().rev()) + .chain(self.base.map(|base| base.global.scope())) + .find_map(|scope| scope.get(var)) + .ok_or_else(|| unknown_variable(var)) + } + + /// Try to access a variable immutably in math. + pub fn get_in_math(&self, var: &str) -> StrResult<&Value> { + std::iter::once(&self.top) + .chain(self.scopes.iter().rev()) + .chain(self.base.map(|base| base.math.scope())) + .find_map(|scope| scope.get(var)) + .ok_or_else(|| eco_format!("unknown variable: {}", var)) + } + + /// Try to access a variable mutably. + pub fn get_mut(&mut self, var: &str) -> StrResult<&mut Value> { + std::iter::once(&mut self.top) + .chain(&mut self.scopes.iter_mut().rev()) + .find_map(|scope| scope.get_mut(var)) + .ok_or_else(|| { + match self.base.and_then(|base| base.global.scope().get(var)) { + Some(_) => eco_format!("cannot mutate a constant: {}", var), + _ => unknown_variable(var), + } + })? + } +} + +/// The error message when a variable is not found. +#[cold] +fn unknown_variable(var: &str) -> EcoString { + if var.contains('-') { + eco_format!("unknown variable: {} - if you meant to use subtraction, try adding spaces around the minus sign.", var) + } else { + eco_format!("unknown variable: {}", var) + } +} + +/// A map from binding names to values. +#[derive(Default, Clone, Hash)] +pub struct Scope(BTreeMap<EcoString, Slot>, bool); + +impl Scope { + /// Create a new empty scope. + pub fn new() -> Self { + Self(BTreeMap::new(), false) + } + + /// Create a new scope with duplication prevention. + pub fn deduplicating() -> Self { + Self(BTreeMap::new(), true) + } + + /// Bind a value to a name. + #[track_caller] + pub fn define(&mut self, name: impl Into<EcoString>, value: impl IntoValue) { + let name = name.into(); + + #[cfg(debug_assertions)] + if self.1 && self.0.contains_key(&name) { + panic!("duplicate definition: {name}"); + } + + self.0.insert(name, Slot::new(value.into_value(), Kind::Normal)); + } + + /// Define a captured, immutable binding. + pub fn define_captured(&mut self, var: impl Into<EcoString>, value: impl IntoValue) { + self.0 + .insert(var.into(), Slot::new(value.into_value(), Kind::Captured)); + } + + /// Try to access a variable immutably. + pub fn get(&self, var: &str) -> Option<&Value> { + self.0.get(var).map(Slot::read) + } + + /// Try to access a variable mutably. + pub fn get_mut(&mut self, var: &str) -> Option<StrResult<&mut Value>> { + self.0.get_mut(var).map(Slot::write) + } + + /// Iterate over all definitions. + pub fn iter(&self) -> impl Iterator<Item = (&EcoString, &Value)> { + self.0.iter().map(|(k, v)| (k, v.read())) + } +} + +impl Debug for Scope { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Scope ")?; + f.debug_map() + .entries(self.0.iter().map(|(k, v)| (k, v.read()))) + .finish() + } +} + +/// A slot where a value is stored. +#[derive(Clone, Hash)] +struct Slot { + /// The stored value. + value: Value, + /// The kind of slot, determines how the value can be accessed. + kind: Kind, +} + +/// The different kinds of slots. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +enum Kind { + /// A normal, mutable binding. + Normal, + /// A captured copy of another variable. + Captured, +} + +impl Slot { + /// Create a new slot. + fn new(value: Value, kind: Kind) -> Self { + Self { value, kind } + } + + /// Read the value. + fn read(&self) -> &Value { + &self.value + } + + /// Try to write to the value. + fn write(&mut self) -> StrResult<&mut Value> { + match self.kind { + Kind::Normal => Ok(&mut self.value), + Kind::Captured => { + bail!("variables from outside the function are read-only and cannot be modified") + } + } + } +} diff --git a/crates/typst/src/eval/str.rs b/crates/typst/src/eval/str.rs new file mode 100644 index 00000000..f5e5ab00 --- /dev/null +++ b/crates/typst/src/eval/str.rs @@ -0,0 +1,620 @@ +use std::borrow::{Borrow, Cow}; +use std::fmt::{self, Debug, Display, Formatter, Write}; +use std::hash::{Hash, Hasher}; +use std::ops::{Add, AddAssign, Deref, Range}; + +use ecow::EcoString; +use unicode_segmentation::UnicodeSegmentation; + +use super::{cast, dict, Args, Array, Dict, Func, IntoValue, Value, Vm}; +use crate::diag::{bail, At, SourceResult, StrResult}; +use crate::geom::GenAlign; + +/// Create a new [`Str`] from a format string. +#[macro_export] +#[doc(hidden)] +macro_rules! __format_str { + ($($tts:tt)*) => {{ + $crate::eval::Str::from($crate::eval::eco_format!($($tts)*)) + }}; +} + +#[doc(inline)] +pub use crate::__format_str as format_str; +#[doc(hidden)] +pub use ecow::eco_format; + +/// An immutable reference counted string. +#[derive(Default, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Str(EcoString); + +impl Str { + /// Create a new, empty string. + pub fn new() -> Self { + Self(EcoString::new()) + } + + /// Return `true` if the length is 0. + pub fn is_empty(&self) -> bool { + self.0.len() == 0 + } + + /// The length of the string in bytes. + pub fn len(&self) -> usize { + self.0.len() + } + + /// A string slice containing the entire string. + pub fn as_str(&self) -> &str { + self + } + + /// Extract the first grapheme cluster. + pub fn first(&self) -> StrResult<Self> { + self.0 + .graphemes(true) + .next() + .map(Into::into) + .ok_or_else(string_is_empty) + } + + /// Extract the last grapheme cluster. + pub fn last(&self) -> StrResult<Self> { + self.0 + .graphemes(true) + .next_back() + .map(Into::into) + .ok_or_else(string_is_empty) + } + + /// Extract the grapheme cluster at the given index. + pub fn at<'a>(&'a self, index: i64, default: Option<&'a str>) -> StrResult<Self> { + let len = self.len(); + let grapheme = self + .locate_opt(index)? + .and_then(|i| self.0[i..].graphemes(true).next()) + .or(default) + .ok_or_else(|| no_default_and_out_of_bounds(index, len))?; + Ok(grapheme.into()) + } + + /// Extract a contiguous substring. + pub fn slice(&self, start: i64, end: Option<i64>) -> StrResult<Self> { + let start = self.locate(start)?; + let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start); + Ok(self.0[start..end].into()) + } + + /// The grapheme clusters the string consists of. + pub fn clusters(&self) -> Array { + self.as_str().graphemes(true).map(|s| Value::Str(s.into())).collect() + } + + /// The codepoints the string consists of. + pub fn codepoints(&self) -> Array { + self.chars().map(|c| Value::Str(c.into())).collect() + } + + /// Whether the given pattern exists in this string. + pub fn contains(&self, pattern: StrPattern) -> bool { + match pattern { + StrPattern::Str(pat) => self.0.contains(pat.as_str()), + StrPattern::Regex(re) => re.is_match(self), + } + } + + /// Whether this string begins with the given pattern. + pub fn starts_with(&self, pattern: StrPattern) -> bool { + match pattern { + StrPattern::Str(pat) => self.0.starts_with(pat.as_str()), + StrPattern::Regex(re) => re.find(self).map_or(false, |m| m.start() == 0), + } + } + + /// Whether this string ends with the given pattern. + pub fn ends_with(&self, pattern: StrPattern) -> bool { + match pattern { + StrPattern::Str(pat) => self.0.ends_with(pat.as_str()), + StrPattern::Regex(re) => { + re.find_iter(self).last().map_or(false, |m| m.end() == self.0.len()) + } + } + } + + /// The text of the pattern's first match in this string. + pub fn find(&self, pattern: StrPattern) -> Option<Self> { + match pattern { + StrPattern::Str(pat) => self.0.contains(pat.as_str()).then_some(pat), + StrPattern::Regex(re) => re.find(self).map(|m| m.as_str().into()), + } + } + + /// The position of the pattern's first match in this string. + pub fn position(&self, pattern: StrPattern) -> Option<i64> { + match pattern { + StrPattern::Str(pat) => self.0.find(pat.as_str()).map(|i| i as i64), + StrPattern::Regex(re) => re.find(self).map(|m| m.start() as i64), + } + } + + /// The start and, text and capture groups (if any) of the first match of + /// the pattern in this string. + pub fn match_(&self, pattern: StrPattern) -> Option<Dict> { + match pattern { + StrPattern::Str(pat) => { + self.0.match_indices(pat.as_str()).next().map(match_to_dict) + } + StrPattern::Regex(re) => re.captures(self).map(captures_to_dict), + } + } + + /// The start, end, text and capture groups (if any) of all matches of the + /// pattern in this string. + pub fn matches(&self, pattern: StrPattern) -> Array { + match pattern { + StrPattern::Str(pat) => self + .0 + .match_indices(pat.as_str()) + .map(match_to_dict) + .map(Value::Dict) + .collect(), + StrPattern::Regex(re) => re + .captures_iter(self) + .map(captures_to_dict) + .map(Value::Dict) + .collect(), + } + } + + /// Split this string at whitespace or a specific pattern. + pub fn split(&self, pattern: Option<StrPattern>) -> Array { + let s = self.as_str(); + match pattern { + None => s.split_whitespace().map(|v| Value::Str(v.into())).collect(), + Some(StrPattern::Str(pat)) => { + s.split(pat.as_str()).map(|v| Value::Str(v.into())).collect() + } + Some(StrPattern::Regex(re)) => { + re.split(s).map(|v| Value::Str(v.into())).collect() + } + } + } + + /// Trim either whitespace or the given pattern at both or just one side of + /// the string. If `repeat` is true, the pattern is trimmed repeatedly + /// instead of just once. Repeat must only be given in combination with a + /// pattern. + pub fn trim( + &self, + pattern: Option<StrPattern>, + at: Option<StrSide>, + repeat: bool, + ) -> Self { + let mut start = matches!(at, Some(StrSide::Start) | None); + let end = matches!(at, Some(StrSide::End) | None); + + let trimmed = match pattern { + None => match at { + None => self.0.trim(), + Some(StrSide::Start) => self.0.trim_start(), + Some(StrSide::End) => self.0.trim_end(), + }, + Some(StrPattern::Str(pat)) => { + let pat = pat.as_str(); + let mut s = self.as_str(); + if repeat { + if start { + s = s.trim_start_matches(pat); + } + if end { + s = s.trim_end_matches(pat); + } + } else { + if start { + s = s.strip_prefix(pat).unwrap_or(s); + } + if end { + s = s.strip_suffix(pat).unwrap_or(s); + } + } + s + } + Some(StrPattern::Regex(re)) => { + let s = self.as_str(); + let mut last = 0; + let mut range = 0..s.len(); + + for m in re.find_iter(s) { + // Does this match follow directly after the last one? + let consecutive = last == m.start(); + + // As long as we're consecutive and still trimming at the + // start, trim. + start &= consecutive; + if start { + range.start = m.end(); + start &= repeat; + } + + // Reset end trim if we aren't consecutive anymore or aren't + // repeating. + if end && (!consecutive || !repeat) { + range.end = m.start(); + } + + last = m.end(); + } + + // Is the last match directly at the end? + if last < s.len() { + range.end = s.len(); + } + + &s[range.start..range.start.max(range.end)] + } + }; + + trimmed.into() + } + + /// Replace at most `count` occurrences of the given pattern with a + /// replacement string or function (beginning from the start). If no count + /// is given, all occurrences are replaced. + pub fn replace( + &self, + vm: &mut Vm, + pattern: StrPattern, + with: Replacement, + count: Option<usize>, + ) -> SourceResult<Self> { + // Heuristic: Assume the new string is about the same length as + // the current string. + let mut output = EcoString::with_capacity(self.as_str().len()); + + // Replace one match of a pattern with the replacement. + let mut last_match = 0; + let mut handle_match = |range: Range<usize>, dict: Dict| -> SourceResult<()> { + // Push everything until the match. + output.push_str(&self[last_match..range.start]); + last_match = range.end; + + // Determine and push the replacement. + match &with { + Replacement::Str(s) => output.push_str(s), + Replacement::Func(func) => { + let args = Args::new(func.span(), [dict]); + let piece = func.call_vm(vm, args)?.cast::<Str>().at(func.span())?; + output.push_str(&piece); + } + } + + Ok(()) + }; + + // Iterate over the matches of the `pattern`. + let count = count.unwrap_or(usize::MAX); + match &pattern { + StrPattern::Str(pat) => { + for m in self.match_indices(pat.as_str()).take(count) { + let (start, text) = m; + handle_match(start..start + text.len(), match_to_dict(m))?; + } + } + StrPattern::Regex(re) => { + for caps in re.captures_iter(self).take(count) { + // Extract the entire match over all capture groups. + let m = caps.get(0).unwrap(); + handle_match(m.start()..m.end(), captures_to_dict(caps))?; + } + } + } + + // Push the remainder. + output.push_str(&self[last_match..]); + Ok(output.into()) + } + + /// Repeat the string a number of times. + pub fn repeat(&self, n: i64) -> StrResult<Self> { + let n = usize::try_from(n) + .ok() + .and_then(|n| self.0.len().checked_mul(n).map(|_| n)) + .ok_or_else(|| format!("cannot repeat this string {} times", n))?; + + Ok(Self(self.0.repeat(n))) + } + + /// Resolve an index, if it is within bounds. + /// Errors on invalid char boundaries. + fn locate_opt(&self, index: i64) -> StrResult<Option<usize>> { + let wrapped = + if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) }; + + let resolved = wrapped + .and_then(|v| usize::try_from(v).ok()) + .filter(|&v| v <= self.0.len()); + + if resolved.map_or(false, |i| !self.0.is_char_boundary(i)) { + return Err(not_a_char_boundary(index)); + } + + Ok(resolved) + } + + /// Resolve an index or throw an out of bounds error. + fn locate(&self, index: i64) -> StrResult<usize> { + self.locate_opt(index)? + .ok_or_else(|| out_of_bounds(index, self.len())) + } +} + +/// The out of bounds access error message. +#[cold] +fn out_of_bounds(index: i64, len: usize) -> EcoString { + eco_format!("string index out of bounds (index: {}, len: {})", index, len) +} + +/// The out of bounds access error message when no default value was given. +#[cold] +fn no_default_and_out_of_bounds(index: i64, len: usize) -> EcoString { + eco_format!("no default value was specified and string index out of bounds (index: {}, len: {})", index, len) +} + +/// The char boundary access error message. +#[cold] +fn not_a_char_boundary(index: i64) -> EcoString { + eco_format!("string index {} is not a character boundary", index) +} + +/// The error message when the string is empty. +#[cold] +fn string_is_empty() -> EcoString { + "string is empty".into() +} + +/// Convert an item of std's `match_indices` to a dictionary. +fn match_to_dict((start, text): (usize, &str)) -> Dict { + dict! { + "start" => start, + "end" => start + text.len(), + "text" => text, + "captures" => Array::new(), + } +} + +/// Convert regex captures to a dictionary. +fn captures_to_dict(cap: regex::Captures) -> Dict { + let m = cap.get(0).expect("missing first match"); + dict! { + "start" => m.start(), + "end" => m.end(), + "text" => m.as_str(), + "captures" => cap.iter() + .skip(1) + .map(|opt| opt.map_or(Value::None, |m| m.as_str().into_value())) + .collect::<Array>(), + } +} + +impl Deref for Str { + type Target = str; + + fn deref(&self) -> &str { + &self.0 + } +} + +impl Display for Str { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(self) + } +} + +impl Debug for Str { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_char('"')?; + for c in self.chars() { + match c { + '\0' => f.write_str("\\u{0}")?, + '\'' => f.write_str("'")?, + '"' => f.write_str(r#"\""#)?, + _ => Display::fmt(&c.escape_debug(), f)?, + } + } + f.write_char('"') + } +} + +impl Add for Str { + type Output = Self; + + fn add(mut self, rhs: Self) -> Self::Output { + self += rhs; + self + } +} + +impl AddAssign for Str { + fn add_assign(&mut self, rhs: Self) { + self.0.push_str(rhs.as_str()); + } +} + +impl AsRef<str> for Str { + fn as_ref(&self) -> &str { + self + } +} + +impl Borrow<str> for Str { + fn borrow(&self) -> &str { + self + } +} + +impl From<char> for Str { + fn from(c: char) -> Self { + Self(c.into()) + } +} + +impl From<&str> for Str { + fn from(s: &str) -> Self { + Self(s.into()) + } +} + +impl From<EcoString> for Str { + fn from(s: EcoString) -> Self { + Self(s) + } +} + +impl From<String> for Str { + fn from(s: String) -> Self { + Self(s.into()) + } +} + +impl From<Cow<'_, str>> for Str { + fn from(s: Cow<str>) -> Self { + Self(s.into()) + } +} + +impl FromIterator<char> for Str { + fn from_iter<T: IntoIterator<Item = char>>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + +impl From<Str> for EcoString { + fn from(str: Str) -> Self { + str.0 + } +} + +impl From<Str> for String { + fn from(s: Str) -> Self { + s.0.into() + } +} + +cast! { + char, + self => Value::Str(self.into()), + string: Str => { + let mut chars = string.chars(); + match (chars.next(), chars.next()) { + (Some(c), None) => c, + _ => bail!("expected exactly one character"), + } + }, +} + +cast! { + &str, + self => Value::Str(self.into()), +} + +cast! { + EcoString, + self => Value::Str(self.into()), + v: Str => v.into(), +} + +cast! { + String, + self => Value::Str(self.into()), + v: Str => v.into(), +} + +/// A regular expression. +#[derive(Clone)] +pub struct Regex(regex::Regex); + +impl Regex { + /// Create a new regular expression. + pub fn new(re: &str) -> StrResult<Self> { + regex::Regex::new(re).map(Self).map_err(|err| eco_format!("{err}")) + } +} + +impl Deref for Regex { + type Target = regex::Regex; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Debug for Regex { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "regex({:?})", self.0.as_str()) + } +} + +impl PartialEq for Regex { + fn eq(&self, other: &Self) -> bool { + self.0.as_str() == other.0.as_str() + } +} + +impl Hash for Regex { + fn hash<H: Hasher>(&self, state: &mut H) { + self.0.as_str().hash(state); + } +} + +cast! { + type Regex: "regular expression", +} + +/// A pattern which can be searched for in a string. +#[derive(Debug, Clone)] +pub enum StrPattern { + /// Just a string. + Str(Str), + /// A regular expression. + Regex(Regex), +} + +cast! { + StrPattern, + text: Str => Self::Str(text), + regex: Regex => Self::Regex(regex), +} + +/// A side of a string. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub enum StrSide { + /// The logical start of the string, may be left or right depending on the + /// language. + Start, + /// The logical end of the string. + End, +} + +cast! { + StrSide, + align: GenAlign => match align { + GenAlign::Start => Self::Start, + GenAlign::End => Self::End, + _ => bail!("expected either `start` or `end`"), + }, +} + +/// A replacement for a matched [`Str`] +pub enum Replacement { + /// A string a match is replaced with. + Str(Str), + /// Function of type Dict -> Str (see `captures_to_dict` or `match_to_dict`) + /// whose output is inserted for the match. + Func(Func), +} + +cast! { + Replacement, + text: Str => Self::Str(text), + func: Func => Self::Func(func) +} diff --git a/crates/typst/src/eval/symbol.rs b/crates/typst/src/eval/symbol.rs new file mode 100644 index 00000000..0925202e --- /dev/null +++ b/crates/typst/src/eval/symbol.rs @@ -0,0 +1,210 @@ +use std::cmp::Reverse; +use std::collections::BTreeSet; +use std::fmt::{self, Debug, Display, Formatter, Write}; +use std::sync::Arc; + +use ecow::EcoString; + +use crate::diag::{bail, StrResult}; + +/// A symbol, possibly with variants. +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct Symbol(Repr); + +/// The internal representation. +#[derive(Clone, Eq, PartialEq, Hash)] +enum Repr { + Single(char), + Const(&'static [(&'static str, char)]), + Multi(Arc<(List, EcoString)>), +} + +/// A collection of symbols. +#[derive(Clone, Eq, PartialEq, Hash)] +enum List { + Static(&'static [(&'static str, char)]), + Runtime(Box<[(EcoString, char)]>), +} + +impl Symbol { + /// Create a new symbol from a single character. + pub const fn new(c: char) -> Self { + Self(Repr::Single(c)) + } + + /// Create a symbol with a static variant list. + #[track_caller] + pub const fn list(list: &'static [(&'static str, char)]) -> Self { + debug_assert!(!list.is_empty()); + Self(Repr::Const(list)) + } + + /// Create a symbol with a runtime variant list. + #[track_caller] + pub fn runtime(list: Box<[(EcoString, char)]>) -> Self { + debug_assert!(!list.is_empty()); + Self(Repr::Multi(Arc::new((List::Runtime(list), EcoString::new())))) + } + + /// Get the symbol's text. + pub fn get(&self) -> char { + match &self.0 { + Repr::Single(c) => *c, + Repr::Const(_) => find(self.variants(), "").unwrap(), + Repr::Multi(arc) => find(self.variants(), &arc.1).unwrap(), + } + } + + /// Apply a modifier to the symbol. + pub fn modified(mut self, modifier: &str) -> StrResult<Self> { + if let Repr::Const(list) = self.0 { + self.0 = Repr::Multi(Arc::new((List::Static(list), EcoString::new()))); + } + + if let Repr::Multi(arc) = &mut self.0 { + let (list, modifiers) = Arc::make_mut(arc); + if !modifiers.is_empty() { + modifiers.push('.'); + } + modifiers.push_str(modifier); + if find(list.variants(), modifiers).is_some() { + return Ok(self); + } + } + + bail!("unknown symbol modifier") + } + + /// The characters that are covered by this symbol. + pub fn variants(&self) -> impl Iterator<Item = (&str, char)> { + match &self.0 { + Repr::Single(c) => Variants::Single(Some(*c).into_iter()), + Repr::Const(list) => Variants::Static(list.iter()), + Repr::Multi(arc) => arc.0.variants(), + } + } + + /// Possible modifiers. + pub fn modifiers(&self) -> impl Iterator<Item = &str> + '_ { + let mut set = BTreeSet::new(); + let modifiers = match &self.0 { + Repr::Multi(arc) => arc.1.as_str(), + _ => "", + }; + for modifier in self.variants().flat_map(|(name, _)| name.split('.')) { + if !modifier.is_empty() && !contained(modifiers, modifier) { + set.insert(modifier); + } + } + set.into_iter() + } + + /// Normalize an accent to a combining one. + pub fn combining_accent(c: char) -> Option<char> { + Some(match c { + '\u{0300}' | '`' => '\u{0300}', + '\u{0301}' | '´' => '\u{0301}', + '\u{0302}' | '^' | 'ˆ' => '\u{0302}', + '\u{0303}' | '~' | '∼' | '˜' => '\u{0303}', + '\u{0304}' | '¯' => '\u{0304}', + '\u{0305}' | '-' | '‾' | '−' => '\u{0305}', + '\u{0306}' | '˘' => '\u{0306}', + '\u{0307}' | '.' | '˙' | '⋅' => '\u{0307}', + '\u{0308}' | '¨' => '\u{0308}', + '\u{20db}' => '\u{20db}', + '\u{20dc}' => '\u{20dc}', + '\u{030a}' | '∘' | '○' => '\u{030a}', + '\u{030b}' | '˝' => '\u{030b}', + '\u{030c}' | 'ˇ' => '\u{030c}', + '\u{20d6}' | '←' => '\u{20d6}', + '\u{20d7}' | '→' | '⟶' => '\u{20d7}', + _ => return None, + }) + } +} + +impl Debug for Symbol { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_char(self.get()) + } +} + +impl Display for Symbol { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_char(self.get()) + } +} + +impl List { + /// The characters that are covered by this list. + fn variants(&self) -> Variants<'_> { + match self { + List::Static(list) => Variants::Static(list.iter()), + List::Runtime(list) => Variants::Runtime(list.iter()), + } + } +} + +/// Iterator over variants. +enum Variants<'a> { + Single(std::option::IntoIter<char>), + Static(std::slice::Iter<'static, (&'static str, char)>), + Runtime(std::slice::Iter<'a, (EcoString, char)>), +} + +impl<'a> Iterator for Variants<'a> { + type Item = (&'a str, char); + + fn next(&mut self) -> Option<Self::Item> { + match self { + Self::Single(iter) => Some(("", iter.next()?)), + Self::Static(list) => list.next().copied(), + Self::Runtime(list) => list.next().map(|(s, c)| (s.as_str(), *c)), + } + } +} + +/// Find the best symbol from the list. +fn find<'a>( + variants: impl Iterator<Item = (&'a str, char)>, + modifiers: &str, +) -> Option<char> { + let mut best = None; + let mut best_score = None; + + // Find the best table entry with this name. + 'outer: for candidate in variants { + for modifier in parts(modifiers) { + if !contained(candidate.0, modifier) { + continue 'outer; + } + } + + let mut matching = 0; + let mut total = 0; + for modifier in parts(candidate.0) { + if contained(modifiers, modifier) { + matching += 1; + } + total += 1; + } + + let score = (matching, Reverse(total)); + if best_score.map_or(true, |b| score > b) { + best = Some(candidate.1); + best_score = Some(score); + } + } + + best +} + +/// Split a modifier list into its parts. +fn parts(modifiers: &str) -> impl Iterator<Item = &str> { + modifiers.split('.').filter(|s| !s.is_empty()) +} + +/// Whether the modifier string contains the modifier `m`. +fn contained(modifiers: &str, m: &str) -> bool { + parts(modifiers).any(|part| part == m) +} diff --git a/crates/typst/src/eval/value.rs b/crates/typst/src/eval/value.rs new file mode 100644 index 00000000..b1782cab --- /dev/null +++ b/crates/typst/src/eval/value.rs @@ -0,0 +1,461 @@ +use std::any::Any; +use std::cmp::Ordering; +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +use ecow::eco_format; +use siphasher::sip128::{Hasher128, SipHasher13}; + +use super::{ + cast, format_str, ops, Args, Array, CastInfo, Content, Dict, FromValue, Func, + IntoValue, Module, Reflect, Str, Symbol, +}; +use crate::diag::StrResult; +use crate::geom::{Abs, Angle, Color, Em, Fr, Length, Ratio, Rel}; +use crate::model::{Label, Styles}; +use crate::syntax::{ast, Span}; +use crate::util::Bytes; + +/// A computational value. +#[derive(Default, Clone)] +pub enum Value { + /// The value that indicates the absence of a meaningful value. + #[default] + None, + /// A value that indicates some smart default behaviour. + Auto, + /// A boolean: `true, false`. + Bool(bool), + /// An integer: `120`. + Int(i64), + /// A floating-point number: `1.2`, `10e-4`. + Float(f64), + /// A length: `12pt`, `3cm`, `1.5em`, `1em - 2pt`. + Length(Length), + /// An angle: `1.5rad`, `90deg`. + Angle(Angle), + /// A ratio: `50%`. + Ratio(Ratio), + /// A relative length, combination of a ratio and a length: `20% + 5cm`. + Relative(Rel<Length>), + /// A fraction: `1fr`. + Fraction(Fr), + /// A color value: `#f79143ff`. + Color(Color), + /// A symbol: `arrow.l`. + Symbol(Symbol), + /// A string: `"string"`. + Str(Str), + /// Raw bytes. + Bytes(Bytes), + /// A label: `<intro>`. + Label(Label), + /// A content value: `[*Hi* there]`. + Content(Content), + // Content styles. + Styles(Styles), + /// An array of values: `(1, "hi", 12cm)`. + Array(Array), + /// A dictionary value: `(a: 1, b: "hi")`. + Dict(Dict), + /// An executable function. + Func(Func), + /// Captured arguments to a function. + Args(Args), + /// A module. + Module(Module), + /// A dynamic value. + Dyn(Dynamic), +} + +impl Value { + /// Create a new dynamic value. + pub fn dynamic<T>(any: T) -> Self + where + T: Type + Debug + PartialEq + Hash + Sync + Send + 'static, + { + Self::Dyn(Dynamic::new(any)) + } + + /// Create a numeric value from a number with a unit. + pub fn numeric(pair: (f64, ast::Unit)) -> Self { + let (v, unit) = pair; + match unit { + ast::Unit::Length(unit) => Abs::with_unit(v, unit).into_value(), + ast::Unit::Angle(unit) => Angle::with_unit(v, unit).into_value(), + ast::Unit::Em => Em::new(v).into_value(), + ast::Unit::Fr => Fr::new(v).into_value(), + ast::Unit::Percent => Ratio::new(v / 100.0).into_value(), + } + } + + /// The name of the stored value's type. + pub fn type_name(&self) -> &'static str { + match self { + Self::None => "none", + Self::Auto => "auto", + Self::Bool(_) => bool::TYPE_NAME, + Self::Int(_) => i64::TYPE_NAME, + Self::Float(_) => f64::TYPE_NAME, + Self::Length(_) => Length::TYPE_NAME, + Self::Angle(_) => Angle::TYPE_NAME, + Self::Ratio(_) => Ratio::TYPE_NAME, + Self::Relative(_) => Rel::<Length>::TYPE_NAME, + Self::Fraction(_) => Fr::TYPE_NAME, + Self::Color(_) => Color::TYPE_NAME, + Self::Symbol(_) => Symbol::TYPE_NAME, + Self::Str(_) => Str::TYPE_NAME, + Self::Bytes(_) => Bytes::TYPE_NAME, + Self::Label(_) => Label::TYPE_NAME, + Self::Content(_) => Content::TYPE_NAME, + Self::Styles(_) => Styles::TYPE_NAME, + Self::Array(_) => Array::TYPE_NAME, + Self::Dict(_) => Dict::TYPE_NAME, + Self::Func(_) => Func::TYPE_NAME, + Self::Args(_) => Args::TYPE_NAME, + Self::Module(_) => Module::TYPE_NAME, + Self::Dyn(v) => v.type_name(), + } + } + + /// Try to cast the value into a specific type. + pub fn cast<T: FromValue>(self) -> StrResult<T> { + T::from_value(self) + } + + /// Try to access a field on the value. + pub fn field(&self, field: &str) -> StrResult<Value> { + match self { + Self::Symbol(symbol) => symbol.clone().modified(field).map(Self::Symbol), + Self::Dict(dict) => dict.at(field, None).cloned(), + Self::Content(content) => content.at(field, None), + Self::Module(module) => module.get(field).cloned(), + Self::Func(func) => func.get(field).cloned(), + v => Err(eco_format!("cannot access fields on type {}", v.type_name())), + } + } + + /// Return the debug representation of the value. + pub fn repr(&self) -> Str { + format_str!("{:?}", self) + } + + /// Attach a span to the value, if possible. + pub fn spanned(self, span: Span) -> Self { + match self { + Value::Content(v) => Value::Content(v.spanned(span)), + Value::Func(v) => Value::Func(v.spanned(span)), + v => v, + } + } + + /// Return the display representation of the value. + pub fn display(self) -> Content { + match self { + Self::None => Content::empty(), + Self::Int(v) => item!(text)(eco_format!("{}", v)), + Self::Float(v) => item!(text)(eco_format!("{}", v)), + Self::Str(v) => item!(text)(v.into()), + Self::Symbol(v) => item!(text)(v.get().into()), + Self::Content(v) => v, + Self::Func(_) => Content::empty(), + Self::Module(module) => module.content(), + _ => item!(raw)(self.repr().into(), Some("typc".into()), false), + } + } + + /// Try to extract documentation for the value. + pub fn docs(&self) -> Option<&'static str> { + match self { + Self::Func(func) => func.info().map(|info| info.docs), + _ => None, + } + } +} + +impl Debug for Value { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::None => f.pad("none"), + Self::Auto => f.pad("auto"), + Self::Bool(v) => Debug::fmt(v, f), + Self::Int(v) => Debug::fmt(v, f), + Self::Float(v) => Debug::fmt(v, f), + Self::Length(v) => Debug::fmt(v, f), + Self::Angle(v) => Debug::fmt(v, f), + Self::Ratio(v) => Debug::fmt(v, f), + Self::Relative(v) => Debug::fmt(v, f), + Self::Fraction(v) => Debug::fmt(v, f), + Self::Color(v) => Debug::fmt(v, f), + Self::Symbol(v) => Debug::fmt(v, f), + Self::Str(v) => Debug::fmt(v, f), + Self::Bytes(v) => Debug::fmt(v, f), + Self::Label(v) => Debug::fmt(v, f), + Self::Content(v) => Debug::fmt(v, f), + Self::Styles(v) => Debug::fmt(v, f), + Self::Array(v) => Debug::fmt(v, f), + Self::Dict(v) => Debug::fmt(v, f), + Self::Func(v) => Debug::fmt(v, f), + Self::Args(v) => Debug::fmt(v, f), + Self::Module(v) => Debug::fmt(v, f), + Self::Dyn(v) => Debug::fmt(v, f), + } + } +} + +impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + ops::equal(self, other) + } +} + +impl PartialOrd for Value { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + ops::compare(self, other).ok() + } +} + +impl Hash for Value { + fn hash<H: Hasher>(&self, state: &mut H) { + std::mem::discriminant(self).hash(state); + match self { + Self::None => {} + Self::Auto => {} + Self::Bool(v) => v.hash(state), + Self::Int(v) => v.hash(state), + Self::Float(v) => v.to_bits().hash(state), + Self::Length(v) => v.hash(state), + Self::Angle(v) => v.hash(state), + Self::Ratio(v) => v.hash(state), + Self::Relative(v) => v.hash(state), + Self::Fraction(v) => v.hash(state), + Self::Color(v) => v.hash(state), + Self::Symbol(v) => v.hash(state), + Self::Str(v) => v.hash(state), + Self::Bytes(v) => v.hash(state), + Self::Label(v) => v.hash(state), + Self::Content(v) => v.hash(state), + Self::Styles(v) => v.hash(state), + Self::Array(v) => v.hash(state), + Self::Dict(v) => v.hash(state), + Self::Func(v) => v.hash(state), + Self::Args(v) => v.hash(state), + Self::Module(v) => v.hash(state), + Self::Dyn(v) => v.hash(state), + } + } +} + +/// A dynamic value. +#[derive(Clone, Hash)] +#[allow(clippy::derived_hash_with_manual_eq)] +pub struct Dynamic(Arc<dyn Bounds>); + +impl Dynamic { + /// Create a new instance from any value that satisfies the required bounds. + pub fn new<T>(any: T) -> Self + where + T: Type + Debug + PartialEq + Hash + Sync + Send + 'static, + { + Self(Arc::new(any)) + } + + /// Whether the wrapped type is `T`. + pub fn is<T: Type + 'static>(&self) -> bool { + (*self.0).as_any().is::<T>() + } + + /// Try to downcast to a reference to a specific type. + pub fn downcast<T: Type + 'static>(&self) -> Option<&T> { + (*self.0).as_any().downcast_ref() + } + + /// The name of the stored value's type. + pub fn type_name(&self) -> &'static str { + self.0.dyn_type_name() + } +} + +impl Debug for Dynamic { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Debug::fmt(&self.0, f) + } +} + +impl PartialEq for Dynamic { + fn eq(&self, other: &Self) -> bool { + self.0.dyn_eq(other) + } +} + +cast! { + Dynamic, + self => Value::Dyn(self), +} + +trait Bounds: Debug + Sync + Send + 'static { + fn as_any(&self) -> &dyn Any; + fn dyn_eq(&self, other: &Dynamic) -> bool; + fn dyn_type_name(&self) -> &'static str; + fn hash128(&self) -> u128; +} + +impl<T> Bounds for T +where + T: Type + Debug + PartialEq + Hash + Sync + Send + 'static, +{ + fn as_any(&self) -> &dyn Any { + self + } + + fn dyn_eq(&self, other: &Dynamic) -> bool { + let Some(other) = other.downcast::<Self>() else { return false }; + self == other + } + + fn dyn_type_name(&self) -> &'static str { + T::TYPE_NAME + } + + #[tracing::instrument(skip_all)] + fn hash128(&self) -> u128 { + // Also hash the TypeId since values with different types but + // equal data should be different. + let mut state = SipHasher13::new(); + self.type_id().hash(&mut state); + self.hash(&mut state); + state.finish128().as_u128() + } +} + +impl Hash for dyn Bounds { + fn hash<H: Hasher>(&self, state: &mut H) { + state.write_u128(self.hash128()); + } +} + +/// The type of a value. +pub trait Type { + /// The name of the type. + const TYPE_NAME: &'static str; +} + +/// Implement traits for primitives. +macro_rules! primitive { + ( + $ty:ty: $name:literal, $variant:ident + $(, $other:ident$(($binding:ident))? => $out:expr)* + ) => { + impl Type for $ty { + const TYPE_NAME: &'static str = $name; + } + + impl Reflect for $ty { + fn describe() -> CastInfo { + CastInfo::Type(Self::TYPE_NAME) + } + + fn castable(value: &Value) -> bool { + matches!(value, Value::$variant(_) + $(| primitive!(@$other $(($binding))?))*) + } + } + + impl IntoValue for $ty { + fn into_value(self) -> Value { + Value::$variant(self) + } + } + + impl FromValue for $ty { + fn from_value(value: Value) -> StrResult<Self> { + match value { + Value::$variant(v) => Ok(v), + $(Value::$other$(($binding))? => Ok($out),)* + v => Err(eco_format!( + "expected {}, found {}", + Self::TYPE_NAME, + v.type_name(), + )), + } + } + } + }; + + (@$other:ident($binding:ident)) => { Value::$other(_) }; + (@$other:ident) => { Value::$other }; +} + +primitive! { bool: "boolean", Bool } +primitive! { i64: "integer", Int } +primitive! { f64: "float", Float, Int(v) => v as f64 } +primitive! { Length: "length", Length } +primitive! { Angle: "angle", Angle } +primitive! { Ratio: "ratio", Ratio } +primitive! { Rel<Length>: "relative length", + Relative, + Length(v) => v.into(), + Ratio(v) => v.into() +} +primitive! { Fr: "fraction", Fraction } +primitive! { Color: "color", Color } +primitive! { Symbol: "symbol", Symbol } +primitive! { + Str: "string", + Str, + Symbol(symbol) => symbol.get().into() +} +primitive! { Bytes: "bytes", Bytes } +primitive! { Label: "label", Label } +primitive! { Content: "content", + Content, + None => Content::empty(), + Symbol(v) => item!(text)(v.get().into()), + Str(v) => item!(text)(v.into()) +} +primitive! { Styles: "styles", Styles } +primitive! { Array: "array", Array } +primitive! { Dict: "dictionary", Dict } +primitive! { Func: "function", Func } +primitive! { Args: "arguments", Args } +primitive! { Module: "module", Module } + +#[cfg(test)] +mod tests { + use super::*; + use crate::eval::{array, dict}; + use crate::geom::RgbaColor; + + #[track_caller] + fn test(value: impl IntoValue, exp: &str) { + assert_eq!(format!("{:?}", value.into_value()), exp); + } + + #[test] + fn test_value_debug() { + // Primitives. + test(Value::None, "none"); + test(false, "false"); + test(12i64, "12"); + test(3.24, "3.24"); + test(Abs::pt(5.5), "5.5pt"); + test(Angle::deg(90.0), "90deg"); + test(Ratio::one() / 2.0, "50%"); + test(Ratio::new(0.3) + Length::from(Abs::cm(2.0)), "30% + 56.69pt"); + test(Fr::one() * 7.55, "7.55fr"); + test(Color::Rgba(RgbaColor::new(1, 1, 1, 0xff)), "rgb(\"#010101\")"); + + // Collections. + test("hello", r#""hello""#); + test("\n", r#""\n""#); + test("\\", r#""\\""#); + test("\"", r#""\"""#); + test(array![], "()"); + test(array![Value::None], "(none,)"); + test(array![1, 2], "(1, 2)"); + test(dict![], "(:)"); + test(dict!["one" => 1], "(one: 1)"); + test(dict!["two" => false, "one" => 1], "(two: false, one: 1)"); + } +} diff --git a/crates/typst/src/export/mod.rs b/crates/typst/src/export/mod.rs new file mode 100644 index 00000000..eb0731a9 --- /dev/null +++ b/crates/typst/src/export/mod.rs @@ -0,0 +1,7 @@ +//! Exporting into external formats. + +mod pdf; +mod render; + +pub use self::pdf::pdf; +pub use self::render::render; diff --git a/crates/typst/src/export/pdf/font.rs b/crates/typst/src/export/pdf/font.rs new file mode 100644 index 00000000..f0676d8f --- /dev/null +++ b/crates/typst/src/export/pdf/font.rs @@ -0,0 +1,204 @@ +use std::collections::BTreeMap; + +use ecow::{eco_format, EcoString}; +use pdf_writer::types::{CidFontType, FontFlags, SystemInfo, UnicodeCmap}; +use pdf_writer::{Filter, Finish, Name, Rect, Str}; +use ttf_parser::{name_id, GlyphId, Tag}; +use unicode_general_category::GeneralCategory; + +use super::{deflate, EmExt, PdfContext, RefExt}; +use crate::font::Font; +use crate::util::{Bytes, SliceExt}; + +const CMAP_NAME: Name = Name(b"Custom"); +const SYSTEM_INFO: SystemInfo = SystemInfo { + registry: Str(b"Adobe"), + ordering: Str(b"Identity"), + supplement: 0, +}; + +/// Embed all used fonts into the PDF. +#[tracing::instrument(skip_all)] +pub fn write_fonts(ctx: &mut PdfContext) { + for font in ctx.font_map.items() { + let type0_ref = ctx.alloc.bump(); + let cid_ref = ctx.alloc.bump(); + let descriptor_ref = ctx.alloc.bump(); + let cmap_ref = ctx.alloc.bump(); + let data_ref = ctx.alloc.bump(); + ctx.font_refs.push(type0_ref); + + let glyph_set = ctx.glyph_sets.get_mut(font).unwrap(); + let metrics = font.metrics(); + let ttf = font.ttf(); + + let postscript_name = font + .find_name(name_id::POST_SCRIPT_NAME) + .unwrap_or_else(|| "unknown".to_string()); + + let base_font = eco_format!("ABCDEF+{}", postscript_name); + let base_font = Name(base_font.as_bytes()); + + // Write the base font object referencing the CID font. + ctx.writer + .type0_font(type0_ref) + .base_font(base_font) + .encoding_predefined(Name(b"Identity-H")) + .descendant_font(cid_ref) + .to_unicode(cmap_ref); + + // Check for the presence of CFF outlines to select the correct + // CID-Font subtype. + let subtype = match ttf + .raw_face() + .table(Tag::from_bytes(b"CFF ")) + .or(ttf.raw_face().table(Tag::from_bytes(b"CFF2"))) + { + Some(_) => CidFontType::Type0, + None => CidFontType::Type2, + }; + + // Write the CID font referencing the font descriptor. + let mut cid = ctx.writer.cid_font(cid_ref); + cid.subtype(subtype); + cid.base_font(base_font); + cid.system_info(SYSTEM_INFO); + cid.font_descriptor(descriptor_ref); + cid.default_width(0.0); + + if subtype == CidFontType::Type2 { + cid.cid_to_gid_map_predefined(Name(b"Identity")); + } + + // Extract the widths of all glyphs. + let num_glyphs = ttf.number_of_glyphs(); + let mut widths = vec![0.0; num_glyphs as usize]; + for &g in glyph_set.keys() { + let x = ttf.glyph_hor_advance(GlyphId(g)).unwrap_or(0); + widths[g as usize] = font.to_em(x).to_font_units(); + } + + // Write all non-zero glyph widths. + let mut first = 0; + let mut width_writer = cid.widths(); + for (w, group) in widths.group_by_key(|&w| w) { + let end = first + group.len(); + if w != 0.0 { + let last = end - 1; + width_writer.same(first as u16, last as u16, w); + } + first = end; + } + + width_writer.finish(); + cid.finish(); + + let mut flags = FontFlags::empty(); + flags.set(FontFlags::SERIF, postscript_name.contains("Serif")); + flags.set(FontFlags::FIXED_PITCH, ttf.is_monospaced()); + flags.set(FontFlags::ITALIC, ttf.is_italic()); + flags.insert(FontFlags::SYMBOLIC); + flags.insert(FontFlags::SMALL_CAP); + + let global_bbox = ttf.global_bounding_box(); + let bbox = Rect::new( + font.to_em(global_bbox.x_min).to_font_units(), + font.to_em(global_bbox.y_min).to_font_units(), + font.to_em(global_bbox.x_max).to_font_units(), + font.to_em(global_bbox.y_max).to_font_units(), + ); + + let italic_angle = ttf.italic_angle().unwrap_or(0.0); + let ascender = metrics.ascender.to_font_units(); + let descender = metrics.descender.to_font_units(); + let cap_height = metrics.cap_height.to_font_units(); + let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0); + + // Write the font descriptor (contains metrics about the font). + let mut font_descriptor = ctx.writer.font_descriptor(descriptor_ref); + font_descriptor + .name(base_font) + .flags(flags) + .bbox(bbox) + .italic_angle(italic_angle) + .ascent(ascender) + .descent(descender) + .cap_height(cap_height) + .stem_v(stem_v); + + match subtype { + CidFontType::Type0 => font_descriptor.font_file3(data_ref), + CidFontType::Type2 => font_descriptor.font_file2(data_ref), + }; + + font_descriptor.finish(); + + // Write the /ToUnicode character map, which maps glyph ids back to + // unicode codepoints to enable copying out of the PDF. + let cmap = create_cmap(ttf, glyph_set); + ctx.writer.cmap(cmap_ref, &cmap.finish()); + + // Subset and write the font's bytes. + let glyphs: Vec<_> = glyph_set.keys().copied().collect(); + let data = subset_font(font, &glyphs); + let mut stream = ctx.writer.stream(data_ref, &data); + stream.filter(Filter::FlateDecode); + + if subtype == CidFontType::Type0 { + stream.pair(Name(b"Subtype"), Name(b"CIDFontType0C")); + } + + stream.finish(); + } +} + +/// Subset a font to the given glyphs. +#[comemo::memoize] +fn subset_font(font: &Font, glyphs: &[u16]) -> Bytes { + let data = font.data(); + let profile = subsetter::Profile::pdf(glyphs); + let subsetted = subsetter::subset(data, font.index(), profile); + let data = subsetted.as_deref().unwrap_or(data); + deflate(data).into() +} + +/// Create a /ToUnicode CMap. +fn create_cmap( + ttf: &ttf_parser::Face, + glyph_set: &mut BTreeMap<u16, EcoString>, +) -> UnicodeCmap { + // For glyphs that have codepoints mapping to in the font's cmap table, we + // prefer them over pre-existing text mappings from the document. Only + // things that don't have a corresponding codepoint (or only a private-use + // one) like the "Th" in Linux Libertine get the text of their first + // occurrences in the document instead. + for subtable in ttf.tables().cmap.into_iter().flat_map(|table| table.subtables) { + if !subtable.is_unicode() { + continue; + } + + subtable.codepoints(|n| { + let Some(c) = std::char::from_u32(n) else { return }; + if unicode_general_category::get_general_category(c) + == GeneralCategory::PrivateUse + { + return; + } + + let Some(GlyphId(g)) = ttf.glyph_index(c) else { return }; + if glyph_set.contains_key(&g) { + glyph_set.insert(g, c.into()); + } + }); + } + + // Produce a reverse mapping from glyphs to unicode strings. + let mut cmap = UnicodeCmap::new(CMAP_NAME, SYSTEM_INFO); + for (&g, text) in glyph_set.iter() { + if !text.is_empty() { + cmap.pair_with_multiple(g, text.chars()); + } + } + + cmap +} diff --git a/crates/typst/src/export/pdf/image.rs b/crates/typst/src/export/pdf/image.rs new file mode 100644 index 00000000..48472d9f --- /dev/null +++ b/crates/typst/src/export/pdf/image.rs @@ -0,0 +1,143 @@ +use std::io::Cursor; + +use image::{DynamicImage, GenericImageView, Rgba}; +use pdf_writer::{Filter, Finish}; + +use super::{deflate, PdfContext, RefExt}; +use crate::image::{DecodedImage, Image, RasterFormat}; +use crate::util::Bytes; + +/// Embed all used images into the PDF. +#[tracing::instrument(skip_all)] +pub fn write_images(ctx: &mut PdfContext) { + for image in ctx.image_map.items() { + let image_ref = ctx.alloc.bump(); + let icc_ref = ctx.alloc.bump(); + ctx.image_refs.push(image_ref); + + let width = image.width(); + let height = image.height(); + + // Add the primary image. + // TODO: Error if image could not be encoded. + match image.decoded().as_ref() { + DecodedImage::Raster(dynamic, icc, _) => { + // TODO: Error if image could not be encoded. + let (data, filter, has_color) = encode_image(image); + let mut image = ctx.writer.image_xobject(image_ref, &data); + image.filter(filter); + image.width(width as i32); + image.height(height as i32); + image.bits_per_component(8); + + let space = image.color_space(); + if icc.is_some() { + space.icc_based(icc_ref); + } else if has_color { + space.device_rgb(); + } else { + space.device_gray(); + } + + // Add a second gray-scale image containing the alpha values if + // this image has an alpha channel. + if dynamic.color().has_alpha() { + let (alpha_data, alpha_filter) = encode_alpha(dynamic); + let mask_ref = ctx.alloc.bump(); + image.s_mask(mask_ref); + image.finish(); + + let mut mask = ctx.writer.image_xobject(mask_ref, &alpha_data); + mask.filter(alpha_filter); + mask.width(width as i32); + mask.height(height as i32); + mask.color_space().device_gray(); + mask.bits_per_component(8); + } else { + image.finish(); + } + + if let Some(icc) = icc { + let compressed = deflate(&icc.0); + let mut stream = ctx.writer.icc_profile(icc_ref, &compressed); + stream.filter(Filter::FlateDecode); + if has_color { + stream.n(3); + stream.alternate().srgb(); + } else { + stream.n(1); + stream.alternate().d65_gray(); + } + } + } + DecodedImage::Svg(svg) => { + let next_ref = svg2pdf::convert_tree_into( + svg, + svg2pdf::Options::default(), + &mut ctx.writer, + image_ref, + ); + ctx.alloc = next_ref; + } + } + } +} + +/// Encode an image with a suitable filter and return the data, filter and +/// whether the image has color. +/// +/// Skips the alpha channel as that's encoded separately. +#[comemo::memoize] +#[tracing::instrument(skip_all)] +fn encode_image(image: &Image) -> (Bytes, Filter, bool) { + let decoded = image.decoded(); + let (dynamic, format) = match decoded.as_ref() { + DecodedImage::Raster(dynamic, _, format) => (dynamic, *format), + _ => panic!("can only encode raster image"), + }; + + match (format, dynamic) { + // 8-bit gray JPEG. + (RasterFormat::Jpg, DynamicImage::ImageLuma8(_)) => { + let mut data = Cursor::new(vec![]); + dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap(); + (data.into_inner().into(), Filter::DctDecode, false) + } + + // 8-bit RGB JPEG (CMYK JPEGs get converted to RGB earlier). + (RasterFormat::Jpg, DynamicImage::ImageRgb8(_)) => { + let mut data = Cursor::new(vec![]); + dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap(); + (data.into_inner().into(), Filter::DctDecode, true) + } + + // TODO: Encode flate streams with PNG-predictor? + + // 8-bit gray PNG. + (RasterFormat::Png, DynamicImage::ImageLuma8(luma)) => { + let data = deflate(luma.as_raw()); + (data.into(), Filter::FlateDecode, false) + } + + // Anything else (including Rgb(a) PNGs). + (_, buf) => { + let (width, height) = buf.dimensions(); + let mut pixels = Vec::with_capacity(3 * width as usize * height as usize); + for (_, _, Rgba([r, g, b, _])) in buf.pixels() { + pixels.push(r); + pixels.push(g); + pixels.push(b); + } + + let data = deflate(&pixels); + (data.into(), Filter::FlateDecode, true) + } + } +} + +/// Encode an image's alpha channel if present. +#[tracing::instrument(skip_all)] +fn encode_alpha(dynamic: &DynamicImage) -> (Vec<u8>, Filter) { + let pixels: Vec<_> = dynamic.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect(); + (deflate(&pixels), Filter::FlateDecode) +} diff --git a/crates/typst/src/export/pdf/mod.rs b/crates/typst/src/export/pdf/mod.rs new file mode 100644 index 00000000..48485862 --- /dev/null +++ b/crates/typst/src/export/pdf/mod.rs @@ -0,0 +1,235 @@ +//! Exporting into PDF documents. + +mod font; +mod image; +mod outline; +mod page; + +use std::cmp::Eq; +use std::collections::{BTreeMap, HashMap}; +use std::hash::Hash; + +use ecow::EcoString; +use pdf_writer::types::Direction; +use pdf_writer::{Finish, Name, PdfWriter, Ref, TextStr}; +use xmp_writer::{LangId, RenditionClass, XmpWriter}; + +use self::page::Page; +use crate::doc::{Document, Lang}; +use crate::font::Font; +use crate::geom::{Abs, Dir, Em}; +use crate::image::Image; +use crate::model::Introspector; + +/// Export a document into a PDF file. +/// +/// Returns the raw bytes making up the PDF file. +#[tracing::instrument(skip_all)] +pub fn pdf(document: &Document) -> Vec<u8> { + let mut ctx = PdfContext::new(document); + page::construct_pages(&mut ctx, &document.pages); + font::write_fonts(&mut ctx); + image::write_images(&mut ctx); + page::write_page_tree(&mut ctx); + write_catalog(&mut ctx); + ctx.writer.finish() +} + +/// Identifies the color space definitions. +const SRGB: Name<'static> = Name(b"srgb"); +const D65_GRAY: Name<'static> = Name(b"d65gray"); + +/// Context for exporting a whole PDF document. +pub struct PdfContext<'a> { + document: &'a Document, + introspector: Introspector, + writer: PdfWriter, + pages: Vec<Page>, + page_heights: Vec<f32>, + alloc: Ref, + page_tree_ref: Ref, + font_refs: Vec<Ref>, + image_refs: Vec<Ref>, + page_refs: Vec<Ref>, + font_map: Remapper<Font>, + image_map: Remapper<Image>, + /// For each font a mapping from used glyphs to their text representation. + /// May contain multiple chars in case of ligatures or similar things. The + /// same glyph can have a different text representation within one document, + /// then we just save the first one. The resulting strings are used for the + /// PDF's /ToUnicode map for glyphs that don't have an entry in the font's + /// cmap. This is important for copy-paste and searching. + glyph_sets: HashMap<Font, BTreeMap<u16, EcoString>>, + languages: HashMap<Lang, usize>, +} + +impl<'a> PdfContext<'a> { + fn new(document: &'a Document) -> Self { + let mut alloc = Ref::new(1); + let page_tree_ref = alloc.bump(); + Self { + document, + introspector: Introspector::new(&document.pages), + writer: PdfWriter::new(), + pages: vec![], + page_heights: vec![], + alloc, + page_tree_ref, + page_refs: vec![], + font_refs: vec![], + image_refs: vec![], + font_map: Remapper::new(), + image_map: Remapper::new(), + glyph_sets: HashMap::new(), + languages: HashMap::new(), + } + } +} + +/// Write the document catalog. +#[tracing::instrument(skip_all)] +fn write_catalog(ctx: &mut PdfContext) { + let lang = ctx + .languages + .iter() + .max_by_key(|(&lang, &count)| (count, lang)) + .map(|(&k, _)| k); + + let dir = if lang.map(Lang::dir) == Some(Dir::RTL) { + Direction::R2L + } else { + Direction::L2R + }; + + // Write the outline tree. + let outline_root_id = outline::write_outline(ctx); + + // Write the document information. + let mut info = ctx.writer.document_info(ctx.alloc.bump()); + let mut xmp = XmpWriter::new(); + if let Some(title) = &ctx.document.title { + info.title(TextStr(title)); + xmp.title([(None, title.as_str())]); + } + + let authors = &ctx.document.author; + if !authors.is_empty() { + info.author(TextStr(&authors.join(", "))); + xmp.creator(authors.iter().map(|s| s.as_str())); + } + info.creator(TextStr("Typst")); + info.finish(); + xmp.creator_tool("Typst"); + xmp.num_pages(ctx.document.pages.len() as u32); + xmp.format("application/pdf"); + xmp.language(ctx.languages.keys().map(|lang| LangId(lang.as_str()))); + xmp.rendition_class(RenditionClass::Proof); + xmp.pdf_version("1.7"); + + let xmp_buf = xmp.finish(None); + let meta_ref = ctx.alloc.bump(); + let mut meta_stream = ctx.writer.stream(meta_ref, xmp_buf.as_bytes()); + meta_stream.pair(Name(b"Type"), Name(b"Metadata")); + meta_stream.pair(Name(b"Subtype"), Name(b"XML")); + meta_stream.finish(); + + // Write the document catalog. + let mut catalog = ctx.writer.catalog(ctx.alloc.bump()); + catalog.pages(ctx.page_tree_ref); + catalog.viewer_preferences().direction(dir); + catalog.pair(Name(b"Metadata"), meta_ref); + + if let Some(outline_root_id) = outline_root_id { + catalog.outlines(outline_root_id); + } + + if let Some(lang) = lang { + catalog.lang(TextStr(lang.as_str())); + } +} + +/// Compress data with the DEFLATE algorithm. +#[tracing::instrument(skip_all)] +fn deflate(data: &[u8]) -> Vec<u8> { + const COMPRESSION_LEVEL: u8 = 6; + miniz_oxide::deflate::compress_to_vec_zlib(data, COMPRESSION_LEVEL) +} + +/// Assigns new, consecutive PDF-internal indices to items. +struct Remapper<T> { + /// Forwards from the items to the pdf indices. + to_pdf: HashMap<T, usize>, + /// Backwards from the pdf indices to the items. + to_items: Vec<T>, +} + +impl<T> Remapper<T> +where + T: Eq + Hash + Clone, +{ + fn new() -> Self { + Self { to_pdf: HashMap::new(), to_items: vec![] } + } + + fn insert(&mut self, item: T) { + let to_layout = &mut self.to_items; + self.to_pdf.entry(item.clone()).or_insert_with(|| { + let pdf_index = to_layout.len(); + to_layout.push(item); + pdf_index + }); + } + + fn map(&self, item: T) -> usize { + self.to_pdf[&item] + } + + fn pdf_indices<'a>( + &'a self, + refs: &'a [Ref], + ) -> impl Iterator<Item = (Ref, usize)> + 'a { + refs.iter().copied().zip(0..self.to_pdf.len()) + } + + fn items(&self) -> impl Iterator<Item = &T> + '_ { + self.to_items.iter() + } +} + +/// Additional methods for [`Abs`]. +trait AbsExt { + /// Convert an to a number of points. + fn to_f32(self) -> f32; +} + +impl AbsExt for Abs { + fn to_f32(self) -> f32 { + self.to_pt() as f32 + } +} + +/// Additional methods for [`Em`]. +trait EmExt { + /// Convert an em length to a number of PDF font units. + fn to_font_units(self) -> f32; +} + +impl EmExt for Em { + fn to_font_units(self) -> f32 { + 1000.0 * self.get() as f32 + } +} + +/// Additional methods for [`Ref`]. +trait RefExt { + /// Bump the reference up by one and return the previous one. + fn bump(&mut self) -> Self; +} + +impl RefExt for Ref { + fn bump(&mut self) -> Self { + let prev = *self; + *self = Self::new(prev.get() + 1); + prev + } +} diff --git a/crates/typst/src/export/pdf/outline.rs b/crates/typst/src/export/pdf/outline.rs new file mode 100644 index 00000000..539647eb --- /dev/null +++ b/crates/typst/src/export/pdf/outline.rs @@ -0,0 +1,127 @@ +use std::num::NonZeroUsize; + +use pdf_writer::{Finish, Ref, TextStr}; + +use super::{AbsExt, PdfContext, RefExt}; +use crate::geom::Abs; +use crate::model::Content; + +/// Construct the outline for the document. +#[tracing::instrument(skip_all)] +pub fn write_outline(ctx: &mut PdfContext) -> Option<Ref> { + let mut tree: Vec<HeadingNode> = vec![]; + for heading in ctx.introspector.query(&item!(heading_func).select()) { + let leaf = HeadingNode::leaf((*heading).clone()); + + let mut children = &mut tree; + while children.last().map_or(false, |last| last.level < leaf.level) { + children = &mut children.last_mut().unwrap().children; + } + + children.push(leaf); + } + + if tree.is_empty() { + return None; + } + + let root_id = ctx.alloc.bump(); + let start_ref = ctx.alloc; + let len = tree.len(); + + let mut prev_ref = None; + for (i, node) in tree.iter().enumerate() { + prev_ref = Some(write_outline_item(ctx, node, root_id, prev_ref, i + 1 == len)); + } + + ctx.writer + .outline(root_id) + .first(start_ref) + .last(Ref::new(ctx.alloc.get() - 1)) + .count(tree.len() as i32); + + Some(root_id) +} + +/// A heading in the outline panel. +#[derive(Debug, Clone)] +struct HeadingNode { + element: Content, + level: NonZeroUsize, + children: Vec<HeadingNode>, +} + +impl HeadingNode { + fn leaf(element: Content) -> Self { + HeadingNode { + level: element.expect_field::<NonZeroUsize>("level"), + element, + children: Vec::new(), + } + } + + fn len(&self) -> usize { + 1 + self.children.iter().map(Self::len).sum::<usize>() + } +} + +/// Write an outline item and all its children. +#[tracing::instrument(skip_all)] +fn write_outline_item( + ctx: &mut PdfContext, + node: &HeadingNode, + parent_ref: Ref, + prev_ref: Option<Ref>, + is_last: bool, +) -> Ref { + let id = ctx.alloc.bump(); + let next_ref = Ref::new(id.get() + node.len() as i32); + + let mut outline = ctx.writer.outline_item(id); + outline.parent(parent_ref); + + if !is_last { + outline.next(next_ref); + } + + if let Some(prev_rev) = prev_ref { + outline.prev(prev_rev); + } + + if !node.children.is_empty() { + let current_child = Ref::new(id.get() + 1); + outline.first(current_child); + outline.last(Ref::new(next_ref.get() - 1)); + outline.count(-(node.children.len() as i32)); + } + + let body = node.element.expect_field::<Content>("body"); + outline.title(TextStr(body.plain_text().trim())); + + let loc = node.element.location().unwrap(); + let pos = ctx.introspector.position(loc); + let index = pos.page.get() - 1; + if let Some(&height) = ctx.page_heights.get(index) { + let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); + outline.dest().page(ctx.page_refs[index]).xyz( + pos.point.x.to_f32(), + height - y.to_f32(), + None, + ); + } + + outline.finish(); + + let mut prev_ref = None; + for (i, child) in node.children.iter().enumerate() { + prev_ref = Some(write_outline_item( + ctx, + child, + id, + prev_ref, + i + 1 == node.children.len(), + )); + } + + id +} diff --git a/crates/typst/src/export/pdf/page.rs b/crates/typst/src/export/pdf/page.rs new file mode 100644 index 00000000..22e590d5 --- /dev/null +++ b/crates/typst/src/export/pdf/page.rs @@ -0,0 +1,565 @@ +use ecow::eco_format; +use pdf_writer::types::{ + ActionType, AnnotationType, ColorSpaceOperand, LineCapStyle, LineJoinStyle, +}; +use pdf_writer::writers::ColorSpace; +use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str}; + +use super::{deflate, AbsExt, EmExt, PdfContext, RefExt, D65_GRAY, SRGB}; +use crate::doc::{Destination, Frame, FrameItem, GroupItem, Meta, TextItem}; +use crate::font::Font; +use crate::geom::{ + self, Abs, Color, Em, Geometry, LineCap, LineJoin, Numeric, Paint, Point, Ratio, + Shape, Size, Stroke, Transform, +}; +use crate::image::Image; + +/// Construct page objects. +#[tracing::instrument(skip_all)] +pub fn construct_pages(ctx: &mut PdfContext, frames: &[Frame]) { + for frame in frames { + construct_page(ctx, frame); + } +} + +/// Construct a page object. +#[tracing::instrument(skip_all)] +pub fn construct_page(ctx: &mut PdfContext, frame: &Frame) { + let page_ref = ctx.alloc.bump(); + ctx.page_refs.push(page_ref); + ctx.page_heights.push(frame.height().to_f32()); + + let mut ctx = PageContext { + parent: ctx, + page_ref, + content: Content::new(), + state: State::default(), + saves: vec![], + bottom: 0.0, + links: vec![], + }; + + let size = frame.size(); + + // Make the coordinate system start at the top-left. + ctx.bottom = size.y.to_f32(); + ctx.transform(Transform { + sx: Ratio::one(), + ky: Ratio::zero(), + kx: Ratio::zero(), + sy: Ratio::new(-1.0), + tx: Abs::zero(), + ty: size.y, + }); + + // Encode the page into the content stream. + write_frame(&mut ctx, frame); + + let page = Page { + size, + content: ctx.content, + id: ctx.page_ref, + links: ctx.links, + }; + + ctx.parent.pages.push(page); +} + +/// Write the page tree. +#[tracing::instrument(skip_all)] +pub fn write_page_tree(ctx: &mut PdfContext) { + for page in std::mem::take(&mut ctx.pages).into_iter() { + write_page(ctx, page); + } + + let mut pages = ctx.writer.pages(ctx.page_tree_ref); + pages + .count(ctx.page_refs.len() as i32) + .kids(ctx.page_refs.iter().copied()); + + let mut resources = pages.resources(); + let mut spaces = resources.color_spaces(); + spaces.insert(SRGB).start::<ColorSpace>().srgb(); + spaces.insert(D65_GRAY).start::<ColorSpace>().d65_gray(); + spaces.finish(); + + let mut fonts = resources.fonts(); + for (font_ref, f) in ctx.font_map.pdf_indices(&ctx.font_refs) { + let name = eco_format!("F{}", f); + fonts.pair(Name(name.as_bytes()), font_ref); + } + + fonts.finish(); + + let mut images = resources.x_objects(); + for (image_ref, im) in ctx.image_map.pdf_indices(&ctx.image_refs) { + let name = eco_format!("Im{}", im); + images.pair(Name(name.as_bytes()), image_ref); + } + + images.finish(); + resources.finish(); + pages.finish(); +} + +/// Write a page tree node. +#[tracing::instrument(skip_all)] +fn write_page(ctx: &mut PdfContext, page: Page) { + let content_id = ctx.alloc.bump(); + + let mut page_writer = ctx.writer.page(page.id); + page_writer.parent(ctx.page_tree_ref); + + let w = page.size.x.to_f32(); + let h = page.size.y.to_f32(); + page_writer.media_box(Rect::new(0.0, 0.0, w, h)); + page_writer.contents(content_id); + + let mut annotations = page_writer.annotations(); + for (dest, rect) in page.links { + let mut annotation = annotations.push(); + annotation.subtype(AnnotationType::Link).rect(rect); + annotation.border(0.0, 0.0, 0.0, None); + + let pos = match dest { + Destination::Url(uri) => { + annotation + .action() + .action_type(ActionType::Uri) + .uri(Str(uri.as_bytes())); + continue; + } + Destination::Position(pos) => pos, + Destination::Location(loc) => ctx.introspector.position(loc), + }; + + let index = pos.page.get() - 1; + let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); + if let Some(&height) = ctx.page_heights.get(index) { + annotation + .action() + .action_type(ActionType::GoTo) + .destination() + .page(ctx.page_refs[index]) + .xyz(pos.point.x.to_f32(), height - y.to_f32(), None); + } + } + + annotations.finish(); + page_writer.finish(); + + let data = page.content.finish(); + let data = deflate(&data); + ctx.writer.stream(content_id, &data).filter(Filter::FlateDecode); +} + +/// Data for an exported page. +pub struct Page { + /// The indirect object id of the page. + pub id: Ref, + /// The page's dimensions. + pub size: Size, + /// The page's content stream. + pub content: Content, + /// Links in the PDF coordinate system. + pub links: Vec<(Destination, Rect)>, +} + +/// An exporter for the contents of a single PDF page. +struct PageContext<'a, 'b> { + parent: &'a mut PdfContext<'b>, + page_ref: Ref, + content: Content, + state: State, + saves: Vec<State>, + bottom: f32, + links: Vec<(Destination, Rect)>, +} + +/// A simulated graphics state used to deduplicate graphics state changes and +/// keep track of the current transformation matrix for link annotations. +#[derive(Debug, Default, Clone)] +struct State { + transform: Transform, + font: Option<(Font, Abs)>, + fill: Option<Paint>, + fill_space: Option<Name<'static>>, + stroke: Option<Stroke>, + stroke_space: Option<Name<'static>>, +} + +impl PageContext<'_, '_> { + fn save_state(&mut self) { + self.saves.push(self.state.clone()); + self.content.save_state(); + } + + fn restore_state(&mut self) { + self.content.restore_state(); + self.state = self.saves.pop().expect("missing state save"); + } + + fn transform(&mut self, transform: Transform) { + let Transform { sx, ky, kx, sy, tx, ty } = transform; + self.state.transform = self.state.transform.pre_concat(transform); + self.content.transform([ + sx.get() as _, + ky.get() as _, + kx.get() as _, + sy.get() as _, + tx.to_f32(), + ty.to_f32(), + ]); + } + + fn set_font(&mut self, font: &Font, size: Abs) { + if self.state.font.as_ref().map(|(f, s)| (f, *s)) != Some((font, size)) { + self.parent.font_map.insert(font.clone()); + let name = eco_format!("F{}", self.parent.font_map.map(font.clone())); + self.content.set_font(Name(name.as_bytes()), size.to_f32()); + self.state.font = Some((font.clone(), size)); + } + } + + fn set_fill(&mut self, fill: &Paint) { + if self.state.fill.as_ref() != Some(fill) { + let f = |c| c as f32 / 255.0; + let Paint::Solid(color) = fill; + match color { + Color::Luma(c) => { + self.set_fill_color_space(D65_GRAY); + self.content.set_fill_gray(f(c.0)); + } + Color::Rgba(c) => { + self.set_fill_color_space(SRGB); + self.content.set_fill_color([f(c.r), f(c.g), f(c.b)]); + } + Color::Cmyk(c) => { + self.reset_fill_color_space(); + self.content.set_fill_cmyk(f(c.c), f(c.m), f(c.y), f(c.k)); + } + } + self.state.fill = Some(fill.clone()); + } + } + + fn set_fill_color_space(&mut self, space: Name<'static>) { + if self.state.fill_space != Some(space) { + self.content.set_fill_color_space(ColorSpaceOperand::Named(space)); + self.state.fill_space = Some(space); + } + } + + fn reset_fill_color_space(&mut self) { + self.state.fill_space = None; + } + + fn set_stroke(&mut self, stroke: &Stroke) { + if self.state.stroke.as_ref() != Some(stroke) { + let Stroke { + paint, + thickness, + line_cap, + line_join, + dash_pattern, + miter_limit, + } = stroke; + + let f = |c| c as f32 / 255.0; + let Paint::Solid(color) = paint; + match color { + Color::Luma(c) => { + self.set_stroke_color_space(D65_GRAY); + self.content.set_stroke_gray(f(c.0)); + } + Color::Rgba(c) => { + self.set_stroke_color_space(SRGB); + self.content.set_stroke_color([f(c.r), f(c.g), f(c.b)]); + } + Color::Cmyk(c) => { + self.reset_stroke_color_space(); + self.content.set_stroke_cmyk(f(c.c), f(c.m), f(c.y), f(c.k)); + } + } + + self.content.set_line_width(thickness.to_f32()); + if self.state.stroke.as_ref().map(|s| &s.line_cap) != Some(line_cap) { + self.content.set_line_cap(line_cap.into()); + } + if self.state.stroke.as_ref().map(|s| &s.line_join) != Some(line_join) { + self.content.set_line_join(line_join.into()); + } + if self.state.stroke.as_ref().map(|s| &s.dash_pattern) != Some(dash_pattern) { + if let Some(pattern) = dash_pattern { + self.content.set_dash_pattern( + pattern.array.iter().map(|l| l.to_f32()), + pattern.phase.to_f32(), + ); + } else { + self.content.set_dash_pattern([], 0.0); + } + } + if self.state.stroke.as_ref().map(|s| &s.miter_limit) != Some(miter_limit) { + self.content.set_miter_limit(miter_limit.0 as f32); + } + self.state.stroke = Some(stroke.clone()); + } + } + + fn set_stroke_color_space(&mut self, space: Name<'static>) { + if self.state.stroke_space != Some(space) { + self.content.set_stroke_color_space(ColorSpaceOperand::Named(space)); + self.state.stroke_space = Some(space); + } + } + + fn reset_stroke_color_space(&mut self) { + self.state.stroke_space = None; + } +} + +/// Encode a frame into the content stream. +fn write_frame(ctx: &mut PageContext, frame: &Frame) { + for &(pos, ref item) in frame.items() { + let x = pos.x.to_f32(); + let y = pos.y.to_f32(); + match item { + FrameItem::Group(group) => write_group(ctx, pos, group), + FrameItem::Text(text) => write_text(ctx, x, y, text), + FrameItem::Shape(shape, _) => write_shape(ctx, x, y, shape), + FrameItem::Image(image, size, _) => write_image(ctx, x, y, image, *size), + FrameItem::Meta(meta, size) => match meta { + Meta::Link(dest) => write_link(ctx, pos, dest, *size), + Meta::Elem(_) => {} + Meta::Hide => {} + Meta::PageNumbering(_) => {} + }, + } + } +} + +/// Encode a group into the content stream. +fn write_group(ctx: &mut PageContext, pos: Point, group: &GroupItem) { + let translation = Transform::translate(pos.x, pos.y); + + ctx.save_state(); + ctx.transform(translation.pre_concat(group.transform)); + + if group.clips { + let size = group.frame.size(); + let w = size.x.to_f32(); + let h = size.y.to_f32(); + ctx.content.move_to(0.0, 0.0); + ctx.content.line_to(w, 0.0); + ctx.content.line_to(w, h); + ctx.content.line_to(0.0, h); + ctx.content.clip_nonzero(); + ctx.content.end_path(); + } + + write_frame(ctx, &group.frame); + ctx.restore_state(); +} + +/// Encode a text run into the content stream. +fn write_text(ctx: &mut PageContext, x: f32, y: f32, text: &TextItem) { + *ctx.parent.languages.entry(text.lang).or_insert(0) += text.glyphs.len(); + + let glyph_set = ctx.parent.glyph_sets.entry(text.font.clone()).or_default(); + for g in &text.glyphs { + let segment = &text.text[g.range()]; + glyph_set.entry(g.id).or_insert_with(|| segment.into()); + } + + ctx.set_fill(&text.fill); + ctx.set_font(&text.font, text.size); + ctx.content.begin_text(); + + // Positiosn the text. + ctx.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]); + + let mut positioned = ctx.content.show_positioned(); + let mut items = positioned.items(); + let mut adjustment = Em::zero(); + let mut encoded = vec![]; + + // Write the glyphs with kerning adjustments. + for glyph in &text.glyphs { + adjustment += glyph.x_offset; + + if !adjustment.is_zero() { + if !encoded.is_empty() { + items.show(Str(&encoded)); + encoded.clear(); + } + + items.adjust(-adjustment.to_font_units()); + adjustment = Em::zero(); + } + + encoded.push((glyph.id >> 8) as u8); + encoded.push((glyph.id & 0xff) as u8); + + if let Some(advance) = text.font.advance(glyph.id) { + adjustment += glyph.x_advance - advance; + } + + adjustment -= glyph.x_offset; + } + + if !encoded.is_empty() { + items.show(Str(&encoded)); + } + + items.finish(); + positioned.finish(); + ctx.content.end_text(); +} + +/// Encode a geometrical shape into the content stream. +fn write_shape(ctx: &mut PageContext, x: f32, y: f32, shape: &Shape) { + let stroke = shape.stroke.as_ref().and_then(|stroke| { + if stroke.thickness.to_f32() > 0.0 { + Some(stroke) + } else { + None + } + }); + + if shape.fill.is_none() && stroke.is_none() { + return; + } + + if let Some(fill) = &shape.fill { + ctx.set_fill(fill); + } + + if let Some(stroke) = stroke { + ctx.set_stroke(stroke); + } + + match shape.geometry { + Geometry::Line(target) => { + let dx = target.x.to_f32(); + let dy = target.y.to_f32(); + ctx.content.move_to(x, y); + ctx.content.line_to(x + dx, y + dy); + } + Geometry::Rect(size) => { + let w = size.x.to_f32(); + let h = size.y.to_f32(); + if w > 0.0 && h > 0.0 { + ctx.content.rect(x, y, w, h); + } + } + Geometry::Path(ref path) => { + write_path(ctx, x, y, path); + } + } + + match (&shape.fill, stroke) { + (None, None) => unreachable!(), + (Some(_), None) => ctx.content.fill_nonzero(), + (None, Some(_)) => ctx.content.stroke(), + (Some(_), Some(_)) => ctx.content.fill_nonzero_and_stroke(), + }; +} + +/// Encode a bezier path into the content stream. +fn write_path(ctx: &mut PageContext, x: f32, y: f32, path: &geom::Path) { + for elem in &path.0 { + match elem { + geom::PathItem::MoveTo(p) => { + ctx.content.move_to(x + p.x.to_f32(), y + p.y.to_f32()) + } + geom::PathItem::LineTo(p) => { + ctx.content.line_to(x + p.x.to_f32(), y + p.y.to_f32()) + } + geom::PathItem::CubicTo(p1, p2, p3) => ctx.content.cubic_to( + x + p1.x.to_f32(), + y + p1.y.to_f32(), + x + p2.x.to_f32(), + y + p2.y.to_f32(), + x + p3.x.to_f32(), + y + p3.y.to_f32(), + ), + geom::PathItem::ClosePath => ctx.content.close_path(), + }; + } +} + +/// Encode a vector or raster image into the content stream. +fn write_image(ctx: &mut PageContext, x: f32, y: f32, image: &Image, size: Size) { + ctx.parent.image_map.insert(image.clone()); + let name = eco_format!("Im{}", ctx.parent.image_map.map(image.clone())); + let w = size.x.to_f32(); + let h = size.y.to_f32(); + ctx.content.save_state(); + ctx.content.transform([w, 0.0, 0.0, -h, x, y + h]); + + if let Some(alt) = image.alt() { + let mut image_span = + ctx.content.begin_marked_content_with_properties(Name(b"Span")); + let mut image_alt = image_span.properties(); + image_alt.pair(Name(b"Alt"), pdf_writer::Str(alt.as_bytes())); + image_alt.finish(); + image_span.finish(); + + ctx.content.x_object(Name(name.as_bytes())); + ctx.content.end_marked_content(); + } else { + ctx.content.x_object(Name(name.as_bytes())); + } + + ctx.content.restore_state(); +} + +/// Save a link for later writing in the annotations dictionary. +fn write_link(ctx: &mut PageContext, pos: Point, dest: &Destination, size: Size) { + let mut min_x = Abs::inf(); + let mut min_y = Abs::inf(); + let mut max_x = -Abs::inf(); + let mut max_y = -Abs::inf(); + + // Compute the bounding box of the transformed link. + for point in [ + pos, + pos + Point::with_x(size.x), + pos + Point::with_y(size.y), + pos + size.to_point(), + ] { + let t = point.transform(ctx.state.transform); + min_x.set_min(t.x); + min_y.set_min(t.y); + max_x.set_max(t.x); + max_y.set_max(t.y); + } + + let x1 = min_x.to_f32(); + let x2 = max_x.to_f32(); + let y1 = max_y.to_f32(); + let y2 = min_y.to_f32(); + let rect = Rect::new(x1, y1, x2, y2); + + ctx.links.push((dest.clone(), rect)); +} + +impl From<&LineCap> for LineCapStyle { + fn from(line_cap: &LineCap) -> Self { + match line_cap { + LineCap::Butt => LineCapStyle::ButtCap, + LineCap::Round => LineCapStyle::RoundCap, + LineCap::Square => LineCapStyle::ProjectingSquareCap, + } + } +} + +impl From<&LineJoin> for LineJoinStyle { + fn from(line_join: &LineJoin) -> Self { + match line_join { + LineJoin::Miter => LineJoinStyle::MiterJoin, + LineJoin::Round => LineJoinStyle::RoundJoin, + LineJoin::Bevel => LineJoinStyle::BevelJoin, + } + } +} diff --git a/crates/typst/src/export/render.rs b/crates/typst/src/export/render.rs new file mode 100644 index 00000000..d8115b12 --- /dev/null +++ b/crates/typst/src/export/render.rs @@ -0,0 +1,673 @@ +//! Rendering into raster images. + +use std::io::Read; +use std::sync::Arc; + +use image::imageops::FilterType; +use image::{GenericImageView, Rgba}; +use pixglyph::Bitmap; +use resvg::FitTo; +use tiny_skia as sk; +use ttf_parser::{GlyphId, OutlineBuilder}; +use usvg::{NodeExt, TreeParsing}; + +use crate::doc::{Frame, FrameItem, GroupItem, Meta, TextItem}; +use crate::font::Font; +use crate::geom::{ + self, Abs, Color, Geometry, LineCap, LineJoin, Paint, PathItem, Shape, Size, Stroke, + Transform, +}; +use crate::image::{DecodedImage, Image}; + +/// Export a frame into a raster image. +/// +/// This renders the frame at the given number of pixels per point and returns +/// the resulting `tiny-skia` pixel buffer. +pub fn render(frame: &Frame, pixel_per_pt: f32, fill: Color) -> sk::Pixmap { + let size = frame.size(); + let pxw = (pixel_per_pt * size.x.to_f32()).round().max(1.0) as u32; + let pxh = (pixel_per_pt * size.y.to_f32()).round().max(1.0) as u32; + + let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap(); + canvas.fill(fill.into()); + + let ts = sk::Transform::from_scale(pixel_per_pt, pixel_per_pt); + render_frame(&mut canvas, ts, None, frame); + + canvas +} + +/// Render a frame into the canvas. +fn render_frame( + canvas: &mut sk::Pixmap, + ts: sk::Transform, + mask: Option<&sk::Mask>, + frame: &Frame, +) { + for (pos, item) in frame.items() { + let x = pos.x.to_f32(); + let y = pos.y.to_f32(); + let ts = ts.pre_translate(x, y); + + match item { + FrameItem::Group(group) => { + render_group(canvas, ts, mask, group); + } + FrameItem::Text(text) => { + render_text(canvas, ts, mask, text); + } + FrameItem::Shape(shape, _) => { + render_shape(canvas, ts, mask, shape); + } + FrameItem::Image(image, size, _) => { + render_image(canvas, ts, mask, image, *size); + } + FrameItem::Meta(meta, _) => match meta { + Meta::Link(_) => {} + Meta::Elem(_) => {} + Meta::PageNumbering(_) => {} + Meta::Hide => {} + }, + } + } +} + +/// Render a group frame with optional transform and clipping into the canvas. +fn render_group( + canvas: &mut sk::Pixmap, + ts: sk::Transform, + mask: Option<&sk::Mask>, + group: &GroupItem, +) { + let ts = ts.pre_concat(group.transform.into()); + + let mut mask = mask; + let storage; + if group.clips { + let size = group.frame.size(); + let w = size.x.to_f32(); + let h = size.y.to_f32(); + if let Some(path) = sk::Rect::from_xywh(0.0, 0.0, w, h) + .map(sk::PathBuilder::from_rect) + .and_then(|path| path.transform(ts)) + { + if let Some(mask) = mask { + let mut mask = mask.clone(); + mask.intersect_path( + &path, + sk::FillRule::default(), + false, + sk::Transform::default(), + ); + storage = mask; + } else { + let pxw = canvas.width(); + let pxh = canvas.height(); + let Some(mut mask) = sk::Mask::new(pxw, pxh) else { + // Fails if clipping rect is empty. In that case we just + // clip everything by returning. + return; + }; + + mask.fill_path( + &path, + sk::FillRule::default(), + false, + sk::Transform::default(), + ); + storage = mask; + }; + + mask = Some(&storage); + } + } + + render_frame(canvas, ts, mask, &group.frame); +} + +/// Render a text run into the canvas. +fn render_text( + canvas: &mut sk::Pixmap, + ts: sk::Transform, + mask: Option<&sk::Mask>, + text: &TextItem, +) { + let mut x = 0.0; + for glyph in &text.glyphs { + let id = GlyphId(glyph.id); + let offset = x + glyph.x_offset.at(text.size).to_f32(); + let ts = ts.pre_translate(offset, 0.0); + + render_svg_glyph(canvas, ts, mask, text, id) + .or_else(|| render_bitmap_glyph(canvas, ts, mask, text, id)) + .or_else(|| render_outline_glyph(canvas, ts, mask, text, id)); + + x += glyph.x_advance.at(text.size).to_f32(); + } +} + +/// Render an SVG glyph into the canvas. +fn render_svg_glyph( + canvas: &mut sk::Pixmap, + ts: sk::Transform, + mask: Option<&sk::Mask>, + text: &TextItem, + id: GlyphId, +) -> Option<()> { + let mut data = text.font.ttf().glyph_svg_image(id)?; + + // Decompress SVGZ. + let mut decoded = vec![]; + if data.starts_with(&[0x1f, 0x8b]) { + let mut decoder = flate2::read::GzDecoder::new(data); + decoder.read_to_end(&mut decoded).ok()?; + data = &decoded; + } + + // Parse XML. + let xml = std::str::from_utf8(data).ok()?; + let document = roxmltree::Document::parse(xml).ok()?; + let root = document.root_element(); + + // Parse SVG. + let opts = usvg::Options::default(); + let tree = usvg::Tree::from_xmltree(&document, &opts).ok()?; + let view_box = tree.view_box.rect; + + // If there's no viewbox defined, use the em square for our scale + // transformation ... + let upem = text.font.units_per_em() as f32; + let (mut width, mut height) = (upem, upem); + + // ... but if there's a viewbox or width, use that. + if root.has_attribute("viewBox") || root.has_attribute("width") { + width = view_box.width() as f32; + } + + // Same as for width. + if root.has_attribute("viewBox") || root.has_attribute("height") { + height = view_box.height() as f32; + } + + let size = text.size.to_f32(); + let ts = ts.pre_scale(size / width, size / height); + + // Compute the space we need to draw our glyph. + // See https://github.com/RazrFalcon/resvg/issues/602 for why + // using the svg size is problematic here. + let mut bbox = usvg::Rect::new_bbox(); + for node in tree.root.descendants() { + if let Some(rect) = node.calculate_bbox().and_then(|b| b.to_rect()) { + bbox = bbox.expand(rect); + } + } + + let canvas_rect = usvg::ScreenRect::new(0, 0, canvas.width(), canvas.height())?; + + // Compute the bbox after the transform is applied. + // We add a nice 5px border along the bounding box to + // be on the safe size. We also compute the intersection + // with the canvas rectangle + let svg_ts = usvg::Transform::new( + ts.sx.into(), + ts.kx.into(), + ts.ky.into(), + ts.sy.into(), + ts.tx.into(), + ts.ty.into(), + ); + let bbox = bbox.transform(&svg_ts)?.to_screen_rect(); + let bbox = usvg::ScreenRect::new( + bbox.left() - 5, + bbox.y() - 5, + bbox.width() + 10, + bbox.height() + 10, + )? + .fit_to_rect(canvas_rect); + + let mut pixmap = sk::Pixmap::new(bbox.width(), bbox.height())?; + + // We offset our transform so that the pixmap starts at the edge of the bbox. + let ts = ts.post_translate(-bbox.left() as f32, -bbox.top() as f32); + resvg::render(&tree, FitTo::Original, ts, pixmap.as_mut())?; + + canvas.draw_pixmap( + bbox.left(), + bbox.top(), + pixmap.as_ref(), + &sk::PixmapPaint::default(), + sk::Transform::identity(), + mask, + ); + + Some(()) +} + +/// Render a bitmap glyph into the canvas. +fn render_bitmap_glyph( + canvas: &mut sk::Pixmap, + ts: sk::Transform, + mask: Option<&sk::Mask>, + text: &TextItem, + id: GlyphId, +) -> Option<()> { + let size = text.size.to_f32(); + let ppem = size * ts.sy; + let raster = text.font.ttf().glyph_raster_image(id, ppem as u16)?; + let image = Image::new(raster.data.into(), raster.format.into(), None).ok()?; + + // FIXME: Vertical alignment isn't quite right for Apple Color Emoji, + // and maybe also for Noto Color Emoji. And: Is the size calculation + // correct? + let h = text.size; + let w = (image.width() as f64 / image.height() as f64) * h; + let dx = (raster.x as f32) / (image.width() as f32) * size; + let dy = (raster.y as f32) / (image.height() as f32) * size; + let ts = ts.pre_translate(dx, -size - dy); + render_image(canvas, ts, mask, &image, Size::new(w, h)) +} + +/// Render an outline glyph into the canvas. This is the "normal" case. +fn render_outline_glyph( + canvas: &mut sk::Pixmap, + ts: sk::Transform, + mask: Option<&sk::Mask>, + text: &TextItem, + id: GlyphId, +) -> Option<()> { + let ppem = text.size.to_f32() * ts.sy; + + // Render a glyph directly as a path. This only happens when the fast glyph + // rasterization can't be used due to very large text size or weird + // scale/skewing transforms. + if ppem > 100.0 || ts.kx != 0.0 || ts.ky != 0.0 || ts.sx != ts.sy { + let path = { + let mut builder = WrappedPathBuilder(sk::PathBuilder::new()); + text.font.ttf().outline_glyph(id, &mut builder)?; + builder.0.finish()? + }; + + let paint = (&text.fill).into(); + let rule = sk::FillRule::default(); + + // Flip vertically because font design coordinate + // system is Y-up. + let scale = text.size.to_f32() / text.font.units_per_em() as f32; + let ts = ts.pre_scale(scale, -scale); + canvas.fill_path(&path, &paint, rule, ts, mask); + return Some(()); + } + + // Rasterize the glyph with `pixglyph`. + #[comemo::memoize] + fn rasterize( + font: &Font, + id: GlyphId, + x: u32, + y: u32, + size: u32, + ) -> Option<Arc<Bitmap>> { + let glyph = pixglyph::Glyph::load(font.ttf(), id)?; + Some(Arc::new(glyph.rasterize( + f32::from_bits(x), + f32::from_bits(y), + f32::from_bits(size), + ))) + } + + // Try to retrieve a prepared glyph or prepare it from scratch if it + // doesn't exist, yet. + let bitmap = + rasterize(&text.font, id, ts.tx.to_bits(), ts.ty.to_bits(), ppem.to_bits())?; + + // If we have a clip mask we first render to a pixmap that we then blend + // with our canvas + if mask.is_some() { + let mw = bitmap.width; + let mh = bitmap.height; + + let Paint::Solid(color) = text.fill; + let c = color.to_rgba(); + + // Pad the pixmap with 1 pixel in each dimension so that we do + // not get any problem with floating point errors along their border + let mut pixmap = sk::Pixmap::new(mw + 2, mh + 2)?; + for x in 0..mw { + for y in 0..mh { + let alpha = bitmap.coverage[(y * mw + x) as usize]; + let color = sk::ColorU8::from_rgba(c.r, c.g, c.b, alpha).premultiply(); + pixmap.pixels_mut()[((y + 1) * (mw + 2) + (x + 1)) as usize] = color; + } + } + + let left = bitmap.left; + let top = bitmap.top; + + canvas.draw_pixmap( + left - 1, + top - 1, + pixmap.as_ref(), + &sk::PixmapPaint::default(), + sk::Transform::identity(), + mask, + ); + } else { + let cw = canvas.width() as i32; + let ch = canvas.height() as i32; + let mw = bitmap.width as i32; + let mh = bitmap.height as i32; + + // Determine the pixel bounding box that we actually need to draw. + let left = bitmap.left; + let right = left + mw; + let top = bitmap.top; + let bottom = top + mh; + + // Premultiply the text color. + let Paint::Solid(color) = text.fill; + let c = color.to_rgba(); + let color = sk::ColorU8::from_rgba(c.r, c.g, c.b, 255).premultiply().get(); + + // Blend the glyph bitmap with the existing pixels on the canvas. + let pixels = bytemuck::cast_slice_mut::<u8, u32>(canvas.data_mut()); + for x in left.clamp(0, cw)..right.clamp(0, cw) { + for y in top.clamp(0, ch)..bottom.clamp(0, ch) { + let ai = ((y - top) * mw + (x - left)) as usize; + let cov = bitmap.coverage[ai]; + if cov == 0 { + continue; + } + + let pi = (y * cw + x) as usize; + if cov == 255 { + pixels[pi] = color; + continue; + } + + let applied = alpha_mul(color, cov as u32); + pixels[pi] = blend_src_over(applied, pixels[pi]); + } + } + } + + Some(()) +} + +/// Render a geometrical shape into the canvas. +fn render_shape( + canvas: &mut sk::Pixmap, + ts: sk::Transform, + mask: Option<&sk::Mask>, + shape: &Shape, +) -> Option<()> { + let path = match shape.geometry { + Geometry::Line(target) => { + let mut builder = sk::PathBuilder::new(); + builder.line_to(target.x.to_f32(), target.y.to_f32()); + builder.finish()? + } + Geometry::Rect(size) => { + let w = size.x.to_f32(); + let h = size.y.to_f32(); + let rect = sk::Rect::from_xywh(0.0, 0.0, w, h)?; + sk::PathBuilder::from_rect(rect) + } + Geometry::Path(ref path) => convert_path(path)?, + }; + + if let Some(fill) = &shape.fill { + let mut paint: sk::Paint = fill.into(); + if matches!(shape.geometry, Geometry::Rect(_)) { + paint.anti_alias = false; + } + + let rule = sk::FillRule::default(); + canvas.fill_path(&path, &paint, rule, ts, mask); + } + + if let Some(Stroke { + paint, + thickness, + line_cap, + line_join, + dash_pattern, + miter_limit, + }) = &shape.stroke + { + let width = thickness.to_f32(); + + // Don't draw zero-pt stroke. + if width > 0.0 { + let dash = dash_pattern.as_ref().and_then(|pattern| { + // tiny-skia only allows dash patterns with an even number of elements, + // while pdf allows any number. + let pattern_len = pattern.array.len(); + let len = + if pattern_len % 2 == 1 { 2 * pattern_len } else { pattern_len }; + let dash_array = + pattern.array.iter().map(|l| l.to_f32()).cycle().take(len).collect(); + + sk::StrokeDash::new(dash_array, pattern.phase.to_f32()) + }); + let paint = paint.into(); + let stroke = sk::Stroke { + width, + line_cap: line_cap.into(), + line_join: line_join.into(), + dash, + miter_limit: miter_limit.0 as f32, + }; + canvas.stroke_path(&path, &paint, &stroke, ts, mask); + } + } + + Some(()) +} + +/// Convert a Typst path into a tiny-skia path. +fn convert_path(path: &geom::Path) -> Option<sk::Path> { + let mut builder = sk::PathBuilder::new(); + for elem in &path.0 { + match elem { + PathItem::MoveTo(p) => { + builder.move_to(p.x.to_f32(), p.y.to_f32()); + } + PathItem::LineTo(p) => { + builder.line_to(p.x.to_f32(), p.y.to_f32()); + } + PathItem::CubicTo(p1, p2, p3) => { + builder.cubic_to( + p1.x.to_f32(), + p1.y.to_f32(), + p2.x.to_f32(), + p2.y.to_f32(), + p3.x.to_f32(), + p3.y.to_f32(), + ); + } + PathItem::ClosePath => { + builder.close(); + } + }; + } + builder.finish() +} + +/// Render a raster or SVG image into the canvas. +fn render_image( + canvas: &mut sk::Pixmap, + ts: sk::Transform, + mask: Option<&sk::Mask>, + image: &Image, + size: Size, +) -> Option<()> { + let view_width = size.x.to_f32(); + let view_height = size.y.to_f32(); + + // For better-looking output, resize `image` to its final size before + // painting it to `canvas`. For the math, see: + // https://github.com/typst/typst/issues/1404#issuecomment-1598374652 + let theta = f32::atan2(-ts.kx, ts.sx); + + // To avoid division by 0, choose the one of { sin, cos } that is + // further from 0. + let prefer_sin = theta.sin().abs() > std::f32::consts::FRAC_1_SQRT_2; + let scale_x = + f32::abs(if prefer_sin { ts.kx / theta.sin() } else { ts.sx / theta.cos() }); + + let aspect = (image.width() as f32) / (image.height() as f32); + let w = (scale_x * view_width.max(aspect * view_height)).ceil() as u32; + let h = ((w as f32) / aspect).ceil() as u32; + + let pixmap = scaled_texture(image, w, h)?; + let paint_scale_x = view_width / pixmap.width() as f32; + let paint_scale_y = view_height / pixmap.height() as f32; + + let paint = sk::Paint { + shader: sk::Pattern::new( + (*pixmap).as_ref(), + sk::SpreadMode::Pad, + sk::FilterQuality::Nearest, + 1.0, + sk::Transform::from_scale(paint_scale_x, paint_scale_y), + ), + ..Default::default() + }; + + let rect = sk::Rect::from_xywh(0.0, 0.0, view_width, view_height)?; + canvas.fill_rect(rect, &paint, ts, mask); + + Some(()) +} + +/// Prepare a texture for an image at a scaled size. +#[comemo::memoize] +fn scaled_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> { + let mut pixmap = sk::Pixmap::new(w, h)?; + match image.decoded().as_ref() { + DecodedImage::Raster(dynamic, _, _) => { + let downscale = w < image.width(); + let filter = + if downscale { FilterType::Lanczos3 } else { FilterType::CatmullRom }; + let buf = dynamic.resize(w, h, filter); + for ((_, _, src), dest) in buf.pixels().zip(pixmap.pixels_mut()) { + let Rgba([r, g, b, a]) = src; + *dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply(); + } + } + DecodedImage::Svg(tree) => { + resvg::render( + tree, + FitTo::Size(w, h), + sk::Transform::identity(), + pixmap.as_mut(), + )?; + } + } + Some(Arc::new(pixmap)) +} + +impl From<Transform> for sk::Transform { + fn from(transform: Transform) -> Self { + let Transform { sx, ky, kx, sy, tx, ty } = transform; + sk::Transform::from_row( + sx.get() as _, + ky.get() as _, + kx.get() as _, + sy.get() as _, + tx.to_f32(), + ty.to_f32(), + ) + } +} + +impl From<&Paint> for sk::Paint<'static> { + fn from(paint: &Paint) -> Self { + let mut sk_paint = sk::Paint::default(); + let Paint::Solid(color) = *paint; + sk_paint.set_color(color.into()); + sk_paint.anti_alias = true; + sk_paint + } +} + +impl From<Color> for sk::Color { + fn from(color: Color) -> Self { + let c = color.to_rgba(); + sk::Color::from_rgba8(c.r, c.g, c.b, c.a) + } +} + +impl From<&LineCap> for sk::LineCap { + fn from(line_cap: &LineCap) -> Self { + match line_cap { + LineCap::Butt => sk::LineCap::Butt, + LineCap::Round => sk::LineCap::Round, + LineCap::Square => sk::LineCap::Square, + } + } +} + +impl From<&LineJoin> for sk::LineJoin { + fn from(line_join: &LineJoin) -> Self { + match line_join { + LineJoin::Miter => sk::LineJoin::Miter, + LineJoin::Round => sk::LineJoin::Round, + LineJoin::Bevel => sk::LineJoin::Bevel, + } + } +} + +/// Allows to build tiny-skia paths from glyph outlines. +struct WrappedPathBuilder(sk::PathBuilder); + +impl OutlineBuilder for WrappedPathBuilder { + fn move_to(&mut self, x: f32, y: f32) { + self.0.move_to(x, y); + } + + fn line_to(&mut self, x: f32, y: f32) { + self.0.line_to(x, y); + } + + fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { + self.0.quad_to(x1, y1, x, y); + } + + fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { + self.0.cubic_to(x1, y1, x2, y2, x, y); + } + + fn close(&mut self) { + self.0.close(); + } +} + +/// Additional methods for [`Length`]. +trait AbsExt { + /// Convert to a number of points as f32. + fn to_f32(self) -> f32; +} + +impl AbsExt for Abs { + fn to_f32(self) -> f32 { + self.to_pt() as f32 + } +} + +// Alpha multiplication and blending are ported from: +// https://skia.googlesource.com/skia/+/refs/heads/main/include/core/SkColorPriv.h + +/// Blends two premulitplied, packed 32-bit RGBA colors. Alpha channel must be +/// in the 8 high bits. +fn blend_src_over(src: u32, dst: u32) -> u32 { + src + alpha_mul(dst, 256 - (src >> 24)) +} + +/// Alpha multiply a color. +fn alpha_mul(color: u32, scale: u32) -> u32 { + let mask = 0xff00ff; + let rb = ((color & mask) * scale) >> 8; + let ag = ((color >> 8) & mask) * scale; + (rb & mask) | (ag & !mask) +} diff --git a/crates/typst/src/file.rs b/crates/typst/src/file.rs new file mode 100644 index 00000000..8aaa746b --- /dev/null +++ b/crates/typst/src/file.rs @@ -0,0 +1,303 @@ +//! File and package management. + +use std::collections::HashMap; +use std::fmt::{self, Debug, Display, Formatter}; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::sync::RwLock; + +use ecow::{eco_format, EcoString}; +use once_cell::sync::Lazy; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::diag::{bail, FileError, StrResult}; +use crate::syntax::is_ident; +use crate::util::PathExt; + +/// The global package-path interner. +static INTERNER: Lazy<RwLock<Interner>> = + Lazy::new(|| RwLock::new(Interner { to_id: HashMap::new(), from_id: Vec::new() })); + +/// A package-path interner. +struct Interner { + to_id: HashMap<Pair, FileId>, + from_id: Vec<Pair>, +} + +/// An interned pair of a package specification and a path. +type Pair = &'static (Option<PackageSpec>, PathBuf); + +/// Identifies a file. +/// +/// This type is globally interned and thus cheap to copy, compare, and hash. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct FileId(u16); + +impl FileId { + /// Create a new interned file specification. + /// + /// The path must start with a `/` or this function will panic. + /// Note that the path is normalized before interning. + #[track_caller] + pub fn new(package: Option<PackageSpec>, path: &Path) -> Self { + assert_eq!( + path.components().next(), + Some(std::path::Component::RootDir), + "file path must be absolute within project or package: {}", + path.display(), + ); + + // Try to find an existing entry that we can reuse. + let pair = (package, path.normalize()); + if let Some(&id) = INTERNER.read().unwrap().to_id.get(&pair) { + return id; + } + + let mut interner = INTERNER.write().unwrap(); + let len = interner.from_id.len(); + if len >= usize::from(u16::MAX) { + panic!("too many file specifications"); + } + + // Create a new entry forever by leaking the pair. We can't leak more + // than 2^16 pair (and typically will leak a lot less), so its not a + // big deal. + let id = FileId(len as u16); + let leaked = Box::leak(Box::new(pair)); + interner.to_id.insert(leaked, id); + interner.from_id.push(leaked); + id + } + + /// Get an id that does not identify any real file. + pub const fn detached() -> Self { + Self(u16::MAX) + } + + /// Whether the id is the detached. + pub const fn is_detached(self) -> bool { + self.0 == Self::detached().0 + } + + /// The package the file resides in, if any. + pub fn package(&self) -> Option<&'static PackageSpec> { + if self.is_detached() { + None + } else { + self.pair().0.as_ref() + } + } + + /// The absolute and normalized path to the file _within_ the project or + /// package. + pub fn path(&self) -> &'static Path { + if self.is_detached() { + Path::new("/detached.typ") + } else { + &self.pair().1 + } + } + + /// Resolve a file location relative to this file. + pub fn join(self, path: &str) -> StrResult<Self> { + if self.is_detached() { + bail!("cannot access file system from here"); + } + + let package = self.package().cloned(); + let base = self.path(); + Ok(if let Some(parent) = base.parent() { + Self::new(package, &parent.join(path)) + } else { + Self::new(package, Path::new(path)) + }) + } + + /// Construct from a raw number. + pub(crate) const fn from_u16(v: u16) -> Self { + Self(v) + } + + /// Extract the raw underlying number. + pub(crate) const fn as_u16(self) -> u16 { + self.0 + } + + /// Get the static pair. + fn pair(&self) -> Pair { + INTERNER.read().unwrap().from_id[usize::from(self.0)] + } +} + +impl Display for FileId { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let path = self.path().display(); + match self.package() { + Some(package) => write!(f, "{package}{path}"), + None => write!(f, "{path}"), + } + } +} + +impl Debug for FileId { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} + +/// Identifies a package. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct PackageSpec { + /// The namespace the package lives in. + pub namespace: EcoString, + /// The name of the package within its namespace. + pub name: EcoString, + /// The package's version. + pub version: Version, +} + +impl FromStr for PackageSpec { + type Err = EcoString; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let mut s = unscanny::Scanner::new(s); + if !s.eat_if('@') { + bail!("package specification must start with '@'"); + } + + let namespace = s.eat_until('/'); + if namespace.is_empty() { + bail!("package specification is missing namespace"); + } else if !is_ident(namespace) { + bail!("`{namespace}` is not a valid package namespace"); + } + + s.eat_if('/'); + + let name = s.eat_until(':'); + if name.is_empty() { + bail!("package specification is missing name"); + } else if !is_ident(name) { + bail!("`{name}` is not a valid package name"); + } + + s.eat_if(':'); + + let version = s.after(); + if version.is_empty() { + bail!("package specification is missing version"); + } + + Ok(Self { + namespace: namespace.into(), + name: name.into(), + version: version.parse()?, + }) + } +} + +impl Display for PackageSpec { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "@{}/{}:{}", self.namespace, self.name, self.version) + } +} + +/// A package's version. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Version { + /// The package's major version. + pub major: u32, + /// The package's minor version. + pub minor: u32, + /// The package's patch version. + pub patch: u32, +} + +impl FromStr for Version { + type Err = EcoString; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let mut parts = s.split('.'); + let mut next = |kind| { + let Some(part) = parts.next().filter(|s| !s.is_empty()) else { + bail!("version number is missing {kind} version"); + }; + part.parse::<u32>() + .map_err(|_| eco_format!("`{part}` is not a valid {kind} version")) + }; + + let major = next("major")?; + let minor = next("minor")?; + let patch = next("patch")?; + if let Some(rest) = parts.next() { + bail!("version number has unexpected fourth component: `{rest}`"); + } + + Ok(Self { major, minor, patch }) + } +} + +impl Display for Version { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + +impl Serialize for Version { + fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> { + s.collect_str(self) + } +} + +impl<'de> Deserialize<'de> for Version { + fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> { + let string = EcoString::deserialize(d)?; + string.parse().map_err(serde::de::Error::custom) + } +} + +/// A parsed package manifest. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct PackageManifest { + /// Details about the package itself. + pub package: PackageInfo, +} + +impl PackageManifest { + /// Parse the manifest from raw bytes. + pub fn parse(bytes: &[u8]) -> StrResult<Self> { + let string = std::str::from_utf8(bytes).map_err(FileError::from)?; + toml::from_str(string).map_err(|err| { + eco_format!("package manifest is malformed: {}", err.message()) + }) + } + + /// Ensure that this manifest is indeed for the specified package. + pub fn validate(&self, spec: &PackageSpec) -> StrResult<()> { + if self.package.name != spec.name { + bail!("package manifest contains mismatched name `{}`", self.package.name); + } + + if self.package.version != spec.version { + bail!( + "package manifest contains mismatched version {}", + self.package.version + ); + } + + Ok(()) + } +} + +/// The `package` key in the manifest. +/// +/// More fields are specified, but they are not relevant to the compiler. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct PackageInfo { + /// The name of the package within its namespace. + pub name: EcoString, + /// The package's version. + pub version: Version, + /// The path of the entrypoint into the package. + pub entrypoint: EcoString, +} diff --git a/crates/typst/src/font/book.rs b/crates/typst/src/font/book.rs new file mode 100644 index 00000000..2b7742bf --- /dev/null +++ b/crates/typst/src/font/book.rs @@ -0,0 +1,546 @@ +use std::cmp::Reverse; +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; +use ttf_parser::{name_id, PlatformId, Tag}; +use unicode_segmentation::UnicodeSegmentation; + +use super::{Font, FontStretch, FontStyle, FontVariant, FontWeight}; + +/// Metadata about a collection of fonts. +#[derive(Default, Clone, Hash)] +pub struct FontBook { + /// Maps from lowercased family names to font indices. + families: BTreeMap<String, Vec<usize>>, + /// Metadata about each font in the collection. + infos: Vec<FontInfo>, +} + +impl FontBook { + /// Create a new, empty font book. + pub fn new() -> Self { + Self { families: BTreeMap::new(), infos: vec![] } + } + + /// Create a font book for a collection of fonts. + pub fn from_fonts<'a>(fonts: impl IntoIterator<Item = &'a Font>) -> Self { + let mut book = Self::new(); + for font in fonts { + book.push(font.info().clone()); + } + book + } + + /// Insert metadata into the font book. + pub fn push(&mut self, info: FontInfo) { + let index = self.infos.len(); + let family = info.family.to_lowercase(); + self.families.entry(family).or_default().push(index); + self.infos.push(info); + } + + /// Get the font info for the given index. + pub fn info(&self, index: usize) -> Option<&FontInfo> { + self.infos.get(index) + } + + /// An ordered iterator over all font families this book knows and details + /// about the fonts that are part of them. + pub fn families( + &self, + ) -> impl Iterator<Item = (&str, impl Iterator<Item = &FontInfo>)> + '_ { + // Since the keys are lowercased, we instead use the family field of the + // first face's info. + self.families.values().map(|ids| { + let family = self.infos[ids[0]].family.as_str(); + let infos = ids.iter().map(|&id| &self.infos[id]); + (family, infos) + }) + } + + /// Try to find a font from the given `family` that matches the given + /// `variant` as closely as possible. + /// + /// The `family` should be all lowercase. + pub fn select(&self, family: &str, variant: FontVariant) -> Option<usize> { + let ids = self.families.get(family)?; + self.find_best_variant(None, variant, ids.iter().copied()) + } + + /// Iterate over all variants of a family. + pub fn select_family(&self, family: &str) -> impl Iterator<Item = usize> + '_ { + self.families + .get(family) + .map(|vec| vec.as_slice()) + .unwrap_or_default() + .iter() + .copied() + } + + /// Try to find and load a fallback font that + /// - is as close as possible to the font `like` (if any) + /// - is as close as possible to the given `variant` + /// - is suitable for shaping the given `text` + pub fn select_fallback( + &self, + like: Option<&FontInfo>, + variant: FontVariant, + text: &str, + ) -> Option<usize> { + // Find the fonts that contain the text's first char ... + let c = text.chars().next()?; + let ids = self + .infos + .iter() + .enumerate() + .filter(|(_, info)| info.coverage.contains(c as u32)) + .map(|(index, _)| index); + + // ... and find the best variant among them. + self.find_best_variant(like, variant, ids) + } + + /// Find the font in the passed iterator that + /// - is closest to the font `like` (if any) + /// - is closest to the given `variant` + /// + /// To do that we compute a key for all variants and select the one with the + /// minimal key. This key prioritizes: + /// - If `like` is some other font: + /// - Are both fonts (not) monospaced? + /// - Do both fonts (not) have serifs? + /// - How many words do the families share in their prefix? E.g. "Noto + /// Sans" and "Noto Sans Arabic" share two words, whereas "IBM Plex + /// Arabic" shares none with "Noto Sans", so prefer "Noto Sans Arabic" + /// if `like` is "Noto Sans". In case there are two equally good + /// matches, we prefer the shorter one because it is less special (e.g. + /// if `like` is "Noto Sans Arabic", we prefer "Noto Sans" over "Noto + /// Sans CJK HK".) + /// - The style (normal / italic / oblique). If we want italic or oblique + /// but it doesn't exist, the other one of the two is still better than + /// normal. + /// - The absolute distance to the target stretch. + /// - The absolute distance to the target weight. + fn find_best_variant( + &self, + like: Option<&FontInfo>, + variant: FontVariant, + ids: impl IntoIterator<Item = usize>, + ) -> Option<usize> { + let mut best = None; + let mut best_key = None; + + for id in ids { + let current = &self.infos[id]; + let key = ( + like.map(|like| { + ( + current.flags.contains(FontFlags::MONOSPACE) + != like.flags.contains(FontFlags::MONOSPACE), + current.flags.contains(FontFlags::SERIF) + != like.flags.contains(FontFlags::SERIF), + Reverse(shared_prefix_words(¤t.family, &like.family)), + current.family.len(), + ) + }), + current.variant.style.distance(variant.style), + current.variant.stretch.distance(variant.stretch), + current.variant.weight.distance(variant.weight), + ); + + if best_key.map_or(true, |b| key < b) { + best = Some(id); + best_key = Some(key); + } + } + + best + } +} + +/// Properties of a single font. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct FontInfo { + /// The typographic font family this font is part of. + pub family: String, + /// Properties that distinguish this font from other fonts in the same + /// family. + pub variant: FontVariant, + /// Properties of the font. + pub flags: FontFlags, + /// The unicode coverage of the font. + pub coverage: Coverage, +} + +bitflags::bitflags! { + /// Bitflags describing characteristics of a font. + #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] + #[derive(Serialize, Deserialize)] + #[serde(transparent)] + pub struct FontFlags: u32 { + /// All glyphs have the same width. + const MONOSPACE = 1 << 0; + /// Glyphs have short strokes at their stems. + const SERIF = 1 << 1; + } +} + +impl FontInfo { + /// Compute metadata for all fonts in the given data. + pub fn iter(data: &[u8]) -> impl Iterator<Item = FontInfo> + '_ { + let count = ttf_parser::fonts_in_collection(data).unwrap_or(1); + (0..count).filter_map(move |index| { + let ttf = ttf_parser::Face::parse(data, index).ok()?; + Self::from_ttf(&ttf) + }) + } + + /// Compute metadata for a single ttf-parser face. + pub(super) fn from_ttf(ttf: &ttf_parser::Face) -> Option<Self> { + // We cannot use Name ID 16 "Typographic Family", because for some + // fonts it groups together more than just Style / Weight / Stretch + // variants (e.g. Display variants of Noto fonts) and then some + // variants become inaccessible from Typst. And even though the + // fsSelection bit WWS should help us decide whether that is the + // case, it's wrong for some fonts (e.g. for certain variants of "Noto + // Sans Display"). + // + // So, instead we use Name ID 1 "Family" and trim many common + // suffixes for which know that they just describe styling (e.g. + // "ExtraBold"). + // + // Also, for Noto fonts we use Name ID 4 "Full Name" instead, + // because Name ID 1 "Family" sometimes contains "Display" and + // sometimes doesn't for the Display variants and that mixes things + // up. + let family = { + let mut family = find_name(ttf, name_id::FAMILY)?; + if family.starts_with("Noto") + || family.starts_with("NewCM") + || family.starts_with("NewComputerModern") + { + family = find_name(ttf, name_id::FULL_NAME)?; + } + typographic_family(&family).to_string() + }; + + let variant = { + let mut full = find_name(ttf, name_id::FULL_NAME).unwrap_or_default(); + full.make_ascii_lowercase(); + + // Some fonts miss the relevant bits for italic or oblique, so + // we also try to infer that from the full name. + let italic = ttf.is_italic() || full.contains("italic"); + let oblique = + ttf.is_oblique() || full.contains("oblique") || full.contains("slanted"); + + let style = match (italic, oblique) { + (false, false) => FontStyle::Normal, + (true, _) => FontStyle::Italic, + (_, true) => FontStyle::Oblique, + }; + + let weight = { + let mut number = ttf.weight().to_number(); + if (family.starts_with("NewCM") + || family.starts_with("New Computer Modern")) + && full.contains("book") + { + number += 50; + } + FontWeight::from_number(number) + }; + + let stretch = FontStretch::from_number(ttf.width().to_number()); + FontVariant { style, weight, stretch } + }; + + // Determine the unicode coverage. + let mut codepoints = vec![]; + for subtable in ttf.tables().cmap.into_iter().flat_map(|table| table.subtables) { + if subtable.is_unicode() { + subtable.codepoints(|c| codepoints.push(c)); + } + } + + let mut flags = FontFlags::empty(); + flags.set(FontFlags::MONOSPACE, ttf.is_monospaced()); + + // Determine whether this is a serif or sans-serif font. + if let Some(panose) = ttf + .raw_face() + .table(Tag::from_bytes(b"OS/2")) + .and_then(|os2| os2.get(32..45)) + { + if matches!(panose, [2, 2..=10, ..]) { + flags.insert(FontFlags::SERIF); + } + } + + Some(FontInfo { + family, + variant, + flags, + coverage: Coverage::from_vec(codepoints), + }) + } +} + +/// Try to find and decode the name with the given id. +pub(super) fn find_name(ttf: &ttf_parser::Face, name_id: u16) -> Option<String> { + ttf.names().into_iter().find_map(|entry| { + if entry.name_id == name_id { + if let Some(string) = entry.to_string() { + return Some(string); + } + + if entry.platform_id == PlatformId::Macintosh && entry.encoding_id == 0 { + return Some(decode_mac_roman(entry.name)); + } + } + + None + }) +} + +/// Decode mac roman encoded bytes into a string. +fn decode_mac_roman(coded: &[u8]) -> String { + #[rustfmt::skip] + const TABLE: [char; 128] = [ + 'Ä', 'Å', 'Ç', 'É', 'Ñ', 'Ö', 'Ü', 'á', 'à', 'â', 'ä', 'ã', 'å', 'ç', 'é', 'è', + 'ê', 'ë', 'í', 'ì', 'î', 'ï', 'ñ', 'ó', 'ò', 'ô', 'ö', 'õ', 'ú', 'ù', 'û', 'ü', + '†', '°', '¢', '£', '§', '•', '¶', 'ß', '®', '©', '™', '´', '¨', '≠', 'Æ', 'Ø', + '∞', '±', '≤', '≥', '¥', 'µ', '∂', '∑', '∏', 'π', '∫', 'ª', 'º', 'Ω', 'æ', 'ø', + '¿', '¡', '¬', '√', 'ƒ', '≈', '∆', '«', '»', '…', '\u{a0}', 'À', 'Ã', 'Õ', 'Œ', 'œ', + '–', '—', '“', '”', '‘', '’', '÷', '◊', 'ÿ', 'Ÿ', '⁄', '€', '‹', '›', 'fi', 'fl', + '‡', '·', '‚', '„', '‰', 'Â', 'Ê', 'Á', 'Ë', 'È', 'Í', 'Î', 'Ï', 'Ì', 'Ó', 'Ô', + '\u{f8ff}', 'Ò', 'Ú', 'Û', 'Ù', 'ı', 'ˆ', '˜', '¯', '˘', '˙', '˚', '¸', '˝', '˛', 'ˇ', + ]; + + fn char_from_mac_roman(code: u8) -> char { + if code < 128 { + code as char + } else { + TABLE[(code - 128) as usize] + } + } + + coded.iter().copied().map(char_from_mac_roman).collect() +} + +/// Trim style naming from a family name and fix bad names. +fn typographic_family(mut family: &str) -> &str { + // Separators between names, modifiers and styles. + const SEPARATORS: [char; 3] = [' ', '-', '_']; + + // Modifiers that can appear in combination with suffixes. + const MODIFIERS: &[&str] = + &["extra", "ext", "ex", "x", "semi", "sem", "sm", "demi", "dem", "ultra"]; + + // Style suffixes. + #[rustfmt::skip] + const SUFFIXES: &[&str] = &[ + "normal", "italic", "oblique", "slanted", + "thin", "th", "hairline", "light", "lt", "regular", "medium", "med", + "md", "bold", "bd", "demi", "extb", "black", "blk", "bk", "heavy", + "narrow", "condensed", "cond", "cn", "cd", "compressed", "expanded", "exp" + ]; + + let mut extra = [].as_slice(); + let newcm = family.starts_with("NewCM") || family.starts_with("NewComputerModern"); + if newcm { + extra = &["book"]; + } + + // Trim spacing and weird leading dots in Apple fonts. + family = family.trim().trim_start_matches('.'); + + // Lowercase the string so that the suffixes match case-insensitively. + let lower = family.to_ascii_lowercase(); + let mut len = usize::MAX; + let mut trimmed = lower.as_str(); + + // Trim style suffixes repeatedly. + while trimmed.len() < len { + len = trimmed.len(); + + // Find style suffix. + let mut t = trimmed; + let mut shortened = false; + while let Some(s) = SUFFIXES.iter().chain(extra).find_map(|s| t.strip_suffix(s)) { + shortened = true; + t = s; + } + + if !shortened { + break; + } + + // Strip optional separator. + if let Some(s) = t.strip_suffix(SEPARATORS) { + trimmed = s; + t = s; + } + + // Also allow an extra modifier, but apply it only if it is separated it + // from the text before it (to prevent false positives). + if let Some(t) = MODIFIERS.iter().find_map(|s| t.strip_suffix(s)) { + if let Some(stripped) = t.strip_suffix(SEPARATORS) { + trimmed = stripped; + } + } + } + + // Apply style suffix trimming. + family = &family[..len]; + + if newcm { + family = family.trim_end_matches("10"); + } + + // Fix bad names. + match family { + "Noto Sans Symbols2" => "Noto Sans Symbols 2", + "NewComputerModern" => "New Computer Modern", + "NewComputerModernMono" => "New Computer Modern Mono", + "NewComputerModernSans" => "New Computer Modern Sans", + "NewComputerModernMath" => "New Computer Modern Math", + "NewCMUncial" | "NewComputerModernUncial" => "New Computer Modern Uncial", + other => other, + } +} + +/// How many words the two strings share in their prefix. +fn shared_prefix_words(left: &str, right: &str) -> usize { + left.unicode_words() + .zip(right.unicode_words()) + .take_while(|(l, r)| l == r) + .count() +} + +/// A compactly encoded set of codepoints. +/// +/// The set is represented by alternating specifications of how many codepoints +/// are not in the set and how many are in the set. +/// +/// For example, for the set `{2, 3, 4, 9, 10, 11, 15, 18, 19}`, there are: +/// - 2 codepoints not inside (0, 1) +/// - 3 codepoints inside (2, 3, 4) +/// - 4 codepoints not inside (5, 6, 7, 8) +/// - 3 codepoints inside (9, 10, 11) +/// - 3 codepoints not inside (12, 13, 14) +/// - 1 codepoint inside (15) +/// - 2 codepoints not inside (16, 17) +/// - 2 codepoints inside (18, 19) +/// +/// So the resulting encoding is `[2, 3, 4, 3, 3, 1, 2, 2]`. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Coverage(Vec<u32>); + +impl Coverage { + /// Encode a vector of codepoints. + pub fn from_vec(mut codepoints: Vec<u32>) -> Self { + codepoints.sort(); + codepoints.dedup(); + + let mut runs = Vec::new(); + let mut next = 0; + + for c in codepoints { + if let Some(run) = runs.last_mut().filter(|_| c == next) { + *run += 1; + } else { + runs.push(c - next); + runs.push(1); + } + + next = c + 1; + } + + Self(runs) + } + + /// Whether the codepoint is covered. + pub fn contains(&self, c: u32) -> bool { + let mut inside = false; + let mut cursor = 0; + + for &run in &self.0 { + if (cursor..cursor + run).contains(&c) { + return inside; + } + cursor += run; + inside = !inside; + } + + false + } + + /// Iterate over all covered codepoints. + pub fn iter(&self) -> impl Iterator<Item = u32> + '_ { + let mut inside = false; + let mut cursor = 0; + self.0.iter().flat_map(move |run| { + let range = if inside { cursor..cursor + run } else { 0..0 }; + inside = !inside; + cursor += run; + range + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_trim_styles() { + assert_eq!(typographic_family("Atma Light"), "Atma"); + assert_eq!(typographic_family("eras bold"), "eras"); + assert_eq!(typographic_family("footlight mt light"), "footlight mt"); + assert_eq!(typographic_family("times new roman"), "times new roman"); + assert_eq!(typographic_family("noto sans mono cond sembd"), "noto sans mono"); + assert_eq!(typographic_family("noto serif SEMCOND sembd"), "noto serif"); + assert_eq!(typographic_family("crimson text"), "crimson text"); + assert_eq!(typographic_family("footlight light"), "footlight"); + assert_eq!(typographic_family("Noto Sans"), "Noto Sans"); + assert_eq!(typographic_family("Noto Sans Light"), "Noto Sans"); + assert_eq!(typographic_family("Noto Sans Semicondensed Heavy"), "Noto Sans"); + assert_eq!(typographic_family("Familx"), "Familx"); + assert_eq!(typographic_family("Font Ultra"), "Font Ultra"); + assert_eq!(typographic_family("Font Ultra Bold"), "Font"); + } + + #[test] + fn test_coverage() { + #[track_caller] + fn test(set: &[u32], runs: &[u32]) { + let coverage = Coverage::from_vec(set.to_vec()); + assert_eq!(coverage.0, runs); + + let max = 5 + set.iter().copied().max().unwrap_or_default(); + for c in 0..max { + assert_eq!(set.contains(&c), coverage.contains(c)); + } + } + + test(&[], &[]); + test(&[0], &[0, 1]); + test(&[1], &[1, 1]); + test(&[0, 1], &[0, 2]); + test(&[0, 1, 3], &[0, 2, 1, 1]); + test( + // {2, 3, 4, 9, 10, 11, 15, 18, 19} + &[18, 19, 2, 4, 9, 11, 15, 3, 3, 10], + &[2, 3, 4, 3, 3, 1, 2, 2], + ) + } + + #[test] + fn test_coverage_iter() { + let codepoints = vec![2, 3, 7, 8, 9, 14, 15, 19, 21]; + let coverage = Coverage::from_vec(codepoints.clone()); + assert_eq!(coverage.iter().collect::<Vec<_>>(), codepoints); + } +} diff --git a/crates/typst/src/font/mod.rs b/crates/typst/src/font/mod.rs new file mode 100644 index 00000000..2353e51c --- /dev/null +++ b/crates/typst/src/font/mod.rs @@ -0,0 +1,249 @@ +//! Font handling. + +mod book; +mod variant; + +pub use self::book::{Coverage, FontBook, FontFlags, FontInfo}; +pub use self::variant::{FontStretch, FontStyle, FontVariant, FontWeight}; + +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +use ttf_parser::GlyphId; + +use self::book::find_name; +use crate::eval::Cast; +use crate::geom::Em; +use crate::util::Bytes; + +/// An OpenType font. +/// +/// Values of this type are cheap to clone and hash. +#[derive(Clone)] +pub struct Font(Arc<Repr>); + +/// The internal representation of a font. +struct Repr { + /// The raw font data, possibly shared with other fonts from the same + /// collection. The vector's allocation must not move, because `ttf` points + /// into it using unsafe code. + data: Bytes, + /// The font's index in the buffer. + index: u32, + /// Metadata about the font. + info: FontInfo, + /// The font's metrics. + metrics: FontMetrics, + /// The underlying ttf-parser face. + ttf: ttf_parser::Face<'static>, + /// The underlying rustybuzz face. + rusty: rustybuzz::Face<'static>, +} + +impl Font { + /// Parse a font from data and collection index. + pub fn new(data: Bytes, index: u32) -> Option<Self> { + // Safety: + // - The slices's location is stable in memory: + // - We don't move the underlying vector + // - Nobody else can move it since we have a strong ref to the `Arc`. + // - The internal 'static lifetime is not leaked because its rewritten + // to the self-lifetime in `ttf()`. + let slice: &'static [u8] = + unsafe { std::slice::from_raw_parts(data.as_ptr(), data.len()) }; + + let ttf = ttf_parser::Face::parse(slice, index).ok()?; + let rusty = rustybuzz::Face::from_slice(slice, index)?; + let metrics = FontMetrics::from_ttf(&ttf); + let info = FontInfo::from_ttf(&ttf)?; + + Some(Self(Arc::new(Repr { data, index, info, metrics, ttf, rusty }))) + } + + /// Parse all fonts in the given data. + pub fn iter(data: Bytes) -> impl Iterator<Item = Self> { + let count = ttf_parser::fonts_in_collection(&data).unwrap_or(1); + (0..count).filter_map(move |index| Self::new(data.clone(), index)) + } + + /// The underlying buffer. + pub fn data(&self) -> &Bytes { + &self.0.data + } + + /// The font's index in the buffer. + pub fn index(&self) -> u32 { + self.0.index + } + + /// The font's metadata. + pub fn info(&self) -> &FontInfo { + &self.0.info + } + + /// The font's metrics. + pub fn metrics(&self) -> &FontMetrics { + &self.0.metrics + } + + /// The number of font units per one em. + pub fn units_per_em(&self) -> f64 { + self.0.metrics.units_per_em + } + + /// Convert from font units to an em length. + pub fn to_em(&self, units: impl Into<f64>) -> Em { + Em::from_units(units, self.units_per_em()) + } + + /// Look up the horizontal advance width of a glyph. + pub fn advance(&self, glyph: u16) -> Option<Em> { + self.0 + .ttf + .glyph_hor_advance(GlyphId(glyph)) + .map(|units| self.to_em(units)) + } + + /// Lookup a name by id. + pub fn find_name(&self, id: u16) -> Option<String> { + find_name(&self.0.ttf, id) + } + + /// A reference to the underlying `ttf-parser` face. + pub fn ttf(&self) -> &ttf_parser::Face<'_> { + // We can't implement Deref because that would leak the + // internal 'static lifetime. + &self.0.ttf + } + + /// A reference to the underlying `rustybuzz` face. + pub fn rusty(&self) -> &rustybuzz::Face<'_> { + // We can't implement Deref because that would leak the + // internal 'static lifetime. + &self.0.rusty + } +} + +impl Hash for Font { + fn hash<H: Hasher>(&self, state: &mut H) { + self.0.data.hash(state); + self.0.index.hash(state); + } +} + +impl Debug for Font { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Font({})", self.info().family) + } +} + +impl Eq for Font {} + +impl PartialEq for Font { + fn eq(&self, other: &Self) -> bool { + self.0.data == other.0.data && self.0.index == other.0.index + } +} + +/// Metrics of a font. +#[derive(Debug, Copy, Clone)] +pub struct FontMetrics { + /// How many font units represent one em unit. + pub units_per_em: f64, + /// The distance from the baseline to the typographic ascender. + pub ascender: Em, + /// The approximate height of uppercase letters. + pub cap_height: Em, + /// The approximate height of non-ascending lowercase letters. + pub x_height: Em, + /// The distance from the baseline to the typographic descender. + pub descender: Em, + /// Recommended metrics for a strikethrough line. + pub strikethrough: LineMetrics, + /// Recommended metrics for an underline. + pub underline: LineMetrics, + /// Recommended metrics for an overline. + pub overline: LineMetrics, +} + +impl FontMetrics { + /// Extract the font's metrics. + pub fn from_ttf(ttf: &ttf_parser::Face) -> Self { + let units_per_em = f64::from(ttf.units_per_em()); + let to_em = |units| Em::from_units(units, units_per_em); + + let ascender = to_em(ttf.typographic_ascender().unwrap_or(ttf.ascender())); + let cap_height = ttf.capital_height().filter(|&h| h > 0).map_or(ascender, to_em); + let x_height = ttf.x_height().filter(|&h| h > 0).map_or(ascender, to_em); + let descender = to_em(ttf.typographic_descender().unwrap_or(ttf.descender())); + let strikeout = ttf.strikeout_metrics(); + let underline = ttf.underline_metrics(); + + let strikethrough = LineMetrics { + position: strikeout.map_or(Em::new(0.25), |s| to_em(s.position)), + thickness: strikeout + .or(underline) + .map_or(Em::new(0.06), |s| to_em(s.thickness)), + }; + + let underline = LineMetrics { + position: underline.map_or(Em::new(-0.2), |s| to_em(s.position)), + thickness: underline + .or(strikeout) + .map_or(Em::new(0.06), |s| to_em(s.thickness)), + }; + + let overline = LineMetrics { + position: cap_height + Em::new(0.1), + thickness: underline.thickness, + }; + + Self { + units_per_em, + ascender, + cap_height, + x_height, + descender, + strikethrough, + underline, + overline, + } + } + + /// Look up a vertical metric. + pub fn vertical(&self, metric: VerticalFontMetric) -> Em { + match metric { + VerticalFontMetric::Ascender => self.ascender, + VerticalFontMetric::CapHeight => self.cap_height, + VerticalFontMetric::XHeight => self.x_height, + VerticalFontMetric::Baseline => Em::zero(), + VerticalFontMetric::Descender => self.descender, + } + } +} + +/// Metrics for a decorative line. +#[derive(Debug, Copy, Clone)] +pub struct LineMetrics { + /// The vertical offset of the line from the baseline. Positive goes + /// upwards, negative downwards. + pub position: Em, + /// The thickness of the line. + pub thickness: Em, +} + +/// Identifies a vertical metric of a font. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum VerticalFontMetric { + /// The font's ascender, which typically exceeds the height of all glyphs. + Ascender, + /// The approximate height of uppercase letters. + CapHeight, + /// The approximate height of non-ascending lowercase letters. + XHeight, + /// The baseline on which the letters rest. + Baseline, + /// The font's ascender, which typically exceeds the depth of all glyphs. + Descender, +} diff --git a/crates/typst/src/font/variant.rs b/crates/typst/src/font/variant.rs new file mode 100644 index 00000000..d4508a59 --- /dev/null +++ b/crates/typst/src/font/variant.rs @@ -0,0 +1,270 @@ +use std::fmt::{self, Debug, Formatter}; + +use serde::{Deserialize, Serialize}; + +use crate::eval::{cast, Cast, IntoValue}; +use crate::geom::Ratio; + +/// Properties that distinguish a font from other fonts in the same family. +#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[derive(Serialize, Deserialize)] +pub struct FontVariant { + /// The style of the font (normal / italic / oblique). + pub style: FontStyle, + /// How heavy the font is (100 - 900). + pub weight: FontWeight, + /// How condensed or expanded the font is (0.5 - 2.0). + pub stretch: FontStretch, +} + +impl FontVariant { + /// Create a variant from its three components. + pub fn new(style: FontStyle, weight: FontWeight, stretch: FontStretch) -> Self { + Self { style, weight, stretch } + } +} + +impl Debug for FontVariant { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{:?}-{:?}-{:?}", self.style, self.weight, self.stretch) + } +} + +/// The style of a font. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[derive(Serialize, Deserialize, Cast)] +#[serde(rename_all = "kebab-case")] +pub enum FontStyle { + /// The default, typically upright style. + Normal, + /// A cursive style with custom letterform. + Italic, + /// Just a slanted version of the normal style. + Oblique, +} + +impl FontStyle { + /// The conceptual distance between the styles, expressed as a number. + pub fn distance(self, other: Self) -> u16 { + if self == other { + 0 + } else if self != Self::Normal && other != Self::Normal { + 1 + } else { + 2 + } + } +} + +impl Default for FontStyle { + fn default() -> Self { + Self::Normal + } +} + +/// The weight of a font. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[derive(Serialize, Deserialize)] +#[serde(transparent)] +pub struct FontWeight(u16); + +impl FontWeight { + /// Thin weight (100). + pub const THIN: Self = Self(100); + + /// Extra light weight (200). + pub const EXTRALIGHT: Self = Self(200); + + /// Light weight (300). + pub const LIGHT: Self = Self(300); + + /// Regular weight (400). + pub const REGULAR: Self = Self(400); + + /// Medium weight (500). + pub const MEDIUM: Self = Self(500); + + /// Semibold weight (600). + pub const SEMIBOLD: Self = Self(600); + + /// Bold weight (700). + pub const BOLD: Self = Self(700); + + /// Extrabold weight (800). + pub const EXTRABOLD: Self = Self(800); + + /// Black weight (900). + pub const BLACK: Self = Self(900); + + /// Create a font weight from a number between 100 and 900, clamping it if + /// necessary. + pub fn from_number(weight: u16) -> Self { + Self(weight.max(100).min(900)) + } + + /// The number between 100 and 900. + pub fn to_number(self) -> u16 { + self.0 + } + + /// Add (or remove) weight, saturating at the boundaries of 100 and 900. + pub fn thicken(self, delta: i16) -> Self { + Self((self.0 as i16).saturating_add(delta).max(100).min(900) as u16) + } + + /// The absolute number distance between this and another font weight. + pub fn distance(self, other: Self) -> u16 { + (self.0 as i16 - other.0 as i16).unsigned_abs() + } +} + +impl Default for FontWeight { + fn default() -> Self { + Self::REGULAR + } +} + +impl Debug for FontWeight { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +cast! { + FontWeight, + self => IntoValue::into_value(match self { + FontWeight::THIN => "thin", + FontWeight::EXTRALIGHT => "extralight", + FontWeight::LIGHT => "light", + FontWeight::REGULAR => "regular", + FontWeight::MEDIUM => "medium", + FontWeight::SEMIBOLD => "semibold", + FontWeight::BOLD => "bold", + FontWeight::EXTRABOLD => "extrabold", + FontWeight::BLACK => "black", + _ => return self.to_number().into_value(), + }), + v: i64 => Self::from_number(v.clamp(0, u16::MAX as i64) as u16), + /// Thin weight (100). + "thin" => Self::THIN, + /// Extra light weight (200). + "extralight" => Self::EXTRALIGHT, + /// Light weight (300). + "light" => Self::LIGHT, + /// Regular weight (400). + "regular" => Self::REGULAR, + /// Medium weight (500). + "medium" => Self::MEDIUM, + /// Semibold weight (600). + "semibold" => Self::SEMIBOLD, + /// Bold weight (700). + "bold" => Self::BOLD, + /// Extrabold weight (800). + "extrabold" => Self::EXTRABOLD, + /// Black weight (900). + "black" => Self::BLACK, +} + +/// The width of a font. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[derive(Serialize, Deserialize)] +#[serde(transparent)] +pub struct FontStretch(u16); + +impl FontStretch { + /// Ultra-condensed stretch (50%). + pub const ULTRA_CONDENSED: Self = Self(500); + + /// Extra-condensed stretch weight (62.5%). + pub const EXTRA_CONDENSED: Self = Self(625); + + /// Condensed stretch (75%). + pub const CONDENSED: Self = Self(750); + + /// Semi-condensed stretch (87.5%). + pub const SEMI_CONDENSED: Self = Self(875); + + /// Normal stretch (100%). + pub const NORMAL: Self = Self(1000); + + /// Semi-expanded stretch (112.5%). + pub const SEMI_EXPANDED: Self = Self(1125); + + /// Expanded stretch (125%). + pub const EXPANDED: Self = Self(1250); + + /// Extra-expanded stretch (150%). + pub const EXTRA_EXPANDED: Self = Self(1500); + + /// Ultra-expanded stretch (200%). + pub const ULTRA_EXPANDED: Self = Self(2000); + + /// Create a font stretch from a ratio between 0.5 and 2.0, clamping it if + /// necessary. + pub fn from_ratio(ratio: Ratio) -> Self { + Self((ratio.get().max(0.5).min(2.0) * 1000.0) as u16) + } + + /// Create a font stretch from an OpenType-style number between 1 and 9, + /// clamping it if necessary. + pub fn from_number(stretch: u16) -> Self { + match stretch { + 0 | 1 => Self::ULTRA_CONDENSED, + 2 => Self::EXTRA_CONDENSED, + 3 => Self::CONDENSED, + 4 => Self::SEMI_CONDENSED, + 5 => Self::NORMAL, + 6 => Self::SEMI_EXPANDED, + 7 => Self::EXPANDED, + 8 => Self::EXTRA_EXPANDED, + _ => Self::ULTRA_EXPANDED, + } + } + + /// The ratio between 0.5 and 2.0 corresponding to this stretch. + pub fn to_ratio(self) -> Ratio { + Ratio::new(self.0 as f64 / 1000.0) + } + + /// The absolute ratio distance between this and another font stretch. + pub fn distance(self, other: Self) -> Ratio { + (self.to_ratio() - other.to_ratio()).abs() + } +} + +impl Default for FontStretch { + fn default() -> Self { + Self::NORMAL + } +} + +impl Debug for FontStretch { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.to_ratio().fmt(f) + } +} + +cast! { + FontStretch, + self => self.to_ratio().into_value(), + v: Ratio => Self::from_ratio(v), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_font_weight_distance() { + let d = |a, b| FontWeight(a).distance(FontWeight(b)); + assert_eq!(d(500, 200), 300); + assert_eq!(d(500, 500), 0); + assert_eq!(d(500, 900), 400); + assert_eq!(d(10, 100), 90); + } + + #[test] + fn test_font_stretch_debug() { + assert_eq!(format!("{:?}", FontStretch::EXPANDED), "125%") + } +} diff --git a/crates/typst/src/geom/abs.rs b/crates/typst/src/geom/abs.rs new file mode 100644 index 00000000..4ca3a9a1 --- /dev/null +++ b/crates/typst/src/geom/abs.rs @@ -0,0 +1,266 @@ +use super::*; + +/// An absolute length. +#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Abs(Scalar); + +impl Abs { + /// The zero length. + pub const fn zero() -> Self { + Self(Scalar(0.0)) + } + + /// The infinite length. + pub const fn inf() -> Self { + Self(Scalar(f64::INFINITY)) + } + + /// Create an absolute length from a number of raw units. + pub const fn raw(raw: f64) -> Self { + Self(Scalar(raw)) + } + + /// Create an absolute length from a value in a unit. + pub fn with_unit(val: f64, unit: AbsUnit) -> Self { + Self(Scalar(val * unit.raw_scale())) + } + + /// Create an absolute length from a number of points. + pub fn pt(pt: f64) -> Self { + Self::with_unit(pt, AbsUnit::Pt) + } + + /// Create an absolute length from a number of millimeters. + pub fn mm(mm: f64) -> Self { + Self::with_unit(mm, AbsUnit::Mm) + } + + /// Create an absolute length from a number of centimeters. + pub fn cm(cm: f64) -> Self { + Self::with_unit(cm, AbsUnit::Cm) + } + + /// Create an absolute length from a number of inches. + pub fn inches(inches: f64) -> Self { + Self::with_unit(inches, AbsUnit::In) + } + + /// Get the value of this absolute length in raw units. + pub const fn to_raw(self) -> f64 { + (self.0).0 + } + + /// Get the value of this absolute length in a unit. + pub fn to_unit(self, unit: AbsUnit) -> f64 { + self.to_raw() / unit.raw_scale() + } + + /// Convert this to a number of points. + pub fn to_pt(self) -> f64 { + self.to_unit(AbsUnit::Pt) + } + + /// Convert this to a number of millimeters. + pub fn to_mm(self) -> f64 { + self.to_unit(AbsUnit::Mm) + } + + /// Convert this to a number of centimeters. + pub fn to_cm(self) -> f64 { + self.to_unit(AbsUnit::Cm) + } + + /// Convert this to a number of inches. + pub fn to_inches(self) -> f64 { + self.to_unit(AbsUnit::In) + } + + /// The absolute value of this length. + pub fn abs(self) -> Self { + Self::raw(self.to_raw().abs()) + } + + /// The minimum of this and another absolute length. + pub fn min(self, other: Self) -> Self { + Self(self.0.min(other.0)) + } + + /// Set to the minimum of this and another absolute length. + pub fn set_min(&mut self, other: Self) { + *self = (*self).min(other); + } + + /// The maximum of this and another absolute length. + pub fn max(self, other: Self) -> Self { + Self(self.0.max(other.0)) + } + + /// Set to the maximum of this and another absolute length. + pub fn set_max(&mut self, other: Self) { + *self = (*self).max(other); + } + + /// Whether the other absolute length fits into this one (i.e. is smaller). + /// Allows for a bit of slack. + pub fn fits(self, other: Self) -> bool { + self.0 + 1e-6 >= other.0 + } + + /// Compares two absolute lengths for whether they are approximately equal. + pub fn approx_eq(self, other: Self) -> bool { + self == other || (self - other).to_raw().abs() < 1e-6 + } + + /// Perform a checked division by a number, returning zero if the result + /// is not finite. + pub fn safe_div(self, number: f64) -> Self { + let result = self.to_raw() / number; + if result.is_finite() { + Self::raw(result) + } else { + Self::zero() + } + } +} + +impl Numeric for Abs { + fn zero() -> Self { + Self::zero() + } + + fn is_finite(self) -> bool { + self.0.is_finite() + } +} + +impl Debug for Abs { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}pt", round_2(self.to_pt())) + } +} + +impl Neg for Abs { + type Output = Self; + + fn neg(self) -> Self { + Self(-self.0) + } +} + +impl Add for Abs { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self(self.0 + other.0) + } +} + +sub_impl!(Abs - Abs -> Abs); + +impl Mul<f64> for Abs { + type Output = Self; + + fn mul(self, other: f64) -> Self { + Self(self.0 * other) + } +} + +impl Mul<Abs> for f64 { + type Output = Abs; + + fn mul(self, other: Abs) -> Abs { + other * self + } +} + +impl Div<f64> for Abs { + type Output = Self; + + fn div(self, other: f64) -> Self { + Self(self.0 / other) + } +} + +impl Div for Abs { + type Output = f64; + + fn div(self, other: Self) -> f64 { + self.to_raw() / other.to_raw() + } +} + +assign_impl!(Abs += Abs); +assign_impl!(Abs -= Abs); +assign_impl!(Abs *= f64); +assign_impl!(Abs /= f64); + +impl Rem for Abs { + type Output = Self; + + fn rem(self, other: Self) -> Self::Output { + Self(self.0 % other.0) + } +} + +impl Sum for Abs { + fn sum<I: Iterator<Item = Self>>(iter: I) -> Self { + Self(iter.map(|s| s.0).sum()) + } +} + +impl<'a> Sum<&'a Self> for Abs { + fn sum<I: Iterator<Item = &'a Self>>(iter: I) -> Self { + Self(iter.map(|s| s.0).sum()) + } +} + +cast! { + Abs, + self => Value::Length(self.into()), +} + +/// Different units of absolute measurement. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub enum AbsUnit { + /// Points. + Pt, + /// Millimeters. + Mm, + /// Centimeters. + Cm, + /// Inches. + In, +} + +impl AbsUnit { + /// How many raw units correspond to a value of `1.0` in this unit. + fn raw_scale(self) -> f64 { + match self { + AbsUnit::Pt => 1.0, + AbsUnit::Mm => 2.83465, + AbsUnit::Cm => 28.3465, + AbsUnit::In => 72.0, + } + } +} + +impl Debug for AbsUnit { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(match self { + AbsUnit::Mm => "mm", + AbsUnit::Pt => "pt", + AbsUnit::Cm => "cm", + AbsUnit::In => "in", + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_length_unit_conversion() { + assert!((Abs::mm(150.0).to_cm() - 15.0) < 1e-4); + } +} diff --git a/crates/typst/src/geom/align.rs b/crates/typst/src/geom/align.rs new file mode 100644 index 00000000..47acd3a6 --- /dev/null +++ b/crates/typst/src/geom/align.rs @@ -0,0 +1,239 @@ +use super::*; + +/// Where to align something along an axis. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum Align { + /// Align at the left side. + Left, + /// Align in the horizontal middle. + Center, + /// Align at the right side. + Right, + /// Align at the top side. + Top, + /// Align in the vertical middle. + Horizon, + /// Align at the bottom side. + Bottom, +} + +impl Align { + /// Top-left alignment. + pub const LEFT_TOP: Axes<Self> = Axes { x: Align::Left, y: Align::Top }; + + /// Center-horizon alignment. + pub const CENTER_HORIZON: Axes<Self> = Axes { x: Align::Center, y: Align::Horizon }; + + /// The axis this alignment belongs to. + pub const fn axis(self) -> Axis { + match self { + Self::Left | Self::Center | Self::Right => Axis::X, + Self::Top | Self::Horizon | Self::Bottom => Axis::Y, + } + } + + /// The inverse alignment. + pub const fn inv(self) -> Self { + match self { + Self::Left => Self::Right, + Self::Center => Self::Center, + Self::Right => Self::Left, + Self::Top => Self::Bottom, + Self::Horizon => Self::Horizon, + Self::Bottom => Self::Top, + } + } + + /// Returns the position of this alignment in a container with the given + /// extent. + pub fn position(self, extent: Abs) -> Abs { + match self { + Self::Left | Self::Top => Abs::zero(), + Self::Center | Self::Horizon => extent / 2.0, + Self::Right | Self::Bottom => extent, + } + } +} + +impl From<Side> for Align { + fn from(side: Side) -> Self { + match side { + Side::Left => Self::Left, + Side::Top => Self::Top, + Side::Right => Self::Right, + Side::Bottom => Self::Bottom, + } + } +} + +impl Debug for Align { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(match self { + Self::Left => "left", + Self::Center => "center", + Self::Right => "right", + Self::Top => "top", + Self::Horizon => "horizon", + Self::Bottom => "bottom", + }) + } +} + +/// The generic alignment representation. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum GenAlign { + /// Align at the start side of the text direction. + Start, + /// Align at the end side of the text direction. + End, + /// Align at a specific alignment. + Specific(Align), +} + +impl GenAlign { + /// The axis this alignment belongs to. + pub const fn axis(self) -> Axis { + match self { + Self::Start | Self::End => Axis::X, + Self::Specific(align) => align.axis(), + } + } +} + +impl From<Align> for GenAlign { + fn from(align: Align) -> Self { + Self::Specific(align) + } +} + +impl From<HorizontalAlign> for GenAlign { + fn from(align: HorizontalAlign) -> Self { + align.0 + } +} + +impl From<VerticalAlign> for GenAlign { + fn from(align: VerticalAlign) -> Self { + align.0 + } +} + +impl Debug for GenAlign { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Start => f.pad("start"), + Self::End => f.pad("end"), + Self::Specific(align) => align.fmt(f), + } + } +} + +cast! { + type GenAlign: "alignment", +} + +cast! { + type Axes<GenAlign>: "2d alignment", +} + +cast! { + Axes<Align>, + self => self.map(GenAlign::from).into_value(), +} + +cast! { + Axes<Option<GenAlign>>, + self => match (self.x, self.y) { + (Some(x), Some(y)) => Axes::new(x, y).into_value(), + (Some(x), None) => x.into_value(), + (None, Some(y)) => y.into_value(), + (None, None) => Value::None, + }, + align: GenAlign => { + let mut aligns = Axes::default(); + aligns.set(align.axis(), Some(align)); + aligns + }, + aligns: Axes<GenAlign> => aligns.map(Some), +} + +impl From<Axes<GenAlign>> for Axes<Option<GenAlign>> { + fn from(axes: Axes<GenAlign>) -> Self { + axes.map(Some) + } +} + +impl From<Axes<Align>> for Axes<Option<GenAlign>> { + fn from(axes: Axes<Align>) -> Self { + axes.map(GenAlign::Specific).into() + } +} + +impl From<Align> for Axes<Option<GenAlign>> { + fn from(align: Align) -> Self { + let mut axes = Axes::splat(None); + axes.set(align.axis(), Some(align.into())); + axes + } +} + +impl Resolve for GenAlign { + type Output = Align; + + fn resolve(self, styles: StyleChain) -> Self::Output { + let dir = item!(dir)(styles); + match self { + Self::Start => dir.start().into(), + Self::End => dir.end().into(), + Self::Specific(align) => align, + } + } +} + +impl Fold for GenAlign { + type Output = Self; + + fn fold(self, _: Self::Output) -> Self::Output { + self + } +} + +impl Fold for Align { + type Output = Self; + + fn fold(self, _: Self::Output) -> Self::Output { + self + } +} + +/// Utility struct to restrict a passed alignment value to the horizontal axis +/// on cast. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct HorizontalAlign(pub GenAlign); + +cast! { + HorizontalAlign, + self => self.0.into_value(), + align: GenAlign => { + if align.axis() != Axis::X { + bail!("alignment must be horizontal"); + } + Self(align) + }, +} + +/// Utility struct to restrict a passed alignment value to the vertical axis on +/// cast. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct VerticalAlign(pub GenAlign); + +cast! { + VerticalAlign, + self => self.0.into_value(), + align: GenAlign => { + if align.axis() != Axis::Y { + bail!("alignment must be vertical"); + } + Self(align) + }, +} diff --git a/crates/typst/src/geom/angle.rs b/crates/typst/src/geom/angle.rs new file mode 100644 index 00000000..c03810d9 --- /dev/null +++ b/crates/typst/src/geom/angle.rs @@ -0,0 +1,188 @@ +use super::*; + +/// An angle. +#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Angle(Scalar); + +impl Angle { + /// The zero angle. + pub const fn zero() -> Self { + Self(Scalar(0.0)) + } + + /// Create an angle from a number of raw units. + pub const fn raw(raw: f64) -> Self { + Self(Scalar(raw)) + } + + /// Create an angle from a value in a unit. + pub fn with_unit(val: f64, unit: AngleUnit) -> Self { + Self(Scalar(val * unit.raw_scale())) + } + + /// Create an angle from a number of radians. + pub fn rad(rad: f64) -> Self { + Self::with_unit(rad, AngleUnit::Rad) + } + + /// Create an angle from a number of degrees. + pub fn deg(deg: f64) -> Self { + Self::with_unit(deg, AngleUnit::Deg) + } + + /// Get the value of this angle in raw units. + pub const fn to_raw(self) -> f64 { + (self.0).0 + } + + /// Get the value of this angle in a unit. + pub fn to_unit(self, unit: AngleUnit) -> f64 { + self.to_raw() / unit.raw_scale() + } + + /// Convert this to a number of radians. + pub fn to_rad(self) -> f64 { + self.to_unit(AngleUnit::Rad) + } + + /// Convert this to a number of degrees. + pub fn to_deg(self) -> f64 { + self.to_unit(AngleUnit::Deg) + } + + /// The absolute value of the this angle. + pub fn abs(self) -> Self { + Self::raw(self.to_raw().abs()) + } + + /// Get the sine of this angle in radians. + pub fn sin(self) -> f64 { + self.to_rad().sin() + } + + /// Get the cosine of this angle in radians. + pub fn cos(self) -> f64 { + self.to_rad().cos() + } + + /// Get the tangent of this angle in radians. + pub fn tan(self) -> f64 { + self.to_rad().tan() + } +} + +impl Numeric for Angle { + fn zero() -> Self { + Self::zero() + } + + fn is_finite(self) -> bool { + self.0.is_finite() + } +} + +impl Debug for Angle { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}deg", round_2(self.to_deg())) + } +} + +impl Neg for Angle { + type Output = Self; + + fn neg(self) -> Self { + Self(-self.0) + } +} + +impl Add for Angle { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self(self.0 + other.0) + } +} + +sub_impl!(Angle - Angle -> Angle); + +impl Mul<f64> for Angle { + type Output = Self; + + fn mul(self, other: f64) -> Self { + Self(self.0 * other) + } +} + +impl Mul<Angle> for f64 { + type Output = Angle; + + fn mul(self, other: Angle) -> Angle { + other * self + } +} + +impl Div<f64> for Angle { + type Output = Self; + + fn div(self, other: f64) -> Self { + Self(self.0 / other) + } +} + +impl Div for Angle { + type Output = f64; + + fn div(self, other: Self) -> f64 { + self.to_raw() / other.to_raw() + } +} + +assign_impl!(Angle += Angle); +assign_impl!(Angle -= Angle); +assign_impl!(Angle *= f64); +assign_impl!(Angle /= f64); + +impl Sum for Angle { + fn sum<I: Iterator<Item = Angle>>(iter: I) -> Self { + Self(iter.map(|s| s.0).sum()) + } +} + +/// Different units of angular measurement. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub enum AngleUnit { + /// Radians. + Rad, + /// Degrees. + Deg, +} + +impl AngleUnit { + /// How many raw units correspond to a value of `1.0` in this unit. + fn raw_scale(self) -> f64 { + match self { + Self::Rad => 1.0, + Self::Deg => PI / 180.0, + } + } +} + +impl Debug for AngleUnit { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(match self { + Self::Rad => "rad", + Self::Deg => "deg", + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_angle_unit_conversion() { + assert!((Angle::rad(2.0 * PI).to_deg() - 360.0) < 1e-4); + assert!((Angle::deg(45.0).to_rad() - std::f64::consts::FRAC_PI_4) < 1e-4); + } +} diff --git a/crates/typst/src/geom/axes.rs b/crates/typst/src/geom/axes.rs new file mode 100644 index 00000000..059d3bb2 --- /dev/null +++ b/crates/typst/src/geom/axes.rs @@ -0,0 +1,305 @@ +use std::any::Any; +use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Not}; + +use super::*; + +/// A container with a horizontal and vertical component. +#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Axes<T> { + /// The horizontal component. + pub x: T, + /// The vertical component. + pub y: T, +} + +impl<T> Axes<T> { + /// Create a new instance from the two components. + pub const fn new(x: T, y: T) -> Self { + Self { x, y } + } + + /// Create a new instance with two equal components. + pub fn splat(v: T) -> Self + where + T: Clone, + { + Self { x: v.clone(), y: v } + } + + /// Map the individual fields with `f`. + pub fn map<F, U>(self, mut f: F) -> Axes<U> + where + F: FnMut(T) -> U, + { + Axes { x: f(self.x), y: f(self.y) } + } + + /// Convert from `&Axes<T>` to `Axes<&T>`. + pub fn as_ref(&self) -> Axes<&T> { + Axes { x: &self.x, y: &self.y } + } + + /// Convert from `&Axes<T>` to `Axes<&<T as Deref>::Target>`. + pub fn as_deref(&self) -> Axes<&T::Target> + where + T: Deref, + { + Axes { x: &self.x, y: &self.y } + } + + /// Convert from `&mut Axes<T>` to `Axes<&mut T>`. + pub fn as_mut(&mut self) -> Axes<&mut T> { + Axes { x: &mut self.x, y: &mut self.y } + } + + /// Zip two instances into an instance over a tuple. + pub fn zip<U>(self, other: Axes<U>) -> Axes<(T, U)> { + Axes { x: (self.x, other.x), y: (self.y, other.y) } + } + + /// Whether a condition is true for at least one of fields. + pub fn any<F>(self, mut f: F) -> bool + where + F: FnMut(&T) -> bool, + { + f(&self.x) || f(&self.y) + } + + /// Whether a condition is true for both fields. + pub fn all<F>(self, mut f: F) -> bool + where + F: FnMut(&T) -> bool, + { + f(&self.x) && f(&self.y) + } + + /// Filter the individual fields with a mask. + pub fn filter(self, mask: Axes<bool>) -> Axes<Option<T>> { + Axes { + x: if mask.x { Some(self.x) } else { None }, + y: if mask.y { Some(self.y) } else { None }, + } + } +} + +impl<T: Default> Axes<T> { + /// Create a new instance with y set to its default value. + pub fn with_x(x: T) -> Self { + Self { x, y: T::default() } + } + + /// Create a new instance with x set to its default value. + pub fn with_y(y: T) -> Self { + Self { x: T::default(), y } + } +} + +impl<T: Ord> Axes<T> { + /// The component-wise minimum of this and another instance. + pub fn min(self, other: Self) -> Self { + Self { x: self.x.min(other.x), y: self.y.min(other.y) } + } + + /// The component-wise minimum of this and another instance. + pub fn max(self, other: Self) -> Self { + Self { x: self.x.max(other.x), y: self.y.max(other.y) } + } + + /// The minimum of width and height. + pub fn min_by_side(self) -> T { + self.x.min(self.y) + } + + /// The minimum of width and height. + pub fn max_by_side(self) -> T { + self.x.max(self.y) + } +} + +impl<T> Get<Axis> for Axes<T> { + type Component = T; + + fn get_ref(&self, axis: Axis) -> &T { + match axis { + Axis::X => &self.x, + Axis::Y => &self.y, + } + } + + fn get_mut(&mut self, axis: Axis) -> &mut T { + match axis { + Axis::X => &mut self.x, + Axis::Y => &mut self.y, + } + } +} + +impl<T> Debug for Axes<T> +where + T: Debug + 'static, +{ + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if let Axes { x: Some(x), y: Some(y) } = + self.as_ref().map(|v| (v as &dyn Any).downcast_ref::<GenAlign>()) + { + write!(f, "{:?} + {:?}", x, y) + } else if (&self.x as &dyn Any).is::<Abs>() { + write!(f, "Size({:?}, {:?})", self.x, self.y) + } else { + write!(f, "Axes({:?}, {:?})", self.x, self.y) + } + } +} + +/// The two layouting axes. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub enum Axis { + /// The horizontal axis. + X, + /// The vertical axis. + Y, +} + +impl Axis { + /// The direction with the given positivity for this axis. + pub fn dir(self, positive: bool) -> Dir { + match (self, positive) { + (Self::X, true) => Dir::LTR, + (Self::X, false) => Dir::RTL, + (Self::Y, true) => Dir::TTB, + (Self::Y, false) => Dir::BTT, + } + } + + /// The other axis. + pub fn other(self) -> Self { + match self { + Self::X => Self::Y, + Self::Y => Self::X, + } + } +} + +impl Debug for Axis { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(match self { + Self::X => "horizontal", + Self::Y => "vertical", + }) + } +} + +impl<T> Axes<Option<T>> { + /// Unwrap the individual fields. + pub fn unwrap_or(self, other: Axes<T>) -> Axes<T> { + Axes { + x: self.x.unwrap_or(other.x), + y: self.y.unwrap_or(other.y), + } + } +} + +impl<T> Axes<Smart<T>> { + /// Unwrap the individual fields. + pub fn unwrap_or(self, other: Axes<T>) -> Axes<T> { + Axes { + x: self.x.unwrap_or(other.x), + y: self.y.unwrap_or(other.y), + } + } +} + +impl Axes<bool> { + /// Select `t.x` if `self.x` is true and `f.x` otherwise and same for `y`. + pub fn select<T>(self, t: Axes<T>, f: Axes<T>) -> Axes<T> { + Axes { + x: if self.x { t.x } else { f.x }, + y: if self.y { t.y } else { f.y }, + } + } +} + +impl Not for Axes<bool> { + type Output = Self; + + fn not(self) -> Self::Output { + Self { x: !self.x, y: !self.y } + } +} + +impl BitOr for Axes<bool> { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + Self { x: self.x | rhs.x, y: self.y | rhs.y } + } +} + +impl BitOr<bool> for Axes<bool> { + type Output = Self; + + fn bitor(self, rhs: bool) -> Self::Output { + Self { x: self.x | rhs, y: self.y | rhs } + } +} + +impl BitAnd for Axes<bool> { + type Output = Self; + + fn bitand(self, rhs: Self) -> Self::Output { + Self { x: self.x & rhs.x, y: self.y & rhs.y } + } +} + +impl BitAnd<bool> for Axes<bool> { + type Output = Self; + + fn bitand(self, rhs: bool) -> Self::Output { + Self { x: self.x & rhs, y: self.y & rhs } + } +} + +impl BitOrAssign for Axes<bool> { + fn bitor_assign(&mut self, rhs: Self) { + self.x |= rhs.x; + self.y |= rhs.y; + } +} + +impl BitAndAssign for Axes<bool> { + fn bitand_assign(&mut self, rhs: Self) { + self.x &= rhs.x; + self.y &= rhs.y; + } +} + +cast! { + Axes<Rel<Length>>, + self => array![self.x, self.y].into_value(), + array: Array => { + let mut iter = array.into_iter(); + match (iter.next(), iter.next(), iter.next()) { + (Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?), + _ => bail!("point array must contain exactly two entries"), + } + }, +} + +impl<T: Resolve> Resolve for Axes<T> { + type Output = Axes<T::Output>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.map(|v| v.resolve(styles)) + } +} + +impl<T: Fold> Fold for Axes<Option<T>> { + type Output = Axes<T::Output>; + + fn fold(self, outer: Self::Output) -> Self::Output { + self.zip(outer).map(|(inner, outer)| match inner { + Some(value) => value.fold(outer), + None => outer, + }) + } +} diff --git a/crates/typst/src/geom/color.rs b/crates/typst/src/geom/color.rs new file mode 100644 index 00000000..c7676c2b --- /dev/null +++ b/crates/typst/src/geom/color.rs @@ -0,0 +1,386 @@ +use std::str::FromStr; + +use super::*; + +/// A color in a dynamic format. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub enum Color { + /// An 8-bit luma color. + Luma(LumaColor), + /// An 8-bit RGBA color. + Rgba(RgbaColor), + /// An 8-bit CMYK color. + Cmyk(CmykColor), +} + +impl Color { + pub const BLACK: Self = Self::Rgba(RgbaColor::new(0x00, 0x00, 0x00, 0xFF)); + pub const GRAY: Self = Self::Rgba(RgbaColor::new(0xAA, 0xAA, 0xAA, 0xFF)); + pub const SILVER: Self = Self::Rgba(RgbaColor::new(0xDD, 0xDD, 0xDD, 0xFF)); + pub const WHITE: Self = Self::Rgba(RgbaColor::new(0xFF, 0xFF, 0xFF, 0xFF)); + pub const NAVY: Self = Self::Rgba(RgbaColor::new(0x00, 0x1f, 0x3f, 0xFF)); + pub const BLUE: Self = Self::Rgba(RgbaColor::new(0x00, 0x74, 0xD9, 0xFF)); + pub const AQUA: Self = Self::Rgba(RgbaColor::new(0x7F, 0xDB, 0xFF, 0xFF)); + pub const TEAL: Self = Self::Rgba(RgbaColor::new(0x39, 0xCC, 0xCC, 0xFF)); + pub const EASTERN: Self = Self::Rgba(RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF)); + pub const PURPLE: Self = Self::Rgba(RgbaColor::new(0xB1, 0x0D, 0xC9, 0xFF)); + pub const FUCHSIA: Self = Self::Rgba(RgbaColor::new(0xF0, 0x12, 0xBE, 0xFF)); + pub const MAROON: Self = Self::Rgba(RgbaColor::new(0x85, 0x14, 0x4b, 0xFF)); + pub const RED: Self = Self::Rgba(RgbaColor::new(0xFF, 0x41, 0x36, 0xFF)); + pub const ORANGE: Self = Self::Rgba(RgbaColor::new(0xFF, 0x85, 0x1B, 0xFF)); + pub const YELLOW: Self = Self::Rgba(RgbaColor::new(0xFF, 0xDC, 0x00, 0xFF)); + pub const OLIVE: Self = Self::Rgba(RgbaColor::new(0x3D, 0x99, 0x70, 0xFF)); + pub const GREEN: Self = Self::Rgba(RgbaColor::new(0x2E, 0xCC, 0x40, 0xFF)); + pub const LIME: Self = Self::Rgba(RgbaColor::new(0x01, 0xFF, 0x70, 0xFF)); + + /// Convert this color to RGBA. + pub fn to_rgba(self) -> RgbaColor { + match self { + Self::Luma(luma) => luma.to_rgba(), + Self::Rgba(rgba) => rgba, + Self::Cmyk(cmyk) => cmyk.to_rgba(), + } + } + + /// Lighten this color by the given factor. + pub fn lighten(self, factor: Ratio) -> Self { + match self { + Self::Luma(luma) => Self::Luma(luma.lighten(factor)), + Self::Rgba(rgba) => Self::Rgba(rgba.lighten(factor)), + Self::Cmyk(cmyk) => Self::Cmyk(cmyk.lighten(factor)), + } + } + + /// Darken this color by the given factor. + pub fn darken(self, factor: Ratio) -> Self { + match self { + Self::Luma(luma) => Self::Luma(luma.darken(factor)), + Self::Rgba(rgba) => Self::Rgba(rgba.darken(factor)), + Self::Cmyk(cmyk) => Self::Cmyk(cmyk.darken(factor)), + } + } + + /// Negate this color. + pub fn negate(self) -> Self { + match self { + Self::Luma(luma) => Self::Luma(luma.negate()), + Self::Rgba(rgba) => Self::Rgba(rgba.negate()), + Self::Cmyk(cmyk) => Self::Cmyk(cmyk.negate()), + } + } +} + +impl Debug for Color { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Luma(c) => Debug::fmt(c, f), + Self::Rgba(c) => Debug::fmt(c, f), + Self::Cmyk(c) => Debug::fmt(c, f), + } + } +} + +/// An 8-bit grayscale color. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct LumaColor(pub u8); + +impl LumaColor { + /// Construct a new luma color. + pub const fn new(luma: u8) -> Self { + Self(luma) + } + + /// Convert to an opque RGBA color. + pub const fn to_rgba(self) -> RgbaColor { + RgbaColor::new(self.0, self.0, self.0, u8::MAX) + } + + /// Convert to CMYK as a fraction of true black. + pub fn to_cmyk(self) -> CmykColor { + CmykColor::new( + round_u8(self.0 as f64 * 0.75), + round_u8(self.0 as f64 * 0.68), + round_u8(self.0 as f64 * 0.67), + round_u8(self.0 as f64 * 0.90), + ) + } + + /// Lighten this color by a factor. + pub fn lighten(self, factor: Ratio) -> Self { + let inc = round_u8((u8::MAX - self.0) as f64 * factor.get()); + Self(self.0.saturating_add(inc)) + } + + /// Darken this color by a factor. + pub fn darken(self, factor: Ratio) -> Self { + let dec = round_u8(self.0 as f64 * factor.get()); + Self(self.0.saturating_sub(dec)) + } + + /// Negate this color. + pub fn negate(self) -> Self { + Self(u8::MAX - self.0) + } +} + +impl Debug for LumaColor { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "luma({})", self.0) + } +} + +impl From<LumaColor> for Color { + fn from(luma: LumaColor) -> Self { + Self::Luma(luma) + } +} + +/// An 8-bit RGBA color. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct RgbaColor { + /// Red channel. + pub r: u8, + /// Green channel. + pub g: u8, + /// Blue channel. + pub b: u8, + /// Alpha channel. + pub a: u8, +} + +impl RgbaColor { + /// Construct a new RGBA color. + pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self { + Self { r, g, b, a } + } + + /// Lighten this color by a factor. + /// + /// The alpha channel is not affected. + pub fn lighten(self, factor: Ratio) -> Self { + let lighten = + |c: u8| c.saturating_add(round_u8((u8::MAX - c) as f64 * factor.get())); + Self { + r: lighten(self.r), + g: lighten(self.g), + b: lighten(self.b), + a: self.a, + } + } + + /// Darken this color by a factor. + /// + /// The alpha channel is not affected. + pub fn darken(self, factor: Ratio) -> Self { + let darken = |c: u8| c.saturating_sub(round_u8(c as f64 * factor.get())); + Self { + r: darken(self.r), + g: darken(self.g), + b: darken(self.b), + a: self.a, + } + } + + /// Negate this color. + /// + /// The alpha channel is not affected. + pub fn negate(self) -> Self { + Self { + r: u8::MAX - self.r, + g: u8::MAX - self.g, + b: u8::MAX - self.b, + a: self.a, + } + } +} + +impl FromStr for RgbaColor { + type Err = &'static str; + + /// Constructs a new color from hex strings like the following: + /// - `#aef` (shorthand, with leading hashtag), + /// - `7a03c2` (without alpha), + /// - `abcdefff` (with alpha). + /// + /// The hashtag is optional and both lower and upper case are fine. + fn from_str(hex_str: &str) -> Result<Self, Self::Err> { + let hex_str = hex_str.strip_prefix('#').unwrap_or(hex_str); + if hex_str.chars().any(|c| !c.is_ascii_hexdigit()) { + return Err("color string contains non-hexadecimal letters"); + } + + let len = hex_str.len(); + let long = len == 6 || len == 8; + let short = len == 3 || len == 4; + let alpha = len == 4 || len == 8; + if !long && !short { + return Err("color string has wrong length"); + } + + let mut values: [u8; 4] = [u8::MAX; 4]; + for elem in if alpha { 0..4 } else { 0..3 } { + let item_len = if long { 2 } else { 1 }; + let pos = elem * item_len; + + let item = &hex_str[pos..(pos + item_len)]; + values[elem] = u8::from_str_radix(item, 16).unwrap(); + + if short { + // Duplicate number for shorthand notation, i.e. `a` -> `aa` + values[elem] += values[elem] * 16; + } + } + + Ok(Self::new(values[0], values[1], values[2], values[3])) + } +} + +impl Debug for RgbaColor { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if f.alternate() { + write!(f, "rgba({}, {}, {}, {})", self.r, self.g, self.b, self.a,)?; + } else { + write!(f, "rgb(\"#{:02x}{:02x}{:02x}", self.r, self.g, self.b)?; + if self.a != 255 { + write!(f, "{:02x}", self.a)?; + } + write!(f, "\")")?; + } + Ok(()) + } +} + +impl<T: Into<RgbaColor>> From<T> for Color { + fn from(rgba: T) -> Self { + Self::Rgba(rgba.into()) + } +} + +cast! { + RgbaColor, + self => Value::Color(self.into()), +} + +/// An 8-bit CMYK color. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct CmykColor { + /// The cyan component. + pub c: u8, + /// The magenta component. + pub m: u8, + /// The yellow component. + pub y: u8, + /// The key (black) component. + pub k: u8, +} + +impl CmykColor { + /// Construct a new CMYK color. + pub const fn new(c: u8, m: u8, y: u8, k: u8) -> Self { + Self { c, m, y, k } + } + + /// Convert this color to RGBA. + pub fn to_rgba(self) -> RgbaColor { + let k = self.k as f64 / 255.0; + let f = |c| { + let c = c as f64 / 255.0; + round_u8(255.0 * (1.0 - c) * (1.0 - k)) + }; + + RgbaColor { r: f(self.c), g: f(self.m), b: f(self.y), a: 255 } + } + + /// Lighten this color by a factor. + pub fn lighten(self, factor: Ratio) -> Self { + let lighten = |c: u8| c.saturating_sub(round_u8(c as f64 * factor.get())); + Self { + c: lighten(self.c), + m: lighten(self.m), + y: lighten(self.y), + k: lighten(self.k), + } + } + + /// Darken this color by a factor. + pub fn darken(self, factor: Ratio) -> Self { + let darken = + |c: u8| c.saturating_add(round_u8((u8::MAX - c) as f64 * factor.get())); + Self { + c: darken(self.c), + m: darken(self.m), + y: darken(self.y), + k: darken(self.k), + } + } + + /// Negate this color. + /// + /// Does not affect the key component. + pub fn negate(self) -> Self { + Self { + c: u8::MAX - self.c, + m: u8::MAX - self.m, + y: u8::MAX - self.y, + k: self.k, + } + } +} + +impl Debug for CmykColor { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let g = |c| 100.0 * (c as f64 / 255.0); + write!( + f, + "cmyk({:.1}%, {:.1}%, {:.1}%, {:.1}%)", + g(self.c), + g(self.m), + g(self.y), + g(self.k), + ) + } +} + +impl From<CmykColor> for Color { + fn from(cmyk: CmykColor) -> Self { + Self::Cmyk(cmyk) + } +} + +/// Convert to the closest u8. +fn round_u8(value: f64) -> u8 { + value.round() as u8 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_color_strings() { + #[track_caller] + fn test(hex: &str, r: u8, g: u8, b: u8, a: u8) { + assert_eq!(RgbaColor::from_str(hex), Ok(RgbaColor::new(r, g, b, a))); + } + + test("f61243ff", 0xf6, 0x12, 0x43, 0xff); + test("b3d8b3", 0xb3, 0xd8, 0xb3, 0xff); + test("fCd2a9AD", 0xfc, 0xd2, 0xa9, 0xad); + test("233", 0x22, 0x33, 0x33, 0xff); + test("111b", 0x11, 0x11, 0x11, 0xbb); + } + + #[test] + fn test_parse_invalid_colors() { + #[track_caller] + fn test(hex: &str, message: &str) { + assert_eq!(RgbaColor::from_str(hex), Err(message)); + } + + test("a5", "color string has wrong length"); + test("12345", "color string has wrong length"); + test("f075ff011", "color string has wrong length"); + test("hmmm", "color string contains non-hexadecimal letters"); + test("14B2AH", "color string contains non-hexadecimal letters"); + } +} diff --git a/crates/typst/src/geom/corners.rs b/crates/typst/src/geom/corners.rs new file mode 100644 index 00000000..5ee1e063 --- /dev/null +++ b/crates/typst/src/geom/corners.rs @@ -0,0 +1,219 @@ +use crate::eval::{CastInfo, FromValue, IntoValue, Reflect}; + +use super::*; + +/// A container with components for the four corners of a rectangle. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Corners<T> { + /// The value for the top left corner. + pub top_left: T, + /// The value for the top right corner. + pub top_right: T, + /// The value for the bottom right corner. + pub bottom_right: T, + /// The value for the bottom left corner. + pub bottom_left: T, +} + +impl<T> Corners<T> { + /// Create a new instance from the four components. + pub const fn new(top_left: T, top_right: T, bottom_right: T, bottom_left: T) -> Self { + Self { top_left, top_right, bottom_right, bottom_left } + } + + /// Create an instance with four equal components. + pub fn splat(value: T) -> Self + where + T: Clone, + { + Self { + top_left: value.clone(), + top_right: value.clone(), + bottom_right: value.clone(), + bottom_left: value, + } + } + + /// Map the individual fields with `f`. + pub fn map<F, U>(self, mut f: F) -> Corners<U> + where + F: FnMut(T) -> U, + { + Corners { + top_left: f(self.top_left), + top_right: f(self.top_right), + bottom_right: f(self.bottom_right), + bottom_left: f(self.bottom_left), + } + } + + /// Zip two instances into one. + pub fn zip<U>(self, other: Corners<U>) -> Corners<(T, U)> { + Corners { + top_left: (self.top_left, other.top_left), + top_right: (self.top_right, other.top_right), + bottom_right: (self.bottom_right, other.bottom_right), + bottom_left: (self.bottom_left, other.bottom_left), + } + } + + /// An iterator over the corners, starting with the top left corner, + /// clockwise. + pub fn iter(&self) -> impl Iterator<Item = &T> { + [&self.top_left, &self.top_right, &self.bottom_right, &self.bottom_left] + .into_iter() + } + + /// Whether all sides are equal. + pub fn is_uniform(&self) -> bool + where + T: PartialEq, + { + self.top_left == self.top_right + && self.top_right == self.bottom_right + && self.bottom_right == self.bottom_left + } +} + +impl<T> Get<Corner> for Corners<T> { + type Component = T; + + fn get_ref(&self, corner: Corner) -> &T { + match corner { + Corner::TopLeft => &self.top_left, + Corner::TopRight => &self.top_right, + Corner::BottomRight => &self.bottom_right, + Corner::BottomLeft => &self.bottom_left, + } + } + + fn get_mut(&mut self, corner: Corner) -> &mut T { + match corner { + Corner::TopLeft => &mut self.top_left, + Corner::TopRight => &mut self.top_right, + Corner::BottomRight => &mut self.bottom_right, + Corner::BottomLeft => &mut self.bottom_left, + } + } +} + +/// The four corners of a rectangle. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Corner { + /// The top left corner. + TopLeft, + /// The top right corner. + TopRight, + /// The bottom right corner. + BottomRight, + /// The bottom left corner. + BottomLeft, +} + +impl<T: Reflect> Reflect for Corners<Option<T>> { + fn describe() -> CastInfo { + T::describe() + Dict::describe() + } + + fn castable(value: &Value) -> bool { + Dict::castable(value) || T::castable(value) + } +} + +impl<T> IntoValue for Corners<T> +where + T: PartialEq + IntoValue, +{ + fn into_value(self) -> Value { + if self.is_uniform() { + return self.top_left.into_value(); + } + + let mut dict = Dict::new(); + let mut handle = |key: &str, component: T| { + let value = component.into_value(); + if value != Value::None { + dict.insert(key.into(), value); + } + }; + + handle("top-left", self.top_left); + handle("top-right", self.top_right); + handle("bottom-right", self.bottom_right); + handle("bottom-left", self.bottom_left); + + Value::Dict(dict) + } +} + +impl<T> FromValue for Corners<Option<T>> +where + T: FromValue + Clone, +{ + fn from_value(mut value: Value) -> StrResult<Self> { + let keys = [ + "top-left", + "top-right", + "bottom-right", + "bottom-left", + "left", + "top", + "right", + "bottom", + "rest", + ]; + + if let Value::Dict(dict) = &mut value { + if dict.iter().any(|(key, _)| keys.contains(&key.as_str())) { + let mut take = |key| dict.take(key).ok().map(T::from_value).transpose(); + let rest = take("rest")?; + let left = take("left")?.or_else(|| rest.clone()); + let top = take("top")?.or_else(|| rest.clone()); + let right = take("right")?.or_else(|| rest.clone()); + let bottom = take("bottom")?.or_else(|| rest.clone()); + let corners = Corners { + top_left: take("top-left")? + .or_else(|| top.clone()) + .or_else(|| left.clone()), + top_right: take("top-right")? + .or_else(|| top.clone()) + .or_else(|| right.clone()), + bottom_right: take("bottom-right")? + .or_else(|| bottom.clone()) + .or_else(|| right.clone()), + bottom_left: take("bottom-left")? + .or_else(|| bottom.clone()) + .or_else(|| left.clone()), + }; + + dict.finish(&keys)?; + return Ok(corners); + } + } + + if T::castable(&value) { + Ok(Self::splat(Some(T::from_value(value)?))) + } else { + Err(Self::error(&value)) + } + } +} + +impl<T: Resolve> Resolve for Corners<T> { + type Output = Corners<T::Output>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.map(|v| v.resolve(styles)) + } +} + +impl<T: Fold> Fold for Corners<Option<T>> { + type Output = Corners<T::Output>; + + fn fold(self, outer: Self::Output) -> Self::Output { + self.zip(outer).map(|(inner, outer)| match inner { + Some(value) => value.fold(outer), + None => outer, + }) + } +} diff --git a/crates/typst/src/geom/dir.rs b/crates/typst/src/geom/dir.rs new file mode 100644 index 00000000..48915471 --- /dev/null +++ b/crates/typst/src/geom/dir.rs @@ -0,0 +1,79 @@ +use super::*; + +/// The four directions into which content can be laid out. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub enum Dir { + /// Left to right. + LTR, + /// Right to left. + RTL, + /// Top to bottom. + TTB, + /// Bottom to top. + BTT, +} + +impl Dir { + /// The specific axis this direction belongs to. + pub const fn axis(self) -> Axis { + match self { + Self::LTR | Self::RTL => Axis::X, + Self::TTB | Self::BTT => Axis::Y, + } + } + + /// The side this direction starts at. + pub const fn start(self) -> Side { + match self { + Self::LTR => Side::Left, + Self::RTL => Side::Right, + Self::TTB => Side::Top, + Self::BTT => Side::Bottom, + } + } + + /// The side this direction ends at. + pub const fn end(self) -> Side { + match self { + Self::LTR => Side::Right, + Self::RTL => Side::Left, + Self::TTB => Side::Bottom, + Self::BTT => Side::Top, + } + } + + /// The inverse direction. + pub const fn inv(self) -> Self { + match self { + Self::LTR => Self::RTL, + Self::RTL => Self::LTR, + Self::TTB => Self::BTT, + Self::BTT => Self::TTB, + } + } + + /// Whether this direction points into the positive coordinate direction. + /// + /// The positive directions are left-to-right and top-to-bottom. + pub const fn is_positive(self) -> bool { + match self { + Self::LTR | Self::TTB => true, + Self::RTL | Self::BTT => false, + } + } +} + +impl Debug for Dir { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(match self { + Self::LTR => "ltr", + Self::RTL => "rtl", + Self::TTB => "ttb", + Self::BTT => "btt", + }) + } +} + +cast! { + type Dir: "direction", +} diff --git a/crates/typst/src/geom/ellipse.rs b/crates/typst/src/geom/ellipse.rs new file mode 100644 index 00000000..ac20ffd3 --- /dev/null +++ b/crates/typst/src/geom/ellipse.rs @@ -0,0 +1,22 @@ +use super::*; + +/// Produce a shape that approximates an axis-aligned ellipse. +pub fn ellipse(size: Size, fill: Option<Paint>, stroke: Option<Stroke>) -> Shape { + // https://stackoverflow.com/a/2007782 + let z = Abs::zero(); + let rx = size.x / 2.0; + let ry = size.y / 2.0; + let m = 0.551784; + let mx = m * rx; + let my = m * ry; + let point = |x, y| Point::new(x + rx, y + ry); + + let mut path = Path::new(); + path.move_to(point(-rx, z)); + path.cubic_to(point(-rx, -my), point(-mx, -ry), point(z, -ry)); + path.cubic_to(point(mx, -ry), point(rx, -my), point(rx, z)); + path.cubic_to(point(rx, my), point(mx, ry), point(z, ry)); + path.cubic_to(point(-mx, ry), point(-rx, my), point(-rx, z)); + + Shape { geometry: Geometry::Path(path), stroke, fill } +} diff --git a/crates/typst/src/geom/em.rs b/crates/typst/src/geom/em.rs new file mode 100644 index 00000000..8dda9ff6 --- /dev/null +++ b/crates/typst/src/geom/em.rs @@ -0,0 +1,153 @@ +use super::*; + +/// A length that is relative to the font size. +/// +/// `1em` is the same as the font size. +#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Em(Scalar); + +impl Em { + /// The zero em length. + pub const fn zero() -> Self { + Self(Scalar(0.0)) + } + + /// The font size. + pub const fn one() -> Self { + Self(Scalar(1.0)) + } + + /// Create a font-relative length. + pub const fn new(em: f64) -> Self { + Self(Scalar(em)) + } + + /// Create an em length from font units at the given units per em. + pub fn from_units(units: impl Into<f64>, units_per_em: f64) -> Self { + Self(Scalar(units.into() / units_per_em)) + } + + /// Create an em length from a length at the given font size. + pub fn from_length(length: Abs, font_size: Abs) -> Self { + let result = length / font_size; + if result.is_finite() { + Self(Scalar(result)) + } else { + Self::zero() + } + } + + /// The number of em units. + pub const fn get(self) -> f64 { + (self.0).0 + } + + /// The absolute value of this em length. + pub fn abs(self) -> Self { + Self::new(self.get().abs()) + } + + /// Convert to an absolute length at the given font size. + pub fn at(self, font_size: Abs) -> Abs { + let resolved = font_size * self.get(); + if resolved.is_finite() { + resolved + } else { + Abs::zero() + } + } +} + +impl Numeric for Em { + fn zero() -> Self { + Self::zero() + } + + fn is_finite(self) -> bool { + self.0.is_finite() + } +} + +impl Debug for Em { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}em", self.get()) + } +} + +impl Neg for Em { + type Output = Self; + + fn neg(self) -> Self { + Self(-self.0) + } +} + +impl Add for Em { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self(self.0 + other.0) + } +} + +sub_impl!(Em - Em -> Em); + +impl Mul<f64> for Em { + type Output = Self; + + fn mul(self, other: f64) -> Self { + Self(self.0 * other) + } +} + +impl Mul<Em> for f64 { + type Output = Em; + + fn mul(self, other: Em) -> Em { + other * self + } +} + +impl Div<f64> for Em { + type Output = Self; + + fn div(self, other: f64) -> Self { + Self(self.0 / other) + } +} + +impl Div for Em { + type Output = f64; + + fn div(self, other: Self) -> f64 { + self.get() / other.get() + } +} + +assign_impl!(Em += Em); +assign_impl!(Em -= Em); +assign_impl!(Em *= f64); +assign_impl!(Em /= f64); + +impl Sum for Em { + fn sum<I: Iterator<Item = Self>>(iter: I) -> Self { + Self(iter.map(|s| s.0).sum()) + } +} + +cast! { + Em, + self => Value::Length(self.into()), +} + +impl Resolve for Em { + type Output = Abs; + + fn resolve(self, styles: StyleChain) -> Self::Output { + if self.is_zero() { + Abs::zero() + } else { + self.at(item!(em)(styles)) + } + } +} diff --git a/crates/typst/src/geom/fr.rs b/crates/typst/src/geom/fr.rs new file mode 100644 index 00000000..c602634d --- /dev/null +++ b/crates/typst/src/geom/fr.rs @@ -0,0 +1,119 @@ +use super::*; + +/// A fraction of remaining space. +#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Fr(Scalar); + +impl Fr { + /// Takes up zero space: `0fr`. + pub const fn zero() -> Self { + Self(Scalar(0.0)) + } + + /// Takes up as much space as all other items with this fraction: `1fr`. + pub const fn one() -> Self { + Self(Scalar(1.0)) + } + + /// Create a new fraction. + pub const fn new(ratio: f64) -> Self { + Self(Scalar(ratio)) + } + + /// Get the underlying number. + pub const fn get(self) -> f64 { + (self.0).0 + } + + /// The absolute value of this fraction. + pub fn abs(self) -> Self { + Self::new(self.get().abs()) + } + + /// Determine this fraction's share in the remaining space. + pub fn share(self, total: Self, remaining: Abs) -> Abs { + let ratio = self / total; + if ratio.is_finite() && remaining.is_finite() { + (ratio * remaining).max(Abs::zero()) + } else { + Abs::zero() + } + } +} + +impl Numeric for Fr { + fn zero() -> Self { + Self::zero() + } + + fn is_finite(self) -> bool { + self.0.is_finite() + } +} + +impl Debug for Fr { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}fr", round_2(self.get())) + } +} + +impl Neg for Fr { + type Output = Self; + + fn neg(self) -> Self { + Self(-self.0) + } +} + +impl Add for Fr { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self(self.0 + other.0) + } +} + +sub_impl!(Fr - Fr -> Fr); + +impl Mul<f64> for Fr { + type Output = Self; + + fn mul(self, other: f64) -> Self { + Self(self.0 * other) + } +} + +impl Mul<Fr> for f64 { + type Output = Fr; + + fn mul(self, other: Fr) -> Fr { + other * self + } +} + +impl Div<f64> for Fr { + type Output = Self; + + fn div(self, other: f64) -> Self { + Self(self.0 / other) + } +} + +impl Div for Fr { + type Output = f64; + + fn div(self, other: Self) -> f64 { + self.get() / other.get() + } +} + +assign_impl!(Fr += Fr); +assign_impl!(Fr -= Fr); +assign_impl!(Fr *= f64); +assign_impl!(Fr /= f64); + +impl Sum for Fr { + fn sum<I: Iterator<Item = Self>>(iter: I) -> Self { + Self(iter.map(|s| s.0).sum()) + } +} diff --git a/crates/typst/src/geom/length.rs b/crates/typst/src/geom/length.rs new file mode 100644 index 00000000..7d0a9841 --- /dev/null +++ b/crates/typst/src/geom/length.rs @@ -0,0 +1,128 @@ +use super::*; + +/// A size or distance, possibly expressed with contextual units. +/// +/// Currently supports absolute and font-relative units, but support could quite +/// easily be extended to other units. +#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Length { + /// The absolute part. + pub abs: Abs, + /// The font-relative part. + pub em: Em, +} + +impl Length { + /// The zero length. + pub const fn zero() -> Self { + Self { abs: Abs::zero(), em: Em::zero() } + } + + /// Try to compute the absolute value of the length. + pub fn try_abs(self) -> Option<Self> { + (self.abs.is_zero() || self.em.is_zero()) + .then(|| Self { abs: self.abs.abs(), em: self.em.abs() }) + } + + /// Try to divide two lengths. + pub fn try_div(self, other: Self) -> Option<f64> { + if self.abs.is_zero() && other.abs.is_zero() { + Some(self.em / other.em) + } else if self.em.is_zero() && other.em.is_zero() { + Some(self.abs / other.abs) + } else { + None + } + } +} + +impl Debug for Length { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match (self.abs.is_zero(), self.em.is_zero()) { + (false, false) => write!(f, "{:?} + {:?}", self.abs, self.em), + (true, false) => self.em.fmt(f), + (_, true) => self.abs.fmt(f), + } + } +} + +impl Numeric for Length { + fn zero() -> Self { + Self::zero() + } + + fn is_finite(self) -> bool { + self.abs.is_finite() && self.em.is_finite() + } +} + +impl PartialOrd for Length { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + if self.em.is_zero() && other.em.is_zero() { + self.abs.partial_cmp(&other.abs) + } else if self.abs.is_zero() && other.abs.is_zero() { + self.em.partial_cmp(&other.em) + } else { + None + } + } +} + +impl From<Abs> for Length { + fn from(abs: Abs) -> Self { + Self { abs, em: Em::zero() } + } +} + +impl From<Em> for Length { + fn from(em: Em) -> Self { + Self { abs: Abs::zero(), em } + } +} + +impl Neg for Length { + type Output = Self; + + fn neg(self) -> Self::Output { + Self { abs: -self.abs, em: -self.em } + } +} + +impl Add for Length { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self { abs: self.abs + rhs.abs, em: self.em + rhs.em } + } +} + +sub_impl!(Length - Length -> Length); + +impl Mul<f64> for Length { + type Output = Self; + + fn mul(self, rhs: f64) -> Self::Output { + Self { abs: self.abs * rhs, em: self.em * rhs } + } +} + +impl Div<f64> for Length { + type Output = Self; + + fn div(self, rhs: f64) -> Self::Output { + Self { abs: self.abs / rhs, em: self.em / rhs } + } +} + +assign_impl!(Length += Length); +assign_impl!(Length -= Length); +assign_impl!(Length *= f64); +assign_impl!(Length /= f64); + +impl Resolve for Length { + type Output = Abs; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.abs + self.em.resolve(styles) + } +} diff --git a/crates/typst/src/geom/macros.rs b/crates/typst/src/geom/macros.rs new file mode 100644 index 00000000..b1b50e22 --- /dev/null +++ b/crates/typst/src/geom/macros.rs @@ -0,0 +1,47 @@ +/// Implement the `Sub` trait based on existing `Neg` and `Add` impls. +macro_rules! sub_impl { + ($a:ident - $b:ident -> $c:ident) => { + impl std::ops::Sub<$b> for $a { + type Output = $c; + + fn sub(self, other: $b) -> $c { + self + -other + } + } + }; +} + +/// Implement an assign trait based on an existing non-assign trait. +macro_rules! assign_impl { + ($a:ident += $b:ident) => { + impl std::ops::AddAssign<$b> for $a { + fn add_assign(&mut self, other: $b) { + *self = *self + other; + } + } + }; + + ($a:ident -= $b:ident) => { + impl std::ops::SubAssign<$b> for $a { + fn sub_assign(&mut self, other: $b) { + *self = *self - other; + } + } + }; + + ($a:ident *= $b:ident) => { + impl std::ops::MulAssign<$b> for $a { + fn mul_assign(&mut self, other: $b) { + *self = *self * other; + } + } + }; + + ($a:ident /= $b:ident) => { + impl std::ops::DivAssign<$b> for $a { + fn div_assign(&mut self, other: $b) { + *self = *self / other; + } + } + }; +} diff --git a/crates/typst/src/geom/mod.rs b/crates/typst/src/geom/mod.rs new file mode 100644 index 00000000..b7a7ff40 --- /dev/null +++ b/crates/typst/src/geom/mod.rs @@ -0,0 +1,121 @@ +//! Geometrical primitives. + +#[macro_use] +mod macros; +mod abs; +mod align; +mod angle; +mod axes; +mod color; +mod corners; +mod dir; +mod ellipse; +mod em; +mod fr; +mod length; +mod paint; +mod path; +mod point; +mod ratio; +mod rel; +mod rounded; +mod scalar; +mod shape; +mod sides; +mod size; +mod smart; +mod stroke; +mod transform; + +pub use self::abs::{Abs, AbsUnit}; +pub use self::align::{Align, GenAlign, HorizontalAlign, VerticalAlign}; +pub use self::angle::{Angle, AngleUnit}; +pub use self::axes::{Axes, Axis}; +pub use self::color::{CmykColor, Color, LumaColor, RgbaColor}; +pub use self::corners::{Corner, Corners}; +pub use self::dir::Dir; +pub use self::ellipse::ellipse; +pub use self::em::Em; +pub use self::fr::Fr; +pub use self::length::Length; +pub use self::paint::Paint; +pub use self::path::{Path, PathItem}; +pub use self::point::Point; +pub use self::ratio::Ratio; +pub use self::rel::Rel; +pub use self::rounded::rounded_rect; +pub use self::scalar::Scalar; +pub use self::shape::{Geometry, Shape}; +pub use self::sides::{Side, Sides}; +pub use self::size::Size; +pub use self::smart::Smart; +pub use self::stroke::{ + DashLength, DashPattern, LineCap, LineJoin, PartialStroke, Stroke, +}; +pub use self::transform::Transform; + +use std::cmp::Ordering; +use std::f64::consts::PI; +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::iter::Sum; +use std::ops::*; + +use crate::diag::{bail, StrResult}; +use crate::eval::{array, cast, Array, Dict, Value}; +use crate::model::{Fold, Resolve, StyleChain}; + +/// Generic access to a structure's components. +pub trait Get<Index> { + /// The structure's component type. + type Component; + + /// Borrow the component for the specified index. + fn get_ref(&self, index: Index) -> &Self::Component; + + /// Borrow the component for the specified index mutably. + fn get_mut(&mut self, index: Index) -> &mut Self::Component; + + /// Convenience method for getting a copy of a component. + fn get(self, index: Index) -> Self::Component + where + Self: Sized, + Self::Component: Copy, + { + *self.get_ref(index) + } + + /// Convenience method for setting a component. + fn set(&mut self, index: Index, component: Self::Component) { + *self.get_mut(index) = component; + } +} + +/// A numeric type. +pub trait Numeric: + Sized + + Debug + + Copy + + PartialEq + + Neg<Output = Self> + + Add<Output = Self> + + Sub<Output = Self> + + Mul<f64, Output = Self> + + Div<f64, Output = Self> +{ + /// The identity element for addition. + fn zero() -> Self; + + /// Whether `self` is zero. + fn is_zero(self) -> bool { + self == Self::zero() + } + + /// Whether `self` consists only of finite parts. + fn is_finite(self) -> bool; +} + +/// Round a float to two decimal places. +pub fn round_2(value: f64) -> f64 { + (value * 100.0).round() / 100.0 +} diff --git a/crates/typst/src/geom/paint.rs b/crates/typst/src/geom/paint.rs new file mode 100644 index 00000000..10fa9fde --- /dev/null +++ b/crates/typst/src/geom/paint.rs @@ -0,0 +1,30 @@ +use super::*; + +/// How a fill or stroke should be painted. +#[derive(Clone, Eq, PartialEq, Hash)] +pub enum Paint { + /// A solid color. + Solid(Color), +} + +impl<T: Into<Color>> From<T> for Paint { + fn from(t: T) -> Self { + Self::Solid(t.into()) + } +} + +impl Debug for Paint { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Solid(color) => color.fmt(f), + } + } +} + +cast! { + Paint, + self => match self { + Self::Solid(color) => Value::Color(color), + }, + color: Color => Self::Solid(color), +} diff --git a/crates/typst/src/geom/path.rs b/crates/typst/src/geom/path.rs new file mode 100644 index 00000000..1c5325a3 --- /dev/null +++ b/crates/typst/src/geom/path.rs @@ -0,0 +1,54 @@ +use super::*; + +/// A bezier path. +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct Path(pub Vec<PathItem>); + +/// An item in a bezier path. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum PathItem { + MoveTo(Point), + LineTo(Point), + CubicTo(Point, Point, Point), + ClosePath, +} + +impl Path { + /// Create an empty path. + pub const fn new() -> Self { + Self(vec![]) + } + + /// Create a path that describes a rectangle. + pub fn rect(size: Size) -> Self { + let z = Abs::zero(); + let point = Point::new; + let mut path = Self::new(); + path.move_to(point(z, z)); + path.line_to(point(size.x, z)); + path.line_to(point(size.x, size.y)); + path.line_to(point(z, size.y)); + path.close_path(); + path + } + + /// Push a [`MoveTo`](PathItem::MoveTo) item. + pub fn move_to(&mut self, p: Point) { + self.0.push(PathItem::MoveTo(p)); + } + + /// Push a [`LineTo`](PathItem::LineTo) item. + pub fn line_to(&mut self, p: Point) { + self.0.push(PathItem::LineTo(p)); + } + + /// Push a [`CubicTo`](PathItem::CubicTo) item. + pub fn cubic_to(&mut self, p1: Point, p2: Point, p3: Point) { + self.0.push(PathItem::CubicTo(p1, p2, p3)); + } + + /// Push a [`ClosePath`](PathItem::ClosePath) item. + pub fn close_path(&mut self) { + self.0.push(PathItem::ClosePath); + } +} diff --git a/crates/typst/src/geom/point.rs b/crates/typst/src/geom/point.rs new file mode 100644 index 00000000..e7811e1e --- /dev/null +++ b/crates/typst/src/geom/point.rs @@ -0,0 +1,146 @@ +use super::*; + +/// A point in 2D. +#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Point { + /// The x coordinate. + pub x: Abs, + /// The y coordinate. + pub y: Abs, +} + +impl Point { + /// The origin point. + pub const fn zero() -> Self { + Self { x: Abs::zero(), y: Abs::zero() } + } + + /// Create a new point from x and y coordinates. + pub const fn new(x: Abs, y: Abs) -> Self { + Self { x, y } + } + + /// Create an instance with two equal components. + pub const fn splat(value: Abs) -> Self { + Self { x: value, y: value } + } + + /// Create a new point with y set to zero. + pub const fn with_x(x: Abs) -> Self { + Self { x, y: Abs::zero() } + } + + /// Create a new point with x set to zero. + pub const fn with_y(y: Abs) -> Self { + Self { x: Abs::zero(), y } + } + + /// The component-wise minimum of this and another point. + pub fn min(self, other: Self) -> Self { + Self { x: self.x.min(other.x), y: self.y.min(other.y) } + } + + /// The component-wise minimum of this and another point. + pub fn max(self, other: Self) -> Self { + Self { x: self.x.max(other.x), y: self.y.max(other.y) } + } + + /// The distance between this point and the origin. + pub fn hypot(self) -> Abs { + Abs::raw(self.x.to_raw().hypot(self.y.to_raw())) + } + + /// Transform the point with the given transformation. + pub fn transform(self, ts: Transform) -> Self { + Self::new( + ts.sx.of(self.x) + ts.kx.of(self.y) + ts.tx, + ts.ky.of(self.x) + ts.sy.of(self.y) + ts.ty, + ) + } + + /// Convert to a size. + pub fn to_size(self) -> Size { + Size::new(self.x, self.y) + } +} + +impl Numeric for Point { + fn zero() -> Self { + Self::zero() + } + + fn is_finite(self) -> bool { + self.x.is_finite() && self.y.is_finite() + } +} + +impl Get<Axis> for Point { + type Component = Abs; + + fn get_ref(&self, axis: Axis) -> &Abs { + match axis { + Axis::X => &self.x, + Axis::Y => &self.y, + } + } + + fn get_mut(&mut self, axis: Axis) -> &mut Abs { + match axis { + Axis::X => &mut self.x, + Axis::Y => &mut self.y, + } + } +} + +impl Debug for Point { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Point({:?}, {:?})", self.x, self.y) + } +} + +impl Neg for Point { + type Output = Self; + + fn neg(self) -> Self { + Self { x: -self.x, y: -self.y } + } +} + +impl Add for Point { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self { x: self.x + other.x, y: self.y + other.y } + } +} + +sub_impl!(Point - Point -> Point); + +impl Mul<f64> for Point { + type Output = Self; + + fn mul(self, other: f64) -> Self { + Self { x: self.x * other, y: self.y * other } + } +} + +impl Mul<Point> for f64 { + type Output = Point; + + fn mul(self, other: Point) -> Point { + other * self + } +} + +impl Div<f64> for Point { + type Output = Self; + + fn div(self, other: f64) -> Self { + Self { x: self.x / other, y: self.y / other } + } +} + +assign_impl!(Point += Point); +assign_impl!(Point -= Point); +assign_impl!(Point *= f64); +assign_impl!(Point /= f64); diff --git a/crates/typst/src/geom/ratio.rs b/crates/typst/src/geom/ratio.rs new file mode 100644 index 00000000..fe87dd6c --- /dev/null +++ b/crates/typst/src/geom/ratio.rs @@ -0,0 +1,133 @@ +use super::*; + +/// A ratio of a whole. +/// +/// _Note_: `50%` is represented as `0.5` here, but stored as `50.0` in the +/// corresponding [literal](crate::syntax::ast::Numeric). +#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Ratio(Scalar); + +impl Ratio { + /// A ratio of `0%` represented as `0.0`. + pub const fn zero() -> Self { + Self(Scalar(0.0)) + } + + /// A ratio of `100%` represented as `1.0`. + pub const fn one() -> Self { + Self(Scalar(1.0)) + } + + /// Create a new ratio from a value, where `1.0` means `100%`. + pub const fn new(ratio: f64) -> Self { + Self(Scalar(ratio)) + } + + /// Get the underlying ratio. + pub const fn get(self) -> f64 { + (self.0).0 + } + + /// Whether the ratio is zero. + pub fn is_zero(self) -> bool { + self.0 == 0.0 + } + + /// Whether the ratio is one. + pub fn is_one(self) -> bool { + self.0 == 1.0 + } + + /// The absolute value of this ratio. + pub fn abs(self) -> Self { + Self::new(self.get().abs()) + } + + /// Return the ratio of the given `whole`. + pub fn of<T: Numeric>(self, whole: T) -> T { + let resolved = whole * self.get(); + if resolved.is_finite() { + resolved + } else { + T::zero() + } + } +} + +impl Debug for Ratio { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}%", round_2(100.0 * self.get())) + } +} + +impl Neg for Ratio { + type Output = Self; + + fn neg(self) -> Self { + Self(-self.0) + } +} + +impl Add for Ratio { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self(self.0 + other.0) + } +} + +sub_impl!(Ratio - Ratio -> Ratio); + +impl Mul for Ratio { + type Output = Self; + + fn mul(self, other: Self) -> Self { + Self(self.0 * other.0) + } +} + +impl Mul<f64> for Ratio { + type Output = Self; + + fn mul(self, other: f64) -> Self { + Self(self.0 * other) + } +} + +impl Mul<Ratio> for f64 { + type Output = Ratio; + + fn mul(self, other: Ratio) -> Ratio { + other * self + } +} + +impl Div<f64> for Ratio { + type Output = Self; + + fn div(self, other: f64) -> Self { + Self(self.0 / other) + } +} + +impl Div<Ratio> for f64 { + type Output = Self; + + fn div(self, other: Ratio) -> Self { + self / other.get() + } +} + +impl Div for Ratio { + type Output = f64; + + fn div(self, other: Self) -> f64 { + self.get() / other.get() + } +} + +assign_impl!(Ratio += Ratio); +assign_impl!(Ratio -= Ratio); +assign_impl!(Ratio *= Ratio); +assign_impl!(Ratio *= f64); +assign_impl!(Ratio /= f64); diff --git a/crates/typst/src/geom/rel.rs b/crates/typst/src/geom/rel.rs new file mode 100644 index 00000000..88972222 --- /dev/null +++ b/crates/typst/src/geom/rel.rs @@ -0,0 +1,246 @@ +use super::*; + +/// A value that is composed of a relative and an absolute part. +#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Rel<T: Numeric> { + /// The relative part. + pub rel: Ratio, + /// The absolute part. + pub abs: T, +} + +impl<T: Numeric> Rel<T> { + /// The zero relative. + pub fn zero() -> Self { + Self { rel: Ratio::zero(), abs: T::zero() } + } + + /// A relative with a ratio of `100%` and no absolute part. + pub fn one() -> Self { + Self { rel: Ratio::one(), abs: T::zero() } + } + + /// Create a new relative from its parts. + pub fn new(rel: Ratio, abs: T) -> Self { + Self { rel, abs } + } + + /// Whether both parts are zero. + pub fn is_zero(self) -> bool { + self.rel.is_zero() && self.abs == T::zero() + } + + /// Whether the relative part is one and the absolute part is zero. + pub fn is_one(self) -> bool { + self.rel.is_one() && self.abs == T::zero() + } + + /// Evaluate this relative to the given `whole`. + pub fn relative_to(self, whole: T) -> T { + self.rel.of(whole) + self.abs + } + + /// Map the absolute part with `f`. + pub fn map<F, U>(self, f: F) -> Rel<U> + where + F: FnOnce(T) -> U, + U: Numeric, + { + Rel { rel: self.rel, abs: f(self.abs) } + } +} + +impl Rel<Length> { + /// Try to divide two relative lengths. + pub fn try_div(self, other: Self) -> Option<f64> { + if self.rel.is_zero() && other.rel.is_zero() { + self.abs.try_div(other.abs) + } else if self.abs.is_zero() && other.abs.is_zero() { + Some(self.rel / other.rel) + } else { + None + } + } +} + +impl<T: Numeric> Debug for Rel<T> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match (self.rel.is_zero(), self.abs.is_zero()) { + (false, false) => write!(f, "{:?} + {:?}", self.rel, self.abs), + (false, true) => self.rel.fmt(f), + (true, _) => self.abs.fmt(f), + } + } +} + +impl From<Abs> for Rel<Length> { + fn from(abs: Abs) -> Self { + Rel::from(Length::from(abs)) + } +} + +impl From<Em> for Rel<Length> { + fn from(em: Em) -> Self { + Rel::from(Length::from(em)) + } +} + +impl<T: Numeric> From<T> for Rel<T> { + fn from(abs: T) -> Self { + Self { rel: Ratio::zero(), abs } + } +} + +impl<T: Numeric> From<Ratio> for Rel<T> { + fn from(rel: Ratio) -> Self { + Self { rel, abs: T::zero() } + } +} + +impl<T: Numeric + PartialOrd> PartialOrd for Rel<T> { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + if self.rel.is_zero() && other.rel.is_zero() { + self.abs.partial_cmp(&other.abs) + } else if self.abs.is_zero() && other.abs.is_zero() { + self.rel.partial_cmp(&other.rel) + } else { + None + } + } +} + +impl<T: Numeric> Neg for Rel<T> { + type Output = Self; + + fn neg(self) -> Self { + Self { rel: -self.rel, abs: -self.abs } + } +} + +impl<T: Numeric> Add for Rel<T> { + type Output = Self; + + fn add(self, other: Self) -> Self::Output { + Self { + rel: self.rel + other.rel, + abs: self.abs + other.abs, + } + } +} + +impl<T: Numeric> Sub for Rel<T> { + type Output = Self; + + fn sub(self, other: Self) -> Self::Output { + self + -other + } +} + +impl<T: Numeric> Mul<f64> for Rel<T> { + type Output = Self; + + fn mul(self, other: f64) -> Self::Output { + Self { rel: self.rel * other, abs: self.abs * other } + } +} + +impl<T: Numeric> Mul<Rel<T>> for f64 { + type Output = Rel<T>; + + fn mul(self, other: Rel<T>) -> Self::Output { + other * self + } +} + +impl<T: Numeric> Div<f64> for Rel<T> { + type Output = Self; + + fn div(self, other: f64) -> Self::Output { + Self { rel: self.rel / other, abs: self.abs / other } + } +} + +impl<T: Numeric + AddAssign> AddAssign for Rel<T> { + fn add_assign(&mut self, other: Self) { + self.rel += other.rel; + self.abs += other.abs; + } +} + +impl<T: Numeric + SubAssign> SubAssign for Rel<T> { + fn sub_assign(&mut self, other: Self) { + self.rel -= other.rel; + self.abs -= other.abs; + } +} + +impl<T: Numeric + MulAssign<f64>> MulAssign<f64> for Rel<T> { + fn mul_assign(&mut self, other: f64) { + self.rel *= other; + self.abs *= other; + } +} + +impl<T: Numeric + DivAssign<f64>> DivAssign<f64> for Rel<T> { + fn div_assign(&mut self, other: f64) { + self.rel /= other; + self.abs /= other; + } +} + +impl<T: Numeric> Add<T> for Ratio { + type Output = Rel<T>; + + fn add(self, other: T) -> Self::Output { + Rel::from(self) + Rel::from(other) + } +} + +impl<T: Numeric> Add<T> for Rel<T> { + type Output = Self; + + fn add(self, other: T) -> Self::Output { + self + Rel::from(other) + } +} + +impl<T: Numeric> Add<Ratio> for Rel<T> { + type Output = Self; + + fn add(self, other: Ratio) -> Self::Output { + self + Rel::from(other) + } +} + +impl<T> Resolve for Rel<T> +where + T: Resolve + Numeric, + <T as Resolve>::Output: Numeric, +{ + type Output = Rel<<T as Resolve>::Output>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.map(|abs| abs.resolve(styles)) + } +} + +impl Fold for Rel<Abs> { + type Output = Self; + + fn fold(self, _: Self::Output) -> Self::Output { + self + } +} + +impl Fold for Rel<Length> { + type Output = Self; + + fn fold(self, _: Self::Output) -> Self::Output { + self + } +} + +cast! { + Rel<Abs>, + self => self.map(Length::from).into_value(), +} diff --git a/crates/typst/src/geom/rounded.rs b/crates/typst/src/geom/rounded.rs new file mode 100644 index 00000000..f1a7ea08 --- /dev/null +++ b/crates/typst/src/geom/rounded.rs @@ -0,0 +1,182 @@ +use super::*; + +/// Produce shapes that together make up a rounded rectangle. +pub fn rounded_rect( + size: Size, + radius: Corners<Abs>, + fill: Option<Paint>, + stroke: Sides<Option<Stroke>>, +) -> Vec<Shape> { + let mut res = vec![]; + if fill.is_some() || (stroke.iter().any(Option::is_some) && stroke.is_uniform()) { + res.push(Shape { + geometry: fill_geometry(size, radius), + fill, + stroke: if stroke.is_uniform() { stroke.top.clone() } else { None }, + }); + } + + if !stroke.is_uniform() { + for (path, stroke) in stroke_segments(size, radius, stroke) { + if stroke.is_some() { + res.push(Shape { geometry: Geometry::Path(path), fill: None, stroke }); + } + } + } + + res +} + +/// Output the shape of the rectangle as a path or primitive rectangle, +/// depending on whether it is rounded. +fn fill_geometry(size: Size, radius: Corners<Abs>) -> Geometry { + if radius.iter().copied().all(Abs::is_zero) { + Geometry::Rect(size) + } else { + let mut paths = stroke_segments(size, radius, Sides::splat(None)); + assert_eq!(paths.len(), 1); + Geometry::Path(paths.pop().unwrap().0) + } +} + +/// Output the minimum number of paths along the rectangles border. +fn stroke_segments( + size: Size, + radius: Corners<Abs>, + stroke: Sides<Option<Stroke>>, +) -> Vec<(Path, Option<Stroke>)> { + let mut res = vec![]; + + let mut connection = Connection::default(); + let mut path = Path::new(); + let mut always_continuous = true; + let max_radius = size.x.min(size.y).max(Abs::zero()) / 2.0; + + for side in [Side::Top, Side::Right, Side::Bottom, Side::Left] { + let continuous = stroke.get_ref(side) == stroke.get_ref(side.next_cw()); + connection = connection.advance(continuous && side != Side::Left); + always_continuous &= continuous; + + draw_side( + &mut path, + side, + size, + radius.get(side.start_corner()).clamp(Abs::zero(), max_radius), + radius.get(side.end_corner()).clamp(Abs::zero(), max_radius), + connection, + ); + + if !continuous { + res.push((std::mem::take(&mut path), stroke.get_ref(side).clone())); + } + } + + if always_continuous { + path.close_path(); + } + + if !path.0.is_empty() { + res.push((path, stroke.left)); + } + + res +} + +/// Draws one side of the rounded rectangle. Will always draw the left arc. The +/// right arc will be drawn halfway if and only if there is no connection. +fn draw_side( + path: &mut Path, + side: Side, + size: Size, + start_radius: Abs, + end_radius: Abs, + connection: Connection, +) { + let angle_left = Angle::deg(if connection.prev { 90.0 } else { 45.0 }); + let angle_right = Angle::deg(if connection.next { 90.0 } else { 45.0 }); + let length = size.get(side.axis()); + + // The arcs for a border of the rectangle along the x-axis, starting at (0,0). + let p1 = Point::with_x(start_radius); + let mut arc1 = bezier_arc( + p1 + Point::new( + -angle_left.sin() * start_radius, + (1.0 - angle_left.cos()) * start_radius, + ), + Point::new(start_radius, start_radius), + p1, + ); + + let p2 = Point::with_x(length - end_radius); + let mut arc2 = bezier_arc( + p2, + Point::new(length - end_radius, end_radius), + p2 + Point::new( + angle_right.sin() * end_radius, + (1.0 - angle_right.cos()) * end_radius, + ), + ); + + let transform = match side { + Side::Left => Transform::rotate(Angle::deg(-90.0)) + .post_concat(Transform::translate(Abs::zero(), size.y)), + Side::Bottom => Transform::rotate(Angle::deg(180.0)) + .post_concat(Transform::translate(size.x, size.y)), + Side::Right => Transform::rotate(Angle::deg(90.0)) + .post_concat(Transform::translate(size.x, Abs::zero())), + _ => Transform::identity(), + }; + + arc1 = arc1.map(|x| x.transform(transform)); + arc2 = arc2.map(|x| x.transform(transform)); + + if !connection.prev { + path.move_to(if start_radius.is_zero() { arc1[3] } else { arc1[0] }); + } + + if !start_radius.is_zero() { + path.cubic_to(arc1[1], arc1[2], arc1[3]); + } + + path.line_to(arc2[0]); + + if !connection.next && !end_radius.is_zero() { + path.cubic_to(arc2[1], arc2[2], arc2[3]); + } +} + +/// Get the control points for a bezier curve that describes a circular arc for +/// a start point, an end point and a center of the circle whose arc connects +/// the two. +fn bezier_arc(start: Point, center: Point, end: Point) -> [Point; 4] { + // https://stackoverflow.com/a/44829356/1567835 + let a = start - center; + let b = end - center; + + let q1 = a.x.to_raw() * a.x.to_raw() + a.y.to_raw() * a.y.to_raw(); + let q2 = q1 + a.x.to_raw() * b.x.to_raw() + a.y.to_raw() * b.y.to_raw(); + let k2 = (4.0 / 3.0) * ((2.0 * q1 * q2).sqrt() - q2) + / (a.x.to_raw() * b.y.to_raw() - a.y.to_raw() * b.x.to_raw()); + + let control_1 = Point::new(center.x + a.x - k2 * a.y, center.y + a.y + k2 * a.x); + let control_2 = Point::new(center.x + b.x + k2 * b.y, center.y + b.y - k2 * b.x); + + [start, control_1, control_2, end] +} + +/// Indicates which sides of the border strokes in a 2D polygon are connected to +/// their neighboring sides. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] +struct Connection { + prev: bool, + next: bool, +} + +impl Connection { + /// Advance to the next clockwise side of the polygon. The argument + /// indicates whether the border is connected on the right side of the next + /// edge. + pub fn advance(self, next: bool) -> Self { + Self { prev: self.next, next } + } +} diff --git a/crates/typst/src/geom/scalar.rs b/crates/typst/src/geom/scalar.rs new file mode 100644 index 00000000..71fb1755 --- /dev/null +++ b/crates/typst/src/geom/scalar.rs @@ -0,0 +1,175 @@ +use super::*; + +/// A 64-bit float that implements `Eq`, `Ord` and `Hash`. +/// +/// Panics if it's `NaN` during any of those operations. +#[derive(Default, Copy, Clone)] +pub struct Scalar(pub f64); + +impl Numeric for Scalar { + fn zero() -> Self { + Self(0.0) + } + + fn is_finite(self) -> bool { + self.0.is_finite() + } +} + +impl From<f64> for Scalar { + fn from(float: f64) -> Self { + Self(float) + } +} + +impl From<Scalar> for f64 { + fn from(scalar: Scalar) -> Self { + scalar.0 + } +} + +impl Debug for Scalar { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Eq for Scalar {} + +impl PartialEq for Scalar { + fn eq(&self, other: &Self) -> bool { + assert!(!self.0.is_nan() && !other.0.is_nan(), "float is NaN"); + self.0 == other.0 + } +} + +impl PartialEq<f64> for Scalar { + fn eq(&self, other: &f64) -> bool { + self == &Self(*other) + } +} + +impl Ord for Scalar { + fn cmp(&self, other: &Self) -> Ordering { + self.partial_cmp(other).expect("float is NaN") + } +} + +impl PartialOrd for Scalar { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + self.0.partial_cmp(&other.0) + } + + fn lt(&self, other: &Self) -> bool { + self.0 < other.0 + } + + fn le(&self, other: &Self) -> bool { + self.0 <= other.0 + } + + fn gt(&self, other: &Self) -> bool { + self.0 > other.0 + } + + fn ge(&self, other: &Self) -> bool { + self.0 >= other.0 + } +} + +impl Hash for Scalar { + fn hash<H: Hasher>(&self, state: &mut H) { + debug_assert!(!self.0.is_nan(), "float is NaN"); + self.0.to_bits().hash(state); + } +} + +impl Neg for Scalar { + type Output = Self; + + fn neg(self) -> Self::Output { + Self(-self.0) + } +} + +impl<T: Into<Self>> Add<T> for Scalar { + type Output = Self; + + fn add(self, rhs: T) -> Self::Output { + Self(self.0 + rhs.into().0) + } +} + +impl<T: Into<Self>> AddAssign<T> for Scalar { + fn add_assign(&mut self, rhs: T) { + self.0 += rhs.into().0; + } +} + +impl<T: Into<Self>> Sub<T> for Scalar { + type Output = Self; + + fn sub(self, rhs: T) -> Self::Output { + Self(self.0 - rhs.into().0) + } +} + +impl<T: Into<Self>> SubAssign<T> for Scalar { + fn sub_assign(&mut self, rhs: T) { + self.0 -= rhs.into().0; + } +} + +impl<T: Into<Self>> Mul<T> for Scalar { + type Output = Self; + + fn mul(self, rhs: T) -> Self::Output { + Self(self.0 * rhs.into().0) + } +} + +impl<T: Into<Self>> MulAssign<T> for Scalar { + fn mul_assign(&mut self, rhs: T) { + self.0 *= rhs.into().0; + } +} + +impl<T: Into<Self>> Div<T> for Scalar { + type Output = Self; + + fn div(self, rhs: T) -> Self::Output { + Self(self.0 / rhs.into().0) + } +} + +impl<T: Into<Self>> DivAssign<T> for Scalar { + fn div_assign(&mut self, rhs: T) { + self.0 /= rhs.into().0; + } +} + +impl<T: Into<Self>> Rem<T> for Scalar { + type Output = Self; + + fn rem(self, rhs: T) -> Self::Output { + Self(self.0 % rhs.into().0) + } +} + +impl<T: Into<Self>> RemAssign<T> for Scalar { + fn rem_assign(&mut self, rhs: T) { + self.0 %= rhs.into().0; + } +} + +impl Sum for Scalar { + fn sum<I: Iterator<Item = Self>>(iter: I) -> Self { + Self(iter.map(|s| s.0).sum()) + } +} + +impl<'a> Sum<&'a Self> for Scalar { + fn sum<I: Iterator<Item = &'a Self>>(iter: I) -> Self { + Self(iter.map(|s| s.0).sum()) + } +} diff --git a/crates/typst/src/geom/shape.rs b/crates/typst/src/geom/shape.rs new file mode 100644 index 00000000..5658c21f --- /dev/null +++ b/crates/typst/src/geom/shape.rs @@ -0,0 +1,35 @@ +use super::*; + +/// A geometric shape with optional fill and stroke. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Shape { + /// The shape's geometry. + pub geometry: Geometry, + /// The shape's background fill. + pub fill: Option<Paint>, + /// The shape's border stroke. + pub stroke: Option<Stroke>, +} + +/// A shape's geometry. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum Geometry { + /// A line to a point (relative to its position). + Line(Point), + /// A rectangle with its origin in the topleft corner. + Rect(Size), + /// A bezier path. + Path(Path), +} + +impl Geometry { + /// Fill the geometry without a stroke. + pub fn filled(self, fill: Paint) -> Shape { + Shape { geometry: self, fill: Some(fill), stroke: None } + } + + /// Stroke the geometry without a fill. + pub fn stroked(self, stroke: Stroke) -> Shape { + Shape { geometry: self, fill: None, stroke: Some(stroke) } + } +} diff --git a/crates/typst/src/geom/sides.rs b/crates/typst/src/geom/sides.rs new file mode 100644 index 00000000..d4b72a9d --- /dev/null +++ b/crates/typst/src/geom/sides.rs @@ -0,0 +1,268 @@ +use crate::eval::{CastInfo, FromValue, IntoValue, Reflect}; + +use super::*; + +/// A container with left, top, right and bottom components. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Sides<T> { + /// The value for the left side. + pub left: T, + /// The value for the top side. + pub top: T, + /// The value for the right side. + pub right: T, + /// The value for the bottom side. + pub bottom: T, +} + +impl<T> Sides<T> { + /// Create a new instance from the four components. + pub const fn new(left: T, top: T, right: T, bottom: T) -> Self { + Self { left, top, right, bottom } + } + + /// Create an instance with four equal components. + pub fn splat(value: T) -> Self + where + T: Clone, + { + Self { + left: value.clone(), + top: value.clone(), + right: value.clone(), + bottom: value, + } + } + + /// Map the individual fields with `f`. + pub fn map<F, U>(self, mut f: F) -> Sides<U> + where + F: FnMut(T) -> U, + { + Sides { + left: f(self.left), + top: f(self.top), + right: f(self.right), + bottom: f(self.bottom), + } + } + + /// Zip two instances into one. + pub fn zip<U>(self, other: Sides<U>) -> Sides<(T, U)> { + Sides { + left: (self.left, other.left), + top: (self.top, other.top), + right: (self.right, other.right), + bottom: (self.bottom, other.bottom), + } + } + + /// An iterator over the sides, starting with the left side, clockwise. + pub fn iter(&self) -> impl Iterator<Item = &T> { + [&self.left, &self.top, &self.right, &self.bottom].into_iter() + } + + /// Whether all sides are equal. + pub fn is_uniform(&self) -> bool + where + T: PartialEq, + { + self.left == self.top && self.top == self.right && self.right == self.bottom + } +} + +impl<T: Add> Sides<T> { + /// Sums up `left` and `right` into `x`, and `top` and `bottom` into `y`. + pub fn sum_by_axis(self) -> Axes<T::Output> { + Axes::new(self.left + self.right, self.top + self.bottom) + } +} + +impl Sides<Rel<Abs>> { + /// Evaluate the sides relative to the given `size`. + pub fn relative_to(self, size: Size) -> Sides<Abs> { + Sides { + left: self.left.relative_to(size.x), + top: self.top.relative_to(size.y), + right: self.right.relative_to(size.x), + bottom: self.bottom.relative_to(size.y), + } + } +} + +impl<T> Get<Side> for Sides<T> { + type Component = T; + + fn get_ref(&self, side: Side) -> &T { + match side { + Side::Left => &self.left, + Side::Top => &self.top, + Side::Right => &self.right, + Side::Bottom => &self.bottom, + } + } + + fn get_mut(&mut self, side: Side) -> &mut T { + match side { + Side::Left => &mut self.left, + Side::Top => &mut self.top, + Side::Right => &mut self.right, + Side::Bottom => &mut self.bottom, + } + } +} + +/// The four sides of objects. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Side { + /// The left side. + Left, + /// The top side. + Top, + /// The right side. + Right, + /// The bottom side. + Bottom, +} + +impl Side { + /// The opposite side. + pub fn inv(self) -> Self { + match self { + Self::Left => Self::Right, + Self::Top => Self::Bottom, + Self::Right => Self::Left, + Self::Bottom => Self::Top, + } + } + + /// The next side, clockwise. + pub fn next_cw(self) -> Self { + match self { + Self::Left => Self::Top, + Self::Top => Self::Right, + Self::Right => Self::Bottom, + Self::Bottom => Self::Left, + } + } + + /// The next side, counter-clockwise. + pub fn next_ccw(self) -> Self { + match self { + Self::Left => Self::Bottom, + Self::Top => Self::Left, + Self::Right => Self::Top, + Self::Bottom => Self::Right, + } + } + + /// The first corner of the side in clockwise order. + pub fn start_corner(self) -> Corner { + match self { + Self::Left => Corner::BottomLeft, + Self::Top => Corner::TopLeft, + Self::Right => Corner::TopRight, + Self::Bottom => Corner::BottomRight, + } + } + + /// The second corner of the side in clockwise order. + pub fn end_corner(self) -> Corner { + self.next_cw().start_corner() + } + + /// Return the corresponding axis. + pub fn axis(self) -> Axis { + match self { + Self::Left | Self::Right => Axis::Y, + Self::Top | Self::Bottom => Axis::X, + } + } +} + +impl<T: Reflect> Reflect for Sides<Option<T>> { + fn describe() -> CastInfo { + T::describe() + Dict::describe() + } + + fn castable(value: &Value) -> bool { + Dict::castable(value) || T::castable(value) + } +} + +impl<T> IntoValue for Sides<T> +where + T: PartialEq + IntoValue, +{ + fn into_value(self) -> Value { + if self.is_uniform() { + return self.left.into_value(); + } + + let mut dict = Dict::new(); + let mut handle = |key: &str, component: T| { + let value = component.into_value(); + if value != Value::None { + dict.insert(key.into(), value); + } + }; + + handle("left", self.left); + handle("top", self.top); + handle("right", self.right); + handle("bottom", self.bottom); + + Value::Dict(dict) + } +} + +impl<T> FromValue for Sides<Option<T>> +where + T: Default + FromValue + Clone, +{ + fn from_value(mut value: Value) -> StrResult<Self> { + let keys = ["left", "top", "right", "bottom", "x", "y", "rest"]; + if let Value::Dict(dict) = &mut value { + if dict.iter().any(|(key, _)| keys.contains(&key.as_str())) { + let mut take = |key| dict.take(key).ok().map(T::from_value).transpose(); + let rest = take("rest")?; + let x = take("x")?.or_else(|| rest.clone()); + let y = take("y")?.or_else(|| rest.clone()); + let sides = Sides { + left: take("left")?.or_else(|| x.clone()), + top: take("top")?.or_else(|| y.clone()), + right: take("right")?.or_else(|| x.clone()), + bottom: take("bottom")?.or_else(|| y.clone()), + }; + + dict.finish(&keys)?; + return Ok(sides); + } + } + + if T::castable(&value) { + Ok(Self::splat(Some(T::from_value(value)?))) + } else { + Err(Self::error(&value)) + } + } +} + +impl<T: Resolve> Resolve for Sides<T> { + type Output = Sides<T::Output>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.map(|v| v.resolve(styles)) + } +} + +impl<T: Fold> Fold for Sides<Option<T>> { + type Output = Sides<T::Output>; + + fn fold(self, outer: Self::Output) -> Self::Output { + self.zip(outer).map(|(inner, outer)| match inner { + Some(value) => value.fold(outer), + None => outer, + }) + } +} diff --git a/crates/typst/src/geom/size.rs b/crates/typst/src/geom/size.rs new file mode 100644 index 00000000..a2e32b77 --- /dev/null +++ b/crates/typst/src/geom/size.rs @@ -0,0 +1,78 @@ +use super::*; + +/// A size in 2D. +pub type Size = Axes<Abs>; + +impl Size { + /// The zero value. + pub const fn zero() -> Self { + Self { x: Abs::zero(), y: Abs::zero() } + } + + /// Whether the other size fits into this one (smaller width and height). + pub fn fits(self, other: Self) -> bool { + self.x.fits(other.x) && self.y.fits(other.y) + } + + /// Convert to a point. + pub fn to_point(self) -> Point { + Point::new(self.x, self.y) + } +} + +impl Numeric for Size { + fn zero() -> Self { + Self::zero() + } + + fn is_finite(self) -> bool { + self.x.is_finite() && self.y.is_finite() + } +} + +impl Neg for Size { + type Output = Self; + + fn neg(self) -> Self { + Self { x: -self.x, y: -self.y } + } +} + +impl Add for Size { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self { x: self.x + other.x, y: self.y + other.y } + } +} + +sub_impl!(Size - Size -> Size); + +impl Mul<f64> for Size { + type Output = Self; + + fn mul(self, other: f64) -> Self { + Self { x: self.x * other, y: self.y * other } + } +} + +impl Mul<Size> for f64 { + type Output = Size; + + fn mul(self, other: Size) -> Size { + other * self + } +} + +impl Div<f64> for Size { + type Output = Self; + + fn div(self, other: f64) -> Self { + Self { x: self.x / other, y: self.y / other } + } +} + +assign_impl!(Size -= Size); +assign_impl!(Size += Size); +assign_impl!(Size *= f64); +assign_impl!(Size /= f64); diff --git a/crates/typst/src/geom/smart.rs b/crates/typst/src/geom/smart.rs new file mode 100644 index 00000000..a6271c20 --- /dev/null +++ b/crates/typst/src/geom/smart.rs @@ -0,0 +1,146 @@ +use crate::eval::{AutoValue, CastInfo, FromValue, IntoValue, Reflect}; + +use super::*; + +/// A value that can be automatically determined. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum Smart<T> { + /// The value should be determined smartly based on the circumstances. + Auto, + /// A specific value. + Custom(T), +} + +impl<T> Smart<T> { + /// Whether the value is `Auto`. + pub fn is_auto(&self) -> bool { + matches!(self, Self::Auto) + } + + /// Whether this holds a custom value. + pub fn is_custom(&self) -> bool { + matches!(self, Self::Custom(_)) + } + + /// Returns a reference the contained custom value. + /// If the value is [`Smart::Auto`], `None` is returned. + pub fn as_custom(self) -> Option<T> { + match self { + Self::Auto => None, + Self::Custom(x) => Some(x), + } + } + + /// Map the contained custom value with `f`. + pub fn map<F, U>(self, f: F) -> Smart<U> + where + F: FnOnce(T) -> U, + { + match self { + Self::Auto => Smart::Auto, + Self::Custom(x) => Smart::Custom(f(x)), + } + } + + /// Map the contained custom value with `f` if it contains a custom value, + /// otherwise returns `default`. + pub fn map_or<F, U>(self, default: U, f: F) -> U + where + F: FnOnce(T) -> U, + { + match self { + Self::Auto => default, + Self::Custom(x) => f(x), + } + } + + /// Keeps `self` if it contains a custom value, otherwise returns `other`. + pub fn or(self, other: Smart<T>) -> Self { + match self { + Self::Custom(x) => Self::Custom(x), + Self::Auto => other, + } + } + + /// Returns the contained custom value or a provided default value. + pub fn unwrap_or(self, default: T) -> T { + match self { + Self::Auto => default, + Self::Custom(x) => x, + } + } + + /// Returns the contained custom value or computes a default value. + pub fn unwrap_or_else<F>(self, f: F) -> T + where + F: FnOnce() -> T, + { + match self { + Self::Auto => f(), + Self::Custom(x) => x, + } + } + + /// Returns the contained custom value or the default value. + pub fn unwrap_or_default(self) -> T + where + T: Default, + { + self.unwrap_or_else(T::default) + } +} + +impl<T> Default for Smart<T> { + fn default() -> Self { + Self::Auto + } +} + +impl<T: Reflect> Reflect for Smart<T> { + fn castable(value: &Value) -> bool { + AutoValue::castable(value) || T::castable(value) + } + + fn describe() -> CastInfo { + T::describe() + AutoValue::describe() + } +} + +impl<T: IntoValue> IntoValue for Smart<T> { + fn into_value(self) -> Value { + match self { + Smart::Custom(v) => v.into_value(), + Smart::Auto => Value::Auto, + } + } +} + +impl<T: FromValue> FromValue for Smart<T> { + fn from_value(value: Value) -> StrResult<Self> { + match value { + Value::Auto => Ok(Self::Auto), + v if T::castable(&v) => Ok(Self::Custom(T::from_value(v)?)), + _ => Err(Self::error(&value)), + } + } +} + +impl<T: Resolve> Resolve for Smart<T> { + type Output = Smart<T::Output>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.map(|v| v.resolve(styles)) + } +} + +impl<T> Fold for Smart<T> +where + T: Fold, + T::Output: Default, +{ + type Output = Smart<T::Output>; + + fn fold(self, outer: Self::Output) -> Self::Output { + self.map(|inner| inner.fold(outer.unwrap_or_default())) + } +} diff --git a/crates/typst/src/geom/stroke.rs b/crates/typst/src/geom/stroke.rs new file mode 100644 index 00000000..66264d5d --- /dev/null +++ b/crates/typst/src/geom/stroke.rs @@ -0,0 +1,387 @@ +use crate::eval::{Cast, FromValue}; + +use super::*; + +/// A stroke of a geometric shape. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Stroke { + /// The stroke's paint. + pub paint: Paint, + /// The stroke's thickness. + pub thickness: Abs, + /// The stroke's line cap. + pub line_cap: LineCap, + /// The stroke's line join. + pub line_join: LineJoin, + /// The stroke's line dash pattern. + pub dash_pattern: Option<DashPattern<Abs, Abs>>, + /// The miter limit. Defaults to 4.0, same as `tiny-skia`. + pub miter_limit: Scalar, +} + +impl Default for Stroke { + fn default() -> Self { + Self { + paint: Paint::Solid(Color::BLACK), + thickness: Abs::pt(1.0), + line_cap: LineCap::Butt, + line_join: LineJoin::Miter, + dash_pattern: None, + miter_limit: Scalar(4.0), + } + } +} + +/// A partial stroke representation. +/// +/// In this representation, both fields are optional so that you can pass either +/// just a paint (`red`), just a thickness (`0.1em`) or both (`2pt + red`) where +/// this is expected. +#[derive(Default, Clone, Eq, PartialEq, Hash)] +pub struct PartialStroke<T = Length> { + /// The stroke's paint. + pub paint: Smart<Paint>, + /// The stroke's thickness. + pub thickness: Smart<T>, + /// The stroke's line cap. + pub line_cap: Smart<LineCap>, + /// The stroke's line join. + pub line_join: Smart<LineJoin>, + /// The stroke's line dash pattern. + pub dash_pattern: Smart<Option<DashPattern<T>>>, + /// The miter limit. + pub miter_limit: Smart<Scalar>, +} + +impl<T> PartialStroke<T> { + /// Map the contained lengths with `f`. + pub fn map<F, U>(self, f: F) -> PartialStroke<U> + where + F: Fn(T) -> U, + { + PartialStroke { + paint: self.paint, + thickness: self.thickness.map(&f), + line_cap: self.line_cap, + line_join: self.line_join, + dash_pattern: self.dash_pattern.map(|pattern| { + pattern.map(|pattern| DashPattern { + array: pattern + .array + .into_iter() + .map(|l| match l { + DashLength::Length(v) => DashLength::Length(f(v)), + DashLength::LineWidth => DashLength::LineWidth, + }) + .collect(), + phase: f(pattern.phase), + }) + }), + miter_limit: self.miter_limit, + } + } +} + +impl PartialStroke<Abs> { + /// Unpack the stroke, filling missing fields from the `default`. + pub fn unwrap_or(self, default: Stroke) -> Stroke { + let thickness = self.thickness.unwrap_or(default.thickness); + let dash_pattern = self + .dash_pattern + .map(|pattern| { + pattern.map(|pattern| DashPattern { + array: pattern + .array + .into_iter() + .map(|l| l.finish(thickness)) + .collect(), + phase: pattern.phase, + }) + }) + .unwrap_or(default.dash_pattern); + + Stroke { + paint: self.paint.unwrap_or(default.paint), + thickness, + line_cap: self.line_cap.unwrap_or(default.line_cap), + line_join: self.line_join.unwrap_or(default.line_join), + dash_pattern, + miter_limit: self.miter_limit.unwrap_or(default.miter_limit), + } + } + + /// Unpack the stroke, filling missing fields with the default values. + pub fn unwrap_or_default(self) -> Stroke { + self.unwrap_or(Stroke::default()) + } +} + +impl<T: Debug> Debug for PartialStroke<T> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let Self { + paint, + thickness, + line_cap, + line_join, + dash_pattern, + miter_limit, + } = &self; + if line_cap.is_auto() + && line_join.is_auto() + && dash_pattern.is_auto() + && miter_limit.is_auto() + { + match (&self.paint, &self.thickness) { + (Smart::Custom(paint), Smart::Custom(thickness)) => { + write!(f, "{thickness:?} + {paint:?}") + } + (Smart::Custom(paint), Smart::Auto) => paint.fmt(f), + (Smart::Auto, Smart::Custom(thickness)) => thickness.fmt(f), + (Smart::Auto, Smart::Auto) => f.pad("1pt + black"), + } + } else { + write!(f, "(")?; + let mut sep = ""; + if let Smart::Custom(paint) = &paint { + write!(f, "{}paint: {:?}", sep, paint)?; + sep = ", "; + } + if let Smart::Custom(thickness) = &thickness { + write!(f, "{}thickness: {:?}", sep, thickness)?; + sep = ", "; + } + if let Smart::Custom(cap) = &line_cap { + write!(f, "{}cap: {:?}", sep, cap)?; + sep = ", "; + } + if let Smart::Custom(join) = &line_join { + write!(f, "{}join: {:?}", sep, join)?; + sep = ", "; + } + if let Smart::Custom(dash) = &dash_pattern { + write!(f, "{}dash: {:?}", sep, dash)?; + sep = ", "; + } + if let Smart::Custom(miter_limit) = &miter_limit { + write!(f, "{}miter-limit: {:?}", sep, miter_limit)?; + } + write!(f, ")")?; + Ok(()) + } + } +} + +impl Resolve for PartialStroke { + type Output = PartialStroke<Abs>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + PartialStroke { + paint: self.paint, + thickness: self.thickness.resolve(styles), + line_cap: self.line_cap, + line_join: self.line_join, + dash_pattern: self.dash_pattern.resolve(styles), + miter_limit: self.miter_limit, + } + } +} + +impl Fold for PartialStroke<Abs> { + type Output = Self; + + fn fold(self, outer: Self::Output) -> Self::Output { + Self { + paint: self.paint.or(outer.paint), + thickness: self.thickness.or(outer.thickness), + line_cap: self.line_cap.or(outer.line_cap), + line_join: self.line_join.or(outer.line_join), + dash_pattern: self.dash_pattern.or(outer.dash_pattern), + miter_limit: self.miter_limit.or(outer.miter_limit), + } + } +} + +cast! { + type PartialStroke: "stroke", + thickness: Length => Self { + thickness: Smart::Custom(thickness), + ..Default::default() + }, + color: Color => Self { + paint: Smart::Custom(color.into()), + ..Default::default() + }, + mut dict: Dict => { + fn take<T: FromValue>(dict: &mut Dict, key: &str) -> StrResult<Smart<T>> { + Ok(dict.take(key).ok().map(T::from_value) + .transpose()?.map(Smart::Custom).unwrap_or(Smart::Auto)) + } + + let paint = take::<Paint>(&mut dict, "paint")?; + let thickness = take::<Length>(&mut dict, "thickness")?; + let line_cap = take::<LineCap>(&mut dict, "cap")?; + let line_join = take::<LineJoin>(&mut dict, "join")?; + let dash_pattern = take::<Option<DashPattern>>(&mut dict, "dash")?; + let miter_limit = take::<f64>(&mut dict, "miter-limit")?; + dict.finish(&["paint", "thickness", "cap", "join", "dash", "miter-limit"])?; + + Self { + paint, + thickness, + line_cap, + line_join, + dash_pattern, + miter_limit: miter_limit.map(Scalar), + } + }, +} + +cast! { + PartialStroke<Abs>, + self => self.map(Length::from).into_value(), +} + +/// The line cap of a stroke +#[derive(Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum LineCap { + Butt, + Round, + Square, +} + +impl Debug for LineCap { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + LineCap::Butt => write!(f, "\"butt\""), + LineCap::Round => write!(f, "\"round\""), + LineCap::Square => write!(f, "\"square\""), + } + } +} + +/// The line join of a stroke +#[derive(Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum LineJoin { + Miter, + Round, + Bevel, +} + +impl Debug for LineJoin { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + LineJoin::Miter => write!(f, "\"miter\""), + LineJoin::Round => write!(f, "\"round\""), + LineJoin::Bevel => write!(f, "\"bevel\""), + } + } +} + +/// A line dash pattern. +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct DashPattern<T = Length, DT = DashLength<T>> { + /// The dash array. + pub array: Vec<DT>, + /// The dash phase. + pub phase: T, +} + +impl<T: Debug, DT: Debug> Debug for DashPattern<T, DT> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "(array: (")?; + for (i, elem) in self.array.iter().enumerate() { + if i == 0 { + write!(f, "{:?}", elem)?; + } else { + write!(f, ", {:?}", elem)?; + } + } + write!(f, "), phase: {:?})", self.phase)?; + Ok(()) + } +} + +impl<T: Default> From<Vec<DashLength<T>>> for DashPattern<T> { + fn from(array: Vec<DashLength<T>>) -> Self { + Self { array, phase: T::default() } + } +} + +impl Resolve for DashPattern { + type Output = DashPattern<Abs>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + DashPattern { + array: self.array.into_iter().map(|l| l.resolve(styles)).collect(), + phase: self.phase.resolve(styles), + } + } +} + +// Same names as tikz: +// https://tex.stackexchange.com/questions/45275/tikz-get-values-for-predefined-dash-patterns +cast! { + DashPattern, + + "solid" => Vec::new().into(), + "dotted" => vec![DashLength::LineWidth, Abs::pt(2.0).into()].into(), + "densely-dotted" => vec![DashLength::LineWidth, Abs::pt(1.0).into()].into(), + "loosely-dotted" => vec![DashLength::LineWidth, Abs::pt(4.0).into()].into(), + "dashed" => vec![Abs::pt(3.0).into(), Abs::pt(3.0).into()].into(), + "densely-dashed" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into()].into(), + "loosely-dashed" => vec![Abs::pt(3.0).into(), Abs::pt(6.0).into()].into(), + "dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into(), DashLength::LineWidth, Abs::pt(2.0).into()].into(), + "densely-dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(1.0).into(), DashLength::LineWidth, Abs::pt(1.0).into()].into(), + "loosely-dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(4.0).into(), DashLength::LineWidth, Abs::pt(4.0).into()].into(), + + array: Vec<DashLength> => Self { array, phase: Length::zero() }, + mut dict: Dict => { + let array: Vec<DashLength> = dict.take("array")?.cast()?; + let phase = dict.take("phase").ok().map(Value::cast) + .transpose()?.unwrap_or(Length::zero()); + dict.finish(&["array", "phase"])?; + Self { + array, + phase, + } + }, +} + +/// The length of a dash in a line dash pattern +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum DashLength<T = Length> { + LineWidth, + Length(T), +} + +impl From<Abs> for DashLength { + fn from(l: Abs) -> Self { + DashLength::Length(l.into()) + } +} + +impl<T> DashLength<T> { + fn finish(self, line_width: T) -> T { + match self { + Self::LineWidth => line_width, + Self::Length(l) => l, + } + } +} + +impl Resolve for DashLength { + type Output = DashLength<Abs>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + match self { + Self::LineWidth => DashLength::LineWidth, + Self::Length(v) => DashLength::Length(v.resolve(styles)), + } + } +} + +cast! { + DashLength, + "dot" => Self::LineWidth, + v: Length => Self::Length(v), +} diff --git a/crates/typst/src/geom/transform.rs b/crates/typst/src/geom/transform.rs new file mode 100644 index 00000000..1ff1dfdd --- /dev/null +++ b/crates/typst/src/geom/transform.rs @@ -0,0 +1,77 @@ +use super::*; + +/// A scale-skew-translate transformation. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Transform { + pub sx: Ratio, + pub ky: Ratio, + pub kx: Ratio, + pub sy: Ratio, + pub tx: Abs, + pub ty: Abs, +} + +impl Transform { + /// The identity transformation. + pub const fn identity() -> Self { + Self { + sx: Ratio::one(), + ky: Ratio::zero(), + kx: Ratio::zero(), + sy: Ratio::one(), + tx: Abs::zero(), + ty: Abs::zero(), + } + } + + /// A translate transform. + pub const fn translate(tx: Abs, ty: Abs) -> Self { + Self { tx, ty, ..Self::identity() } + } + + /// A scale transform. + pub const fn scale(sx: Ratio, sy: Ratio) -> Self { + Self { sx, sy, ..Self::identity() } + } + + /// A rotate transform. + pub fn rotate(angle: Angle) -> Self { + let cos = Ratio::new(angle.cos()); + let sin = Ratio::new(angle.sin()); + Self { + sx: cos, + ky: sin, + kx: -sin, + sy: cos, + ..Self::default() + } + } + + /// Whether this is the identity transformation. + pub fn is_identity(self) -> bool { + self == Self::identity() + } + + /// Pre-concatenate another transformation. + pub fn pre_concat(self, prev: Self) -> Self { + Transform { + sx: self.sx * prev.sx + self.kx * prev.ky, + ky: self.ky * prev.sx + self.sy * prev.ky, + kx: self.sx * prev.kx + self.kx * prev.sy, + sy: self.ky * prev.kx + self.sy * prev.sy, + tx: self.sx.of(prev.tx) + self.kx.of(prev.ty) + self.tx, + ty: self.ky.of(prev.tx) + self.sy.of(prev.ty) + self.ty, + } + } + + /// Post-concatenate another transformation. + pub fn post_concat(self, next: Self) -> Self { + next.pre_concat(self) + } +} + +impl Default for Transform { + fn default() -> Self { + Self::identity() + } +} diff --git a/crates/typst/src/ide/analyze.rs b/crates/typst/src/ide/analyze.rs new file mode 100644 index 00000000..dad466c1 --- /dev/null +++ b/crates/typst/src/ide/analyze.rs @@ -0,0 +1,111 @@ +use comemo::Track; +use ecow::EcoString; + +use crate::doc::Frame; +use crate::eval::{eval, Module, Route, Tracer, Value}; +use crate::model::{Introspector, Label}; +use crate::syntax::{ast, LinkedNode, Source, SyntaxKind}; +use crate::World; + +/// Try to determine a set of possible values for an expression. +pub fn analyze_expr(world: &(dyn World + 'static), node: &LinkedNode) -> Vec<Value> { + match node.cast::<ast::Expr>() { + Some(ast::Expr::None(_)) => vec![Value::None], + Some(ast::Expr::Auto(_)) => vec![Value::Auto], + Some(ast::Expr::Bool(v)) => vec![Value::Bool(v.get())], + Some(ast::Expr::Int(v)) => vec![Value::Int(v.get())], + Some(ast::Expr::Float(v)) => vec![Value::Float(v.get())], + Some(ast::Expr::Numeric(v)) => vec![Value::numeric(v.get())], + Some(ast::Expr::Str(v)) => vec![Value::Str(v.get().into())], + + Some(ast::Expr::FieldAccess(access)) => { + let Some(child) = node.children().next() else { return vec![] }; + analyze_expr(world, &child) + .into_iter() + .filter_map(|target| target.field(&access.field()).ok()) + .collect() + } + + Some(_) => { + if let Some(parent) = node.parent() { + if parent.kind() == SyntaxKind::FieldAccess && node.index() > 0 { + return analyze_expr(world, parent); + } + } + + let route = Route::default(); + let mut tracer = Tracer::new(Some(node.span())); + typst::eval::eval( + world.track(), + route.track(), + tracer.track_mut(), + &world.main(), + ) + .and_then(|module| { + typst::model::typeset( + world.track(), + tracer.track_mut(), + &module.content(), + ) + }) + .ok(); + + tracer.finish() + } + + _ => vec![], + } +} + +/// Try to load a module from the current source file. +pub fn analyze_import( + world: &(dyn World + 'static), + source: &Source, + path: &str, +) -> Option<Module> { + let route = Route::default(); + let mut tracer = Tracer::default(); + let id = source.id().join(path).ok()?; + let source = world.source(id).ok()?; + eval(world.track(), route.track(), tracer.track_mut(), &source).ok() +} + +/// Find all labels and details for them. +/// +/// Returns: +/// - All labels and descriptions for them, if available +/// - A split offset: All labels before this offset belong to nodes, all after +/// belong to a bibliography. +pub fn analyze_labels( + world: &(dyn World + 'static), + frames: &[Frame], +) -> (Vec<(Label, Option<EcoString>)>, usize) { + let mut output = vec![]; + let introspector = Introspector::new(frames); + let items = &world.library().items; + + // Labels in the document. + for elem in introspector.all() { + let Some(label) = elem.label().cloned() else { continue }; + let details = elem + .field("caption") + .or_else(|| elem.field("body")) + .and_then(|field| match field { + Value::Content(content) => Some(content), + _ => None, + }) + .as_ref() + .unwrap_or(elem) + .plain_text(); + output.push((label, Some(details))); + } + + let split = output.len(); + + // Bibliography keys. + for (key, detail) in (items.bibliography_keys)(introspector.track()) { + output.push((Label(key), detail)); + } + + (output, split) +} diff --git a/crates/typst/src/ide/complete.rs b/crates/typst/src/ide/complete.rs new file mode 100644 index 00000000..16cba1bc --- /dev/null +++ b/crates/typst/src/ide/complete.rs @@ -0,0 +1,1201 @@ +use std::collections::{BTreeSet, HashSet}; + +use ecow::{eco_format, EcoString}; +use if_chain::if_chain; +use unscanny::Scanner; + +use super::analyze::analyze_labels; +use super::{analyze_expr, analyze_import, plain_docs_sentence, summarize_font_family}; +use crate::doc::Frame; +use crate::eval::{format_str, methods_on, CastInfo, Library, Scope, Value}; +use crate::syntax::{ + ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind, +}; +use crate::util::separated_list; +use crate::World; + +/// Autocomplete a cursor position in a source file. +/// +/// Returns the position from which the completions apply and a list of +/// completions. +/// +/// When `explicit` is `true`, the user requested the completion by pressing +/// control and space or something similar. +pub fn autocomplete( + world: &(dyn World + 'static), + frames: &[Frame], + source: &Source, + cursor: usize, + explicit: bool, +) -> Option<(usize, Vec<Completion>)> { + let mut ctx = CompletionContext::new(world, frames, source, cursor, explicit)?; + + let _ = complete_comments(&mut ctx) + || complete_field_accesses(&mut ctx) + || complete_imports(&mut ctx) + || complete_rules(&mut ctx) + || complete_params(&mut ctx) + || complete_markup(&mut ctx) + || complete_math(&mut ctx) + || complete_code(&mut ctx); + + Some((ctx.from, ctx.completions)) +} + +/// An autocompletion option. +#[derive(Debug, Clone)] +pub struct Completion { + /// The kind of item this completes to. + pub kind: CompletionKind, + /// The label the completion is shown with. + pub label: EcoString, + /// The completed version of the input, possibly described with snippet + /// syntax like `${lhs} + ${rhs}`. + /// + /// Should default to the `label` if `None`. + pub apply: Option<EcoString>, + /// An optional short description, at most one sentence. + pub detail: Option<EcoString>, +} + +/// A kind of item that can be completed. +#[derive(Debug, Clone)] +pub enum CompletionKind { + /// A syntactical structure. + Syntax, + /// A function. + Func, + /// A function parameter. + Param, + /// A constant. + Constant, + /// A symbol. + Symbol(char), +} + +/// Complete in comments. Or rather, don't! +fn complete_comments(ctx: &mut CompletionContext) -> bool { + matches!(ctx.leaf.kind(), SyntaxKind::LineComment | SyntaxKind::BlockComment) +} + +/// Complete in markup mode. +fn complete_markup(ctx: &mut CompletionContext) -> bool { + // Bail if we aren't even in markup. + if !matches!( + ctx.leaf.parent_kind(), + None | Some(SyntaxKind::Markup) | Some(SyntaxKind::Ref) + ) { + return false; + } + + // Start of an interpolated identifier: "#|". + if ctx.leaf.kind() == SyntaxKind::Hashtag { + ctx.from = ctx.cursor; + code_completions(ctx, true); + return true; + } + + // An existing identifier: "#pa|". + if ctx.leaf.kind() == SyntaxKind::Ident { + ctx.from = ctx.leaf.offset(); + code_completions(ctx, true); + return true; + } + + // Start of an reference: "@|" or "@he|". + if ctx.leaf.kind() == SyntaxKind::RefMarker { + ctx.from = ctx.leaf.offset() + 1; + ctx.label_completions(); + return true; + } + + // Behind a half-completed binding: "#let x = |". + if_chain! { + if let Some(prev) = ctx.leaf.prev_leaf(); + if prev.kind() == SyntaxKind::Eq; + if prev.parent_kind() == Some(SyntaxKind::LetBinding); + then { + ctx.from = ctx.cursor; + code_completions(ctx, false); + return true; + } + } + + // Directly after a raw block. + let mut s = Scanner::new(ctx.text); + s.jump(ctx.leaf.offset()); + if s.eat_if("```") { + s.eat_while('`'); + let start = s.cursor(); + if s.eat_if(is_id_start) { + s.eat_while(is_id_continue); + } + if s.cursor() == ctx.cursor { + ctx.from = start; + ctx.raw_completions(); + } + return true; + } + + // Anywhere: "|". + if ctx.explicit { + ctx.from = ctx.cursor; + markup_completions(ctx); + return true; + } + + false +} + +/// Add completions for markup snippets. +#[rustfmt::skip] +fn markup_completions(ctx: &mut CompletionContext) { + ctx.snippet_completion( + "expression", + "#${}", + "Variables, function calls, blocks, and more.", + ); + + ctx.snippet_completion( + "linebreak", + "\\\n${}", + "Inserts a forced linebreak.", + ); + + ctx.snippet_completion( + "strong text", + "*${strong}*", + "Strongly emphasizes content by increasing the font weight.", + ); + + ctx.snippet_completion( + "emphasized text", + "_${emphasized}_", + "Emphasizes content by setting it in italic font style.", + ); + + ctx.snippet_completion( + "raw text", + "`${text}`", + "Displays text verbatim, in monospace.", + ); + + ctx.snippet_completion( + "code listing", + "```${lang}\n${code}\n```", + "Inserts computer code with syntax highlighting.", + ); + + ctx.snippet_completion( + "hyperlink", + "https://${example.com}", + "Links to a URL.", + ); + + ctx.snippet_completion( + "label", + "<${name}>", + "Makes the preceding element referenceable.", + ); + + ctx.snippet_completion( + "reference", + "@${name}", + "Inserts a reference to a label.", + ); + + ctx.snippet_completion( + "heading", + "= ${title}", + "Inserts a section heading.", + ); + + ctx.snippet_completion( + "list item", + "- ${item}", + "Inserts an item of a bullet list.", + ); + + ctx.snippet_completion( + "enumeration item", + "+ ${item}", + "Inserts an item of a numbered list.", + ); + + ctx.snippet_completion( + "enumeration item (numbered)", + "${number}. ${item}", + "Inserts an explicitly numbered list item.", + ); + + ctx.snippet_completion( + "term list item", + "/ ${term}: ${description}", + "Inserts an item of a term list.", + ); + + ctx.snippet_completion( + "math (inline)", + "$${x}$", + "Inserts an inline-level mathematical equation.", + ); + + ctx.snippet_completion( + "math (block)", + "$ ${sum_x^2} $", + "Inserts a block-level mathematical equation.", + ); +} + +/// Complete in math mode. +fn complete_math(ctx: &mut CompletionContext) -> bool { + if !matches!( + ctx.leaf.parent_kind(), + Some(SyntaxKind::Equation) + | Some(SyntaxKind::Math) + | Some(SyntaxKind::MathFrac) + | Some(SyntaxKind::MathAttach) + ) { + return false; + } + + // Start of an interpolated identifier: "#|". + if ctx.leaf.kind() == SyntaxKind::Hashtag { + ctx.from = ctx.cursor; + code_completions(ctx, true); + return true; + } + + // Behind existing atom or identifier: "$a|$" or "$abc|$". + if matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathIdent) { + ctx.from = ctx.leaf.offset(); + math_completions(ctx); + return true; + } + + // Anywhere: "$|$". + if ctx.explicit { + ctx.from = ctx.cursor; + math_completions(ctx); + return true; + } + + false +} + +/// Add completions for math snippets. +#[rustfmt::skip] +fn math_completions(ctx: &mut CompletionContext) { + ctx.scope_completions(true, |_| true); + + ctx.snippet_completion( + "subscript", + "${x}_${2:2}", + "Sets something in subscript.", + ); + + ctx.snippet_completion( + "superscript", + "${x}^${2:2}", + "Sets something in superscript.", + ); + + ctx.snippet_completion( + "fraction", + "${x}/${y}", + "Inserts a fraction.", + ); +} + +/// Complete field accesses. +fn complete_field_accesses(ctx: &mut CompletionContext) -> bool { + // Behind an expression plus dot: "emoji.|". + if_chain! { + if ctx.leaf.kind() == SyntaxKind::Dot + || (ctx.leaf.kind() == SyntaxKind::Text + && ctx.leaf.text() == "."); + if ctx.leaf.range().end == ctx.cursor; + if let Some(prev) = ctx.leaf.prev_sibling(); + if prev.is::<ast::Expr>(); + if prev.parent_kind() != Some(SyntaxKind::Markup) || + prev.prev_sibling_kind() == Some(SyntaxKind::Hashtag); + if let Some(value) = analyze_expr(ctx.world, &prev).into_iter().next(); + then { + ctx.from = ctx.cursor; + field_access_completions(ctx, &value); + return true; + } + } + + // Behind a started field access: "emoji.fa|". + if_chain! { + if ctx.leaf.kind() == SyntaxKind::Ident; + if let Some(prev) = ctx.leaf.prev_sibling(); + if prev.kind() == SyntaxKind::Dot; + if let Some(prev_prev) = prev.prev_sibling(); + if prev_prev.is::<ast::Expr>(); + if let Some(value) = analyze_expr(ctx.world, &prev_prev).into_iter().next(); + then { + ctx.from = ctx.leaf.offset(); + field_access_completions(ctx, &value); + return true; + } + } + + false +} + +/// Add completions for all fields on a value. +fn field_access_completions(ctx: &mut CompletionContext, value: &Value) { + for &(method, args) in methods_on(value.type_name()) { + ctx.completions.push(Completion { + kind: CompletionKind::Func, + label: method.into(), + apply: Some(if args { + eco_format!("{method}(${{}})") + } else { + eco_format!("{method}()${{}}") + }), + detail: None, + }) + } + + match value { + Value::Symbol(symbol) => { + for modifier in symbol.modifiers() { + if let Ok(modified) = symbol.clone().modified(modifier) { + ctx.completions.push(Completion { + kind: CompletionKind::Symbol(modified.get()), + label: modifier.into(), + apply: None, + detail: None, + }); + } + } + } + Value::Content(content) => { + for (name, value) in content.fields() { + ctx.value_completion(Some(name.clone()), &value, false, None); + } + } + Value::Dict(dict) => { + for (name, value) in dict.iter() { + ctx.value_completion(Some(name.clone().into()), value, false, None); + } + } + Value::Module(module) => { + for (name, value) in module.scope().iter() { + ctx.value_completion(Some(name.clone()), value, true, None); + } + } + Value::Func(func) => { + if let Some(info) = func.info() { + // Consider all names from the function's scope. + for (name, value) in info.scope.iter() { + ctx.value_completion(Some(name.clone()), value, true, None); + } + } + } + _ => {} + } +} + +/// Complete imports. +fn complete_imports(ctx: &mut CompletionContext) -> bool { + // In an import path for a package: + // "#import "@|", + if_chain! { + if matches!( + ctx.leaf.parent_kind(), + Some(SyntaxKind::ModuleImport | SyntaxKind::ModuleInclude) + ); + if let Some(ast::Expr::Str(str)) = ctx.leaf.cast(); + if str.get().starts_with('@'); + then { + ctx.from = ctx.leaf.offset(); + ctx.package_completions(); + return true; + } + } + + // Behind an import list: + // "#import "path.typ": |", + // "#import "path.typ": a, b, |". + if_chain! { + if let Some(prev) = ctx.leaf.prev_sibling(); + if let Some(ast::Expr::Import(import)) = prev.cast(); + if let Some(ast::Imports::Items(items)) = import.imports(); + if let Some(source) = prev.children().find(|child| child.is::<ast::Expr>()); + if let Some(value) = analyze_expr(ctx.world, &source).into_iter().next(); + then { + ctx.from = ctx.cursor; + import_item_completions(ctx, &items, &value); + return true; + } + } + + // Behind a half-started identifier in an import list: + // "#import "path.typ": thi|", + if_chain! { + if ctx.leaf.kind() == SyntaxKind::Ident; + if let Some(parent) = ctx.leaf.parent(); + if parent.kind() == SyntaxKind::ImportItems; + if let Some(grand) = parent.parent(); + if let Some(ast::Expr::Import(import)) = grand.cast(); + if let Some(ast::Imports::Items(items)) = import.imports(); + if let Some(source) = grand.children().find(|child| child.is::<ast::Expr>()); + if let Some(value) = analyze_expr(ctx.world, &source).into_iter().next(); + then { + ctx.from = ctx.leaf.offset(); + import_item_completions(ctx, &items, &value); + return true; + } + } + + false +} + +/// Add completions for all exports of a module. +fn import_item_completions( + ctx: &mut CompletionContext, + existing: &[ast::Ident], + value: &Value, +) { + let module = match value { + Value::Str(path) => match analyze_import(ctx.world, ctx.source, path) { + Some(module) => module, + None => return, + }, + Value::Module(module) => module.clone(), + _ => return, + }; + + if existing.is_empty() { + ctx.snippet_completion("*", "*", "Import everything."); + } + + for (name, value) in module.scope().iter() { + if existing.iter().all(|ident| ident.as_str() != name) { + ctx.value_completion(Some(name.clone()), value, false, None); + } + } +} + +/// Complete set and show rules. +fn complete_rules(ctx: &mut CompletionContext) -> bool { + // We don't want to complete directly behind the keyword. + if !ctx.leaf.kind().is_trivia() { + return false; + } + + let Some(prev) = ctx.leaf.prev_leaf() else { return false }; + + // Behind the set keyword: "set |". + if matches!(prev.kind(), SyntaxKind::Set) { + ctx.from = ctx.cursor; + set_rule_completions(ctx); + return true; + } + + // Behind the show keyword: "show |". + if matches!(prev.kind(), SyntaxKind::Show) { + ctx.from = ctx.cursor; + show_rule_selector_completions(ctx); + return true; + } + + // Behind a half-completed show rule: "show strong: |". + if_chain! { + if let Some(prev) = ctx.leaf.prev_leaf(); + if matches!(prev.kind(), SyntaxKind::Colon); + if matches!(prev.parent_kind(), Some(SyntaxKind::ShowRule)); + then { + ctx.from = ctx.cursor; + show_rule_recipe_completions(ctx); + return true; + } + } + + false +} + +/// Add completions for all functions from the global scope. +fn set_rule_completions(ctx: &mut CompletionContext) { + ctx.scope_completions(true, |value| { + matches!( + value, + Value::Func(func) if func.info().map_or(false, |info| { + info.params.iter().any(|param| param.settable) + }), + ) + }); +} + +/// Add completions for selectors. +fn show_rule_selector_completions(ctx: &mut CompletionContext) { + ctx.scope_completions( + false, + |value| matches!(value, Value::Func(func) if func.element().is_some()), + ); + + ctx.enrich("", ": "); + + ctx.snippet_completion( + "text selector", + "\"${text}\": ${}", + "Replace occurrences of specific text.", + ); + + ctx.snippet_completion( + "regex selector", + "regex(\"${regex}\"): ${}", + "Replace matches of a regular expression.", + ); +} + +/// Add completions for recipes. +fn show_rule_recipe_completions(ctx: &mut CompletionContext) { + ctx.snippet_completion( + "replacement", + "[${content}]", + "Replace the selected element with content.", + ); + + ctx.snippet_completion( + "replacement (string)", + "\"${text}\"", + "Replace the selected element with a string of text.", + ); + + ctx.snippet_completion( + "transformation", + "element => [${content}]", + "Transform the element with a function.", + ); + + ctx.scope_completions(false, |value| matches!(value, Value::Func(_))); +} + +/// Complete call and set rule parameters. +fn complete_params(ctx: &mut CompletionContext) -> bool { + // Ensure that we are in a function call or set rule's argument list. + let (callee, set, args) = if_chain! { + if let Some(parent) = ctx.leaf.parent(); + if let Some(parent) = match parent.kind() { + SyntaxKind::Named => parent.parent(), + _ => Some(parent), + }; + if let Some(args) = parent.cast::<ast::Args>(); + if let Some(grand) = parent.parent(); + if let Some(expr) = grand.cast::<ast::Expr>(); + let set = matches!(expr, ast::Expr::Set(_)); + if let Some(ast::Expr::Ident(callee)) = match expr { + ast::Expr::FuncCall(call) => Some(call.callee()), + ast::Expr::Set(set) => Some(set.target()), + _ => None, + }; + then { + (callee, set, args) + } else { + return false; + } + }; + + // Find the piece of syntax that decides what we're completing. + let mut deciding = ctx.leaf.clone(); + while !matches!( + deciding.kind(), + SyntaxKind::LeftParen | SyntaxKind::Comma | SyntaxKind::Colon + ) { + let Some(prev) = deciding.prev_leaf() else { break }; + deciding = prev; + } + + // Parameter values: "func(param:|)", "func(param: |)". + if_chain! { + if deciding.kind() == SyntaxKind::Colon; + if let Some(prev) = deciding.prev_leaf(); + if let Some(param) = prev.cast::<ast::Ident>(); + then { + if let Some(next) = deciding.next_leaf() { + ctx.from = ctx.cursor.min(next.offset()); + } + + named_param_value_completions(ctx, &callee, ¶m); + return true; + } + } + + // Parameters: "func(|)", "func(hi|)", "func(12,|)". + if_chain! { + if matches!(deciding.kind(), SyntaxKind::LeftParen | SyntaxKind::Comma); + if deciding.kind() != SyntaxKind::Comma || deciding.range().end < ctx.cursor; + then { + if let Some(next) = deciding.next_leaf() { + ctx.from = ctx.cursor.min(next.offset()); + } + + // Exclude arguments which are already present. + let exclude: Vec<_> = args.items().filter_map(|arg| match arg { + ast::Arg::Named(named) => Some(named.name()), + _ => None, + }).collect(); + + param_completions(ctx, &callee, set, &exclude); + return true; + } + } + + false +} + +/// Add completions for the parameters of a function. +fn param_completions( + ctx: &mut CompletionContext, + callee: &ast::Ident, + set: bool, + exclude: &[ast::Ident], +) { + let info = if_chain! { + if let Some(Value::Func(func)) = ctx.global.get(callee); + if let Some(info) = func.info(); + then { info } + else { return; } + }; + + for param in &info.params { + if exclude.iter().any(|ident| ident.as_str() == param.name) { + continue; + } + + if set && !param.settable { + continue; + } + + if param.named { + ctx.completions.push(Completion { + kind: CompletionKind::Param, + label: param.name.into(), + apply: Some(eco_format!("{}: ${{}}", param.name)), + detail: Some(plain_docs_sentence(param.docs)), + }); + } + + if param.positional { + ctx.cast_completions(¶m.cast); + } + } + + if ctx.before.ends_with(',') { + ctx.enrich(" ", ""); + } +} + +/// Add completions for the values of a named function parameter. +fn named_param_value_completions( + ctx: &mut CompletionContext, + callee: &ast::Ident, + name: &str, +) { + let param = if_chain! { + if let Some(Value::Func(func)) = ctx.global.get(callee); + if let Some(info) = func.info(); + if let Some(param) = info.param(name); + if param.named; + then { param } + else { return; } + }; + + ctx.cast_completions(¶m.cast); + + if callee.as_str() == "text" && name == "font" { + ctx.font_completions(); + } + + if ctx.before.ends_with(':') { + ctx.enrich(" ", ""); + } +} + +/// Complete in code mode. +fn complete_code(ctx: &mut CompletionContext) -> bool { + if matches!( + ctx.leaf.parent_kind(), + None | Some(SyntaxKind::Markup) + | Some(SyntaxKind::Math) + | Some(SyntaxKind::MathFrac) + | Some(SyntaxKind::MathAttach) + | Some(SyntaxKind::MathRoot) + ) { + return false; + } + + // An existing identifier: "{ pa| }". + if ctx.leaf.kind() == SyntaxKind::Ident { + ctx.from = ctx.leaf.offset(); + code_completions(ctx, false); + return true; + } + + // Anywhere: "{ | }". + // But not within or after an expression. + if ctx.explicit + && (ctx.leaf.kind().is_trivia() + || matches!(ctx.leaf.kind(), SyntaxKind::LeftParen | SyntaxKind::LeftBrace)) + { + ctx.from = ctx.cursor; + code_completions(ctx, false); + return true; + } + + false +} + +/// Add completions for expression snippets. +#[rustfmt::skip] +fn code_completions(ctx: &mut CompletionContext, hashtag: bool) { + ctx.scope_completions(true, |value| !hashtag || { + matches!(value, Value::Symbol(_) | Value::Func(_) | Value::Module(_)) + }); + + ctx.snippet_completion( + "function call", + "${function}(${arguments})[${body}]", + "Evaluates a function.", + ); + + ctx.snippet_completion( + "code block", + "{ ${} }", + "Inserts a nested code block.", + ); + + ctx.snippet_completion( + "content block", + "[${content}]", + "Switches into markup mode.", + ); + + ctx.snippet_completion( + "set rule", + "set ${}", + "Sets style properties on an element.", + ); + + ctx.snippet_completion( + "show rule", + "show ${}", + "Redefines the look of an element.", + ); + + ctx.snippet_completion( + "show rule (everything)", + "show: ${}", + "Transforms everything that follows.", + ); + + ctx.snippet_completion( + "let binding", + "let ${name} = ${value}", + "Saves a value in a variable.", + ); + + ctx.snippet_completion( + "let binding (function)", + "let ${name}(${params}) = ${output}", + "Defines a function.", + ); + + ctx.snippet_completion( + "if conditional", + "if ${1 < 2} {\n\t${}\n}", + "Computes or inserts something conditionally.", + ); + + ctx.snippet_completion( + "if-else conditional", + "if ${1 < 2} {\n\t${}\n} else {\n\t${}\n}", + "Computes or inserts different things based on a condition.", + ); + + ctx.snippet_completion( + "while loop", + "while ${1 < 2} {\n\t${}\n}", + "Computes or inserts something while a condition is met.", + ); + + ctx.snippet_completion( + "for loop", + "for ${value} in ${(1, 2, 3)} {\n\t${}\n}", + "Computes or inserts something for each value in a collection.", + ); + + ctx.snippet_completion( + "for loop (with key)", + "for ${key}, ${value} in ${(a: 1, b: 2)} {\n\t${}\n}", + "Computes or inserts something for each key and value in a collection.", + ); + + ctx.snippet_completion( + "break", + "break", + "Exits early from a loop.", + ); + + ctx.snippet_completion( + "continue", + "continue", + "Continues with the next iteration of a loop.", + ); + + ctx.snippet_completion( + "return", + "return ${output}", + "Returns early from a function.", + ); + + ctx.snippet_completion( + "import (file)", + "import \"${file}.typ\": ${items}", + "Imports variables from another file.", + ); + + ctx.snippet_completion( + "import (package)", + "import \"@${}\": ${items}", + "Imports variables from another file.", + ); + + ctx.snippet_completion( + "include (file)", + "include \"${file}.typ\"", + "Includes content from another file.", + ); + + ctx.snippet_completion( + "include (package)", + "include \"@${}\"", + "Includes content from another file.", + ); + + ctx.snippet_completion( + "array", + "(${1, 2, 3})", + "Creates a sequence of values.", + ); + + ctx.snippet_completion( + "dictionary", + "(${a: 1, b: 2})", + "Creates a mapping from names to value.", + ); + + if !hashtag { + ctx.snippet_completion( + "function", + "(${params}) => ${output}", + "Creates an unnamed function.", + ); + } +} + +/// Context for autocompletion. +struct CompletionContext<'a> { + world: &'a (dyn World + 'static), + frames: &'a [Frame], + library: &'a Library, + source: &'a Source, + global: &'a Scope, + math: &'a Scope, + text: &'a str, + before: &'a str, + after: &'a str, + leaf: LinkedNode<'a>, + cursor: usize, + explicit: bool, + from: usize, + completions: Vec<Completion>, + seen_casts: HashSet<u128>, +} + +impl<'a> CompletionContext<'a> { + /// Create a new autocompletion context. + fn new( + world: &'a (dyn World + 'static), + frames: &'a [Frame], + source: &'a Source, + cursor: usize, + explicit: bool, + ) -> Option<Self> { + let text = source.text(); + let library = world.library(); + let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?; + Some(Self { + world, + frames, + library, + source, + global: library.global.scope(), + math: library.math.scope(), + text, + before: &text[..cursor], + after: &text[cursor..], + leaf, + cursor, + explicit, + from: cursor, + completions: vec![], + seen_casts: HashSet::new(), + }) + } + + /// Add a prefix and suffix to all applications. + fn enrich(&mut self, prefix: &str, suffix: &str) { + for Completion { label, apply, .. } in &mut self.completions { + let current = apply.as_ref().unwrap_or(label); + *apply = Some(eco_format!("{prefix}{current}{suffix}")); + } + } + + /// Add a snippet completion. + fn snippet_completion( + &mut self, + label: &'static str, + snippet: &'static str, + docs: &'static str, + ) { + self.completions.push(Completion { + kind: CompletionKind::Syntax, + label: label.into(), + apply: Some(snippet.into()), + detail: Some(docs.into()), + }); + } + + /// Add completions for all font families. + fn font_completions(&mut self) { + let equation = self.before[self.cursor.saturating_sub(25)..].contains("equation"); + for (family, iter) in self.world.book().families() { + let detail = summarize_font_family(iter); + if !equation || family.contains("Math") { + self.value_completion( + None, + &Value::Str(family.into()), + false, + Some(detail.as_str()), + ); + } + } + } + + /// Add completions for all available packages. + fn package_completions(&mut self) { + for (package, description) in self.world.packages() { + self.value_completion( + None, + &Value::Str(format_str!("{package}")), + false, + description.as_deref(), + ); + } + } + + /// Add completions for raw block tags. + fn raw_completions(&mut self) { + for (name, mut tags) in (self.library.items.raw_languages)() { + let lower = name.to_lowercase(); + if !tags.contains(&lower.as_str()) { + tags.push(lower.as_str()); + } + + tags.retain(|tag| is_ident(tag)); + if tags.is_empty() { + continue; + } + + self.completions.push(Completion { + kind: CompletionKind::Constant, + label: name.into(), + apply: Some(tags[0].into()), + detail: Some(separated_list(&tags, " or ").into()), + }); + } + } + + /// Add completions for all labels. + fn label_completions(&mut self) { + for (label, detail) in analyze_labels(self.world, self.frames).0 { + self.completions.push(Completion { + kind: CompletionKind::Constant, + label: label.0, + apply: None, + detail, + }); + } + } + + /// Add a completion for a specific value. + fn value_completion( + &mut self, + label: Option<EcoString>, + value: &Value, + parens: bool, + docs: Option<&str>, + ) { + let label = label.unwrap_or_else(|| value.repr().into()); + let mut apply = None; + + if label.starts_with('"') && self.after.starts_with('"') { + if let Some(trimmed) = label.strip_suffix('"') { + apply = Some(trimmed.into()); + } + } + + let detail = docs.map(Into::into).or_else(|| match value { + Value::Symbol(_) => None, + Value::Func(func) => func.info().map(|info| plain_docs_sentence(info.docs)), + v => { + let repr = v.repr(); + (repr.as_str() != label).then(|| repr.into()) + } + }); + + if parens && matches!(value, Value::Func(_)) { + apply = Some(eco_format!("{label}(${{}})")); + } + + self.completions.push(Completion { + kind: match value { + Value::Func(_) => CompletionKind::Func, + Value::Symbol(s) => CompletionKind::Symbol(s.get()), + _ => CompletionKind::Constant, + }, + label, + apply, + detail, + }); + } + + /// Add completions for a castable. + fn cast_completions(&mut self, cast: &'a CastInfo) { + // Prevent duplicate completions from appearing. + if !self.seen_casts.insert(crate::util::hash128(cast)) { + return; + } + + match cast { + CastInfo::Any => {} + CastInfo::Value(value, docs) => { + self.value_completion(None, value, true, Some(docs)); + } + CastInfo::Type("none") => self.snippet_completion("none", "none", "Nothing."), + CastInfo::Type("auto") => { + self.snippet_completion("auto", "auto", "A smart default."); + } + CastInfo::Type("boolean") => { + self.snippet_completion("false", "false", "No / Disabled."); + self.snippet_completion("true", "true", "Yes / Enabled."); + } + CastInfo::Type("color") => { + self.snippet_completion( + "luma()", + "luma(${v})", + "A custom grayscale color.", + ); + self.snippet_completion( + "rgb()", + "rgb(${r}, ${g}, ${b}, ${a})", + "A custom RGBA color.", + ); + self.snippet_completion( + "cmyk()", + "cmyk(${c}, ${m}, ${y}, ${k})", + "A custom CMYK color.", + ); + self.scope_completions(false, |value| value.type_name() == "color"); + } + CastInfo::Type("function") => { + self.snippet_completion( + "function", + "(${params}) => ${output}", + "A custom function.", + ); + } + CastInfo::Type(ty) => { + self.completions.push(Completion { + kind: CompletionKind::Syntax, + label: (*ty).into(), + apply: Some(eco_format!("${{{ty}}}")), + detail: Some(eco_format!("A value of type {ty}.")), + }); + self.scope_completions(false, |value| value.type_name() == *ty); + } + CastInfo::Union(union) => { + for info in union { + self.cast_completions(info); + } + } + } + } + + /// Add completions for definitions that are available at the cursor. + /// Filters the global/math scope with the given filter. + fn scope_completions(&mut self, parens: bool, filter: impl Fn(&Value) -> bool) { + let mut defined = BTreeSet::new(); + + let mut ancestor = Some(self.leaf.clone()); + while let Some(node) = &ancestor { + let mut sibling = Some(node.clone()); + while let Some(node) = &sibling { + if let Some(v) = node.cast::<ast::LetBinding>() { + for ident in v.kind().idents() { + defined.insert(ident.take()); + } + } + sibling = node.prev_sibling(); + } + + if let Some(parent) = node.parent() { + if let Some(v) = parent.cast::<ast::ForLoop>() { + if node.prev_sibling_kind() != Some(SyntaxKind::In) { + let pattern = v.pattern(); + for ident in pattern.idents() { + defined.insert(ident.take()); + } + } + } + + ancestor = Some(parent.clone()); + continue; + } + + break; + } + + let in_math = matches!( + self.leaf.parent_kind(), + Some(SyntaxKind::Equation) + | Some(SyntaxKind::Math) + | Some(SyntaxKind::MathFrac) + | Some(SyntaxKind::MathAttach) + ); + + let scope = if in_math { self.math } else { self.global }; + for (name, value) in scope.iter() { + if filter(value) && !defined.contains(name) { + self.value_completion(Some(name.clone()), value, parens, None); + } + } + + for name in defined { + if !name.is_empty() { + self.completions.push(Completion { + kind: CompletionKind::Constant, + label: name, + apply: None, + detail: None, + }); + } + } + } +} diff --git a/crates/typst/src/ide/highlight.rs b/crates/typst/src/ide/highlight.rs new file mode 100644 index 00000000..2db636e3 --- /dev/null +++ b/crates/typst/src/ide/highlight.rs @@ -0,0 +1,430 @@ +use crate::syntax::{ast, LinkedNode, SyntaxKind, SyntaxNode}; + +/// A syntax highlighting tag. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Tag { + /// A line or block comment. + Comment, + /// Punctuation in code. + Punctuation, + /// An escape sequence or shorthand. + Escape, + /// Strong markup. + Strong, + /// Emphasized markup. + Emph, + /// A hyperlink. + Link, + /// Raw text. + Raw, + /// A label. + Label, + /// A reference to a label. + Ref, + /// A section heading. + Heading, + /// A marker of a list, enumeration, or term list. + ListMarker, + /// A term in a term list. + ListTerm, + /// The delimiters of an equation. + MathDelimiter, + /// An operator with special meaning in an equation. + MathOperator, + /// A keyword. + Keyword, + /// An operator in code. + Operator, + /// A numeric literal. + Number, + /// A string literal. + String, + /// A function or method name. + Function, + /// An interpolated variable in markup or math. + Interpolated, + /// A syntax error. + Error, +} + +impl Tag { + /// Return the recommended TextMate grammar scope for the given highlighting + /// tag. + pub fn tm_scope(&self) -> &'static str { + match self { + Self::Comment => "comment.typst", + Self::Punctuation => "punctuation.typst", + Self::Escape => "constant.character.escape.typst", + Self::Strong => "markup.bold.typst", + Self::Emph => "markup.italic.typst", + Self::Link => "markup.underline.link.typst", + Self::Raw => "markup.raw.typst", + Self::MathDelimiter => "punctuation.definition.math.typst", + Self::MathOperator => "keyword.operator.math.typst", + Self::Heading => "markup.heading.typst", + Self::ListMarker => "punctuation.definition.list.typst", + Self::ListTerm => "markup.list.term.typst", + Self::Label => "entity.name.label.typst", + Self::Ref => "markup.other.reference.typst", + Self::Keyword => "keyword.typst", + Self::Operator => "keyword.operator.typst", + Self::Number => "constant.numeric.typst", + Self::String => "string.quoted.double.typst", + Self::Function => "entity.name.function.typst", + Self::Interpolated => "meta.interpolation.typst", + Self::Error => "invalid.typst", + } + } + + /// The recommended CSS class for the highlighting tag. + pub fn css_class(self) -> &'static str { + match self { + Self::Comment => "typ-comment", + Self::Punctuation => "typ-punct", + Self::Escape => "typ-escape", + Self::Strong => "typ-strong", + Self::Emph => "typ-emph", + Self::Link => "typ-link", + Self::Raw => "typ-raw", + Self::Label => "typ-label", + Self::Ref => "typ-ref", + Self::Heading => "typ-heading", + Self::ListMarker => "typ-marker", + Self::ListTerm => "typ-term", + Self::MathDelimiter => "typ-math-delim", + Self::MathOperator => "typ-math-op", + Self::Keyword => "typ-key", + Self::Operator => "typ-op", + Self::Number => "typ-num", + Self::String => "typ-str", + Self::Function => "typ-func", + Self::Interpolated => "typ-pol", + Self::Error => "typ-error", + } + } +} + +/// Determine the highlight tag of a linked syntax node. +/// +/// Returns `None` if the node should not be highlighted. +pub fn highlight(node: &LinkedNode) -> Option<Tag> { + match node.kind() { + SyntaxKind::Markup + if node.parent_kind() == Some(SyntaxKind::TermItem) + && node.next_sibling_kind() == Some(SyntaxKind::Colon) => + { + Some(Tag::ListTerm) + } + SyntaxKind::Markup => None, + SyntaxKind::Text => None, + SyntaxKind::Space => None, + SyntaxKind::Linebreak => Some(Tag::Escape), + SyntaxKind::Parbreak => None, + SyntaxKind::Escape => Some(Tag::Escape), + SyntaxKind::Shorthand => Some(Tag::Escape), + SyntaxKind::SmartQuote => None, + SyntaxKind::Strong => Some(Tag::Strong), + SyntaxKind::Emph => Some(Tag::Emph), + SyntaxKind::Raw => Some(Tag::Raw), + SyntaxKind::Link => Some(Tag::Link), + SyntaxKind::Label => Some(Tag::Label), + SyntaxKind::Ref => Some(Tag::Ref), + SyntaxKind::RefMarker => None, + SyntaxKind::Heading => Some(Tag::Heading), + SyntaxKind::HeadingMarker => None, + SyntaxKind::ListItem => None, + SyntaxKind::ListMarker => Some(Tag::ListMarker), + SyntaxKind::EnumItem => None, + SyntaxKind::EnumMarker => Some(Tag::ListMarker), + SyntaxKind::TermItem => None, + SyntaxKind::TermMarker => Some(Tag::ListMarker), + SyntaxKind::Equation => None, + + SyntaxKind::Math => None, + SyntaxKind::MathIdent => highlight_ident(node), + SyntaxKind::MathAlignPoint => Some(Tag::MathOperator), + SyntaxKind::MathDelimited => None, + SyntaxKind::MathAttach => None, + SyntaxKind::MathFrac => None, + SyntaxKind::MathRoot => None, + + SyntaxKind::Hashtag => highlight_hashtag(node), + SyntaxKind::LeftBrace => Some(Tag::Punctuation), + SyntaxKind::RightBrace => Some(Tag::Punctuation), + SyntaxKind::LeftBracket => Some(Tag::Punctuation), + SyntaxKind::RightBracket => Some(Tag::Punctuation), + SyntaxKind::LeftParen => Some(Tag::Punctuation), + SyntaxKind::RightParen => Some(Tag::Punctuation), + SyntaxKind::Comma => Some(Tag::Punctuation), + SyntaxKind::Semicolon => Some(Tag::Punctuation), + SyntaxKind::Colon => Some(Tag::Punctuation), + SyntaxKind::Star => match node.parent_kind() { + Some(SyntaxKind::Strong) => None, + _ => Some(Tag::Operator), + }, + SyntaxKind::Underscore => match node.parent_kind() { + Some(SyntaxKind::MathAttach) => Some(Tag::MathOperator), + _ => None, + }, + SyntaxKind::Dollar => Some(Tag::MathDelimiter), + SyntaxKind::Plus => Some(Tag::Operator), + SyntaxKind::Minus => Some(Tag::Operator), + SyntaxKind::Slash => Some(match node.parent_kind() { + Some(SyntaxKind::MathFrac) => Tag::MathOperator, + _ => Tag::Operator, + }), + SyntaxKind::Hat => Some(Tag::MathOperator), + SyntaxKind::Dot => Some(Tag::Punctuation), + SyntaxKind::Eq => match node.parent_kind() { + Some(SyntaxKind::Heading) => None, + _ => Some(Tag::Operator), + }, + SyntaxKind::EqEq => Some(Tag::Operator), + SyntaxKind::ExclEq => Some(Tag::Operator), + SyntaxKind::Lt => Some(Tag::Operator), + SyntaxKind::LtEq => Some(Tag::Operator), + SyntaxKind::Gt => Some(Tag::Operator), + SyntaxKind::GtEq => Some(Tag::Operator), + SyntaxKind::PlusEq => Some(Tag::Operator), + SyntaxKind::HyphEq => Some(Tag::Operator), + SyntaxKind::StarEq => Some(Tag::Operator), + SyntaxKind::SlashEq => Some(Tag::Operator), + SyntaxKind::Dots => Some(Tag::Operator), + SyntaxKind::Arrow => Some(Tag::Operator), + SyntaxKind::Root => Some(Tag::MathOperator), + + SyntaxKind::Not => Some(Tag::Keyword), + SyntaxKind::And => Some(Tag::Keyword), + SyntaxKind::Or => Some(Tag::Keyword), + SyntaxKind::None => Some(Tag::Keyword), + SyntaxKind::Auto => Some(Tag::Keyword), + SyntaxKind::Let => Some(Tag::Keyword), + SyntaxKind::Set => Some(Tag::Keyword), + SyntaxKind::Show => Some(Tag::Keyword), + SyntaxKind::If => Some(Tag::Keyword), + SyntaxKind::Else => Some(Tag::Keyword), + SyntaxKind::For => Some(Tag::Keyword), + SyntaxKind::In => Some(Tag::Keyword), + SyntaxKind::While => Some(Tag::Keyword), + SyntaxKind::Break => Some(Tag::Keyword), + SyntaxKind::Continue => Some(Tag::Keyword), + SyntaxKind::Return => Some(Tag::Keyword), + SyntaxKind::Import => Some(Tag::Keyword), + SyntaxKind::Include => Some(Tag::Keyword), + SyntaxKind::As => Some(Tag::Keyword), + + SyntaxKind::Code => None, + SyntaxKind::Ident => highlight_ident(node), + SyntaxKind::Bool => Some(Tag::Keyword), + SyntaxKind::Int => Some(Tag::Number), + SyntaxKind::Float => Some(Tag::Number), + SyntaxKind::Numeric => Some(Tag::Number), + SyntaxKind::Str => Some(Tag::String), + SyntaxKind::CodeBlock => None, + SyntaxKind::ContentBlock => None, + SyntaxKind::Parenthesized => None, + SyntaxKind::Array => None, + SyntaxKind::Dict => None, + SyntaxKind::Named => None, + SyntaxKind::Keyed => None, + SyntaxKind::Unary => None, + SyntaxKind::Binary => None, + SyntaxKind::FieldAccess => None, + SyntaxKind::FuncCall => None, + SyntaxKind::Args => None, + SyntaxKind::Spread => None, + SyntaxKind::Closure => None, + SyntaxKind::Params => None, + SyntaxKind::LetBinding => None, + SyntaxKind::SetRule => None, + SyntaxKind::ShowRule => None, + SyntaxKind::Conditional => None, + SyntaxKind::WhileLoop => None, + SyntaxKind::ForLoop => None, + SyntaxKind::ModuleImport => None, + SyntaxKind::ImportItems => None, + SyntaxKind::ModuleInclude => None, + SyntaxKind::LoopBreak => None, + SyntaxKind::LoopContinue => None, + SyntaxKind::FuncReturn => None, + SyntaxKind::Destructuring => None, + SyntaxKind::DestructAssignment => None, + + SyntaxKind::LineComment => Some(Tag::Comment), + SyntaxKind::BlockComment => Some(Tag::Comment), + SyntaxKind::Error => Some(Tag::Error), + SyntaxKind::Eof => None, + } +} + +/// Highlight an identifier based on context. +fn highlight_ident(node: &LinkedNode) -> Option<Tag> { + // Are we directly before an argument list? + let next_leaf = node.next_leaf(); + if let Some(next) = &next_leaf { + if node.range().end == next.offset() + && ((next.kind() == SyntaxKind::LeftParen + && matches!( + next.parent_kind(), + Some(SyntaxKind::Args | SyntaxKind::Params) + )) + || (next.kind() == SyntaxKind::LeftBracket + && next.parent_kind() == Some(SyntaxKind::ContentBlock))) + { + return Some(Tag::Function); + } + } + + // Are we in math? + if node.kind() == SyntaxKind::MathIdent { + return Some(Tag::Interpolated); + } + + // Find the first non-field access ancestor. + let mut ancestor = node; + while ancestor.parent_kind() == Some(SyntaxKind::FieldAccess) { + ancestor = ancestor.parent()?; + } + + // Are we directly before or behind a show rule colon? + if ancestor.parent_kind() == Some(SyntaxKind::ShowRule) + && (next_leaf.map(|leaf| leaf.kind()) == Some(SyntaxKind::Colon) + || node.prev_leaf().map(|leaf| leaf.kind()) == Some(SyntaxKind::Colon)) + { + return Some(Tag::Function); + } + + // Are we (or an ancestor field access) directly after a hashtag. + if ancestor.prev_leaf().map(|leaf| leaf.kind()) == Some(SyntaxKind::Hashtag) { + return Some(Tag::Interpolated); + } + + // Are we behind a dot, that is behind another identifier? + let prev = node.prev_leaf()?; + if prev.kind() == SyntaxKind::Dot { + let prev_prev = prev.prev_leaf()?; + if is_ident(&prev_prev) { + return highlight_ident(&prev_prev); + } + } + + None +} + +/// Highlight a hashtag based on context. +fn highlight_hashtag(node: &LinkedNode) -> Option<Tag> { + let next = node.next_sibling()?; + let expr = next.cast::<ast::Expr>()?; + if !expr.hashtag() { + return None; + } + highlight(&next.leftmost_leaf()?) +} + +/// Whether the node is one of the two identifier nodes. +fn is_ident(node: &LinkedNode) -> bool { + matches!(node.kind(), SyntaxKind::Ident | SyntaxKind::MathIdent) +} + +/// Highlight a node to an HTML `code` element. +/// +/// This uses these [CSS classes for categories](Tag::css_class). +pub fn highlight_html(root: &SyntaxNode) -> String { + let mut buf = String::from("<code>"); + let node = LinkedNode::new(root); + highlight_html_impl(&mut buf, &node); + buf.push_str("</code>"); + buf +} + +/// Highlight one source node, emitting HTML. +fn highlight_html_impl(html: &mut String, node: &LinkedNode) { + let mut span = false; + if let Some(tag) = highlight(node) { + if tag != Tag::Error { + span = true; + html.push_str("<span class=\""); + html.push_str(tag.css_class()); + html.push_str("\">"); + } + } + + let text = node.text(); + if !text.is_empty() { + for c in text.chars() { + match c { + '<' => html.push_str("<"), + '>' => html.push_str(">"), + '&' => html.push_str("&"), + '\'' => html.push_str("'"), + '"' => html.push_str("""), + _ => html.push(c), + } + } + } else { + for child in node.children() { + highlight_html_impl(html, &child); + } + } + + if span { + html.push_str("</span>"); + } +} + +#[cfg(test)] +mod tests { + use std::ops::Range; + + use super::*; + use crate::syntax::Source; + + #[test] + fn test_highlighting() { + use Tag::*; + + #[track_caller] + fn test(text: &str, goal: &[(Range<usize>, Tag)]) { + let mut vec = vec![]; + let source = Source::detached(text); + highlight_tree(&mut vec, &LinkedNode::new(source.root())); + assert_eq!(vec, goal); + } + + fn highlight_tree(tags: &mut Vec<(Range<usize>, Tag)>, node: &LinkedNode) { + if let Some(tag) = highlight(node) { + tags.push((node.range(), tag)); + } + + for child in node.children() { + highlight_tree(tags, &child); + } + } + + test("= *AB*", &[(0..6, Heading), (2..6, Strong)]); + + test( + "#f(x + 1)", + &[ + (0..1, Function), + (1..2, Function), + (2..3, Punctuation), + (5..6, Operator), + (7..8, Number), + (8..9, Punctuation), + ], + ); + + test( + "#let f(x) = x", + &[ + (0..1, Keyword), + (1..4, Keyword), + (5..6, Function), + (6..7, Punctuation), + (8..9, Punctuation), + (10..11, Operator), + ], + ); + } +} diff --git a/crates/typst/src/ide/jump.rs b/crates/typst/src/ide/jump.rs new file mode 100644 index 00000000..14a82e26 --- /dev/null +++ b/crates/typst/src/ide/jump.rs @@ -0,0 +1,173 @@ +use std::num::NonZeroUsize; + +use ecow::EcoString; + +use crate::doc::{Destination, Frame, FrameItem, Meta, Position}; +use crate::file::FileId; +use crate::geom::{Geometry, Point, Size}; +use crate::model::Introspector; +use crate::syntax::{LinkedNode, Source, Span, SyntaxKind}; +use crate::World; + +/// Where to [jump](jump_from_click) to. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Jump { + /// Jump to a position in a source file. + Source(FileId, usize), + /// Jump to an external URL. + Url(EcoString), + /// Jump to a point on a page. + Position(Position), +} + +impl Jump { + fn from_span(world: &dyn World, span: Span) -> Option<Self> { + let source = world.source(span.id()).ok()?; + let node = source.find(span)?; + Some(Self::Source(span.id(), node.offset())) + } +} + +/// Determine where to jump to based on a click in a frame. +pub fn jump_from_click( + world: &dyn World, + frames: &[Frame], + frame: &Frame, + click: Point, +) -> Option<Jump> { + let mut introspector = None; + + // Try to find a link first. + for (pos, item) in frame.items() { + if let FrameItem::Meta(Meta::Link(dest), size) = item { + if is_in_rect(*pos, *size, click) { + return Some(match dest { + Destination::Url(url) => Jump::Url(url.clone()), + Destination::Position(pos) => Jump::Position(*pos), + Destination::Location(loc) => Jump::Position( + introspector + .get_or_insert_with(|| Introspector::new(frames)) + .position(*loc), + ), + }); + } + } + } + + // If there's no link, search for a jump target. + for (mut pos, item) in frame.items().rev() { + match item { + FrameItem::Group(group) => { + // TODO: Handle transformation. + if let Some(span) = + jump_from_click(world, frames, &group.frame, click - pos) + { + return Some(span); + } + } + + FrameItem::Text(text) => { + for glyph in &text.glyphs { + let (span, span_offset) = glyph.span; + if span.is_detached() { + continue; + } + + let width = glyph.x_advance.at(text.size); + if is_in_rect( + Point::new(pos.x, pos.y - text.size), + Size::new(width, text.size), + click, + ) { + let source = world.source(span.id()).ok()?; + let node = source.find(span)?; + let pos = if node.kind() == SyntaxKind::Text { + let range = node.range(); + let mut offset = range.start + usize::from(span_offset); + if (click.x - pos.x) > width / 2.0 { + offset += glyph.range().len(); + } + offset.min(range.end) + } else { + node.offset() + }; + return Some(Jump::Source(source.id(), pos)); + } + + pos.x += width; + } + } + + FrameItem::Shape(shape, span) => { + let Geometry::Rect(size) = shape.geometry else { continue }; + if is_in_rect(pos, size, click) { + return Jump::from_span(world, *span); + } + } + + FrameItem::Image(_, size, span) if is_in_rect(pos, *size, click) => { + return Jump::from_span(world, *span); + } + + _ => {} + } + } + + None +} + +/// Find the output location in the document for a cursor position. +pub fn jump_from_cursor( + frames: &[Frame], + source: &Source, + cursor: usize, +) -> Option<Position> { + let node = LinkedNode::new(source.root()).leaf_at(cursor)?; + if node.kind() != SyntaxKind::Text { + return None; + } + + let span = node.span(); + for (i, frame) in frames.iter().enumerate() { + if let Some(pos) = find_in_frame(frame, span) { + return Some(Position { + page: NonZeroUsize::new(i + 1).unwrap(), + point: pos, + }); + } + } + + None +} + +/// Find the position of a span in a frame. +fn find_in_frame(frame: &Frame, span: Span) -> Option<Point> { + for (mut pos, item) in frame.items() { + if let FrameItem::Group(group) = item { + // TODO: Handle transformation. + if let Some(point) = find_in_frame(&group.frame, span) { + return Some(point + pos); + } + } + + if let FrameItem::Text(text) = item { + for glyph in &text.glyphs { + if glyph.span.0 == span { + return Some(pos); + } + pos.x += glyph.x_advance.at(text.size); + } + } + } + + None +} + +/// Whether a rectangle with the given size at the given position contains the +/// click position. +fn is_in_rect(pos: Point, size: Size, click: Point) -> bool { + pos.x <= click.x + && pos.x + size.x >= click.x + && pos.y <= click.y + && pos.y + size.y >= click.y +} diff --git a/crates/typst/src/ide/mod.rs b/crates/typst/src/ide/mod.rs new file mode 100644 index 00000000..4b08b66b --- /dev/null +++ b/crates/typst/src/ide/mod.rs @@ -0,0 +1,97 @@ +//! Capabilities for IDE support. + +mod analyze; +mod complete; +mod highlight; +mod jump; +mod tooltip; + +pub use self::analyze::analyze_labels; +pub use self::complete::{autocomplete, Completion, CompletionKind}; +pub use self::highlight::{highlight, highlight_html, Tag}; +pub use self::jump::{jump_from_click, jump_from_cursor, Jump}; +pub use self::tooltip::{tooltip, Tooltip}; + +use std::fmt::Write; + +use ecow::{eco_format, EcoString}; + +use self::analyze::*; +use crate::font::{FontInfo, FontStyle}; + +/// Extract the first sentence of plain text of a piece of documentation. +/// +/// Removes Markdown formatting. +fn plain_docs_sentence(docs: &str) -> EcoString { + let mut s = unscanny::Scanner::new(docs); + let mut output = EcoString::new(); + let mut link = false; + while let Some(c) = s.eat() { + match c { + '`' => { + let mut raw = s.eat_until('`'); + if (raw.starts_with('{') && raw.ends_with('}')) + || (raw.starts_with('[') && raw.ends_with(']')) + { + raw = &raw[1..raw.len() - 1]; + } + + s.eat(); + output.push('`'); + output.push_str(raw); + output.push('`'); + } + '[' => link = true, + ']' if link => { + if s.eat_if('(') { + s.eat_until(')'); + s.eat(); + } else if s.eat_if('[') { + s.eat_until(']'); + s.eat(); + } + link = false + } + '*' | '_' => {} + '.' => { + output.push('.'); + break; + } + _ => output.push(c), + } + } + + output +} + +/// Create a short description of a font family. +fn summarize_font_family<'a>(variants: impl Iterator<Item = &'a FontInfo>) -> EcoString { + let mut infos: Vec<_> = variants.collect(); + infos.sort_by_key(|info| info.variant); + + let mut has_italic = false; + let mut min_weight = u16::MAX; + let mut max_weight = 0; + for info in &infos { + let weight = info.variant.weight.to_number(); + has_italic |= info.variant.style == FontStyle::Italic; + min_weight = min_weight.min(weight); + max_weight = min_weight.max(weight); + } + + let count = infos.len(); + let s = if count == 1 { "" } else { "s" }; + let mut detail = eco_format!("{count} variant{s}."); + + if min_weight == max_weight { + write!(detail, " Weight {min_weight}.").unwrap(); + } else { + write!(detail, " Weights {min_weight}–{max_weight}.").unwrap(); + } + + if has_italic { + detail.push_str(" Has italics."); + } + + detail +} diff --git a/crates/typst/src/ide/tooltip.rs b/crates/typst/src/ide/tooltip.rs new file mode 100644 index 00000000..35125e92 --- /dev/null +++ b/crates/typst/src/ide/tooltip.rs @@ -0,0 +1,222 @@ +use std::fmt::Write; + +use ecow::{eco_format, EcoString}; + +use if_chain::if_chain; + +use super::analyze::analyze_labels; +use super::{analyze_expr, plain_docs_sentence, summarize_font_family}; +use crate::doc::Frame; +use crate::eval::{CastInfo, Tracer, Value}; +use crate::geom::{round_2, Length, Numeric}; +use crate::syntax::{ast, LinkedNode, Source, SyntaxKind}; +use crate::util::pretty_comma_list; +use crate::World; + +/// Describe the item under the cursor. +pub fn tooltip( + world: &(dyn World + 'static), + frames: &[Frame], + source: &Source, + cursor: usize, +) -> Option<Tooltip> { + let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?; + if leaf.kind().is_trivia() { + return None; + } + + named_param_tooltip(world, &leaf) + .or_else(|| font_tooltip(world, &leaf)) + .or_else(|| ref_tooltip(world, frames, &leaf)) + .or_else(|| expr_tooltip(world, &leaf)) +} + +/// A hover tooltip. +#[derive(Debug, Clone)] +pub enum Tooltip { + /// A string of text. + Text(EcoString), + /// A string of Typst code. + Code(EcoString), +} + +/// Tooltip for a hovered expression. +fn expr_tooltip(world: &(dyn World + 'static), leaf: &LinkedNode) -> Option<Tooltip> { + let mut ancestor = leaf; + while !ancestor.is::<ast::Expr>() { + ancestor = ancestor.parent()?; + } + + let expr = ancestor.cast::<ast::Expr>()?; + if !expr.hashtag() && !matches!(expr, ast::Expr::MathIdent(_)) { + return None; + } + + let values = analyze_expr(world, ancestor); + + if let [value] = values.as_slice() { + if let Some(docs) = value.docs() { + return Some(Tooltip::Text(plain_docs_sentence(docs))); + } + + if let &Value::Length(length) = value { + if let Some(tooltip) = length_tooltip(length) { + return Some(tooltip); + } + } + } + + if expr.is_literal() { + return None; + } + + let mut last = None; + let mut pieces: Vec<EcoString> = vec![]; + let mut iter = values.iter(); + for value in (&mut iter).take(Tracer::MAX - 1) { + if let Some((prev, count)) = &mut last { + if *prev == value { + *count += 1; + continue; + } else if *count > 1 { + write!(pieces.last_mut().unwrap(), " (x{count})").unwrap(); + } + } + pieces.push(value.repr().into()); + last = Some((value, 1)); + } + + if let Some((_, count)) = last { + if count > 1 { + write!(pieces.last_mut().unwrap(), " (x{count})").unwrap(); + } + } + + if iter.next().is_some() { + pieces.push("...".into()); + } + + let tooltip = pretty_comma_list(&pieces, false); + (!tooltip.is_empty()).then(|| Tooltip::Code(tooltip.into())) +} + +/// Tooltip text for a hovered length. +fn length_tooltip(length: Length) -> Option<Tooltip> { + length.em.is_zero().then(|| { + Tooltip::Code(eco_format!( + "{}pt = {}mm = {}cm = {}in", + round_2(length.abs.to_pt()), + round_2(length.abs.to_mm()), + round_2(length.abs.to_cm()), + round_2(length.abs.to_inches()) + )) + }) +} + +/// Tooltip for a hovered reference. +fn ref_tooltip( + world: &(dyn World + 'static), + frames: &[Frame], + leaf: &LinkedNode, +) -> Option<Tooltip> { + if leaf.kind() != SyntaxKind::RefMarker { + return None; + } + + let target = leaf.text().trim_start_matches('@'); + for (label, detail) in analyze_labels(world, frames).0 { + if label.0 == target { + return Some(Tooltip::Text(detail?)); + } + } + + None +} + +/// Tooltips for components of a named parameter. +fn named_param_tooltip( + world: &(dyn World + 'static), + leaf: &LinkedNode, +) -> Option<Tooltip> { + let (info, named) = if_chain! { + // Ensure that we are in a named pair in the arguments to a function + // call or set rule. + if let Some(parent) = leaf.parent(); + if let Some(named) = parent.cast::<ast::Named>(); + if let Some(grand) = parent.parent(); + if matches!(grand.kind(), SyntaxKind::Args); + if let Some(grand_grand) = grand.parent(); + if let Some(expr) = grand_grand.cast::<ast::Expr>(); + if let Some(ast::Expr::Ident(callee)) = match expr { + ast::Expr::FuncCall(call) => Some(call.callee()), + ast::Expr::Set(set) => Some(set.target()), + _ => None, + }; + + // Find metadata about the function. + if let Some(Value::Func(func)) = world.library().global.scope().get(&callee); + if let Some(info) = func.info(); + then { (info, named) } + else { return None; } + }; + + // Hovering over the parameter name. + if_chain! { + if leaf.index() == 0; + if let Some(ident) = leaf.cast::<ast::Ident>(); + if let Some(param) = info.param(&ident); + then { + return Some(Tooltip::Text(plain_docs_sentence(param.docs))); + } + } + + // Hovering over a string parameter value. + if_chain! { + if let Some(string) = leaf.cast::<ast::Str>(); + if let Some(param) = info.param(&named.name()); + if let Some(docs) = find_string_doc(¶m.cast, &string.get()); + then { + return Some(Tooltip::Text(docs.into())); + } + } + + None +} + +/// Find documentation for a castable string. +fn find_string_doc(info: &CastInfo, string: &str) -> Option<&'static str> { + match info { + CastInfo::Value(Value::Str(s), docs) if s.as_str() == string => Some(docs), + CastInfo::Union(options) => { + options.iter().find_map(|option| find_string_doc(option, string)) + } + _ => None, + } +} + +/// Tooltip for font. +fn font_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option<Tooltip> { + if_chain! { + // Ensure that we are on top of a string. + if let Some(string) = leaf.cast::<ast::Str>(); + let lower = string.get().to_lowercase(); + + // Ensure that we are in the arguments to the text function. + if let Some(parent) = leaf.parent(); + if let Some(named) = parent.cast::<ast::Named>(); + if named.name().as_str() == "font"; + + // Find the font family. + if let Some((_, iter)) = world + .book() + .families() + .find(|&(family, _)| family.to_lowercase().as_str() == lower.as_str()); + + then { + let detail = summarize_font_family(iter); + return Some(Tooltip::Text(detail)); + } + }; + + None +} diff --git a/crates/typst/src/image.rs b/crates/typst/src/image.rs new file mode 100644 index 00000000..3a245c14 --- /dev/null +++ b/crates/typst/src/image.rs @@ -0,0 +1,449 @@ +//! Image handling. + +use std::cell::RefCell; +use std::collections::BTreeMap; +use std::fmt::{self, Debug, Formatter}; +use std::io; +use std::sync::Arc; + +use comemo::{Prehashed, Track, Tracked}; +use ecow::{EcoString, EcoVec}; +use image::codecs::gif::GifDecoder; +use image::codecs::jpeg::JpegDecoder; +use image::codecs::png::PngDecoder; +use image::io::Limits; +use image::{ImageDecoder, ImageResult}; +use usvg::{TreeParsing, TreeTextToPath}; + +use crate::diag::{format_xml_like_error, StrResult}; +use crate::font::Font; +use crate::geom::Axes; +use crate::util::Bytes; +use crate::World; + +/// A raster or vector image. +/// +/// Values of this type are cheap to clone and hash. +#[derive(Clone, Hash, Eq, PartialEq)] +pub struct Image(Arc<Prehashed<Repr>>); + +/// The internal representation. +#[derive(Hash)] +struct Repr { + /// The raw, undecoded image data. + data: Bytes, + /// The format of the encoded `buffer`. + format: ImageFormat, + /// The size of the image. + size: Axes<u32>, + /// A loader for fonts referenced by an image (currently, only applies to + /// SVG). + loader: PreparedLoader, + /// A text describing the image. + alt: Option<EcoString>, +} + +impl Image { + /// Create an image from a buffer and a format. + #[comemo::memoize] + pub fn new( + data: Bytes, + format: ImageFormat, + alt: Option<EcoString>, + ) -> StrResult<Self> { + let loader = PreparedLoader::default(); + let decoded = match format { + ImageFormat::Raster(format) => decode_raster(&data, format)?, + ImageFormat::Vector(VectorFormat::Svg) => { + decode_svg(&data, (&loader as &dyn SvgFontLoader).track())? + } + }; + + Ok(Self(Arc::new(Prehashed::new(Repr { + data, + format, + size: decoded.size(), + loader, + alt, + })))) + } + + /// Create a font-dependant image from a buffer and a format. + #[comemo::memoize] + pub fn with_fonts( + data: Bytes, + format: ImageFormat, + world: Tracked<dyn World + '_>, + fallback_family: Option<&str>, + alt: Option<EcoString>, + ) -> StrResult<Self> { + let loader = WorldLoader::new(world, fallback_family); + let decoded = match format { + ImageFormat::Raster(format) => decode_raster(&data, format)?, + ImageFormat::Vector(VectorFormat::Svg) => { + decode_svg(&data, (&loader as &dyn SvgFontLoader).track())? + } + }; + + Ok(Self(Arc::new(Prehashed::new(Repr { + data, + format, + size: decoded.size(), + loader: loader.into_prepared(), + alt, + })))) + } + + /// The raw image data. + pub fn data(&self) -> &Bytes { + &self.0.data + } + + /// The format of the image. + pub fn format(&self) -> ImageFormat { + self.0.format + } + + /// The size of the image in pixels. + pub fn size(&self) -> Axes<u32> { + self.0.size + } + + /// The width of the image in pixels. + pub fn width(&self) -> u32 { + self.size().x + } + + /// The height of the image in pixels. + pub fn height(&self) -> u32 { + self.size().y + } + + /// A text describing the image. + pub fn alt(&self) -> Option<&str> { + self.0.alt.as_deref() + } + + /// The decoded version of the image. + pub fn decoded(&self) -> Arc<DecodedImage> { + match self.format() { + ImageFormat::Raster(format) => decode_raster(self.data(), format), + ImageFormat::Vector(VectorFormat::Svg) => { + decode_svg(self.data(), (&self.0.loader as &dyn SvgFontLoader).track()) + } + } + .unwrap() + } +} + +impl Debug for Image { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.debug_struct("Image") + .field("format", &self.format()) + .field("width", &self.width()) + .field("height", &self.height()) + .field("alt", &self.alt()) + .finish() + } +} + +/// A raster or vector image format. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum ImageFormat { + /// A raster graphics format. + Raster(RasterFormat), + /// A vector graphics format. + Vector(VectorFormat), +} + +/// A raster graphics format. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum RasterFormat { + /// Raster format for illustrations and transparent graphics. + Png, + /// Lossy raster format suitable for photos. + Jpg, + /// Raster format that is typically used for short animated clips. + Gif, +} + +/// A vector graphics format. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum VectorFormat { + /// The vector graphics format of the web. + Svg, +} + +impl From<RasterFormat> for image::ImageFormat { + fn from(format: RasterFormat) -> Self { + match format { + RasterFormat::Png => image::ImageFormat::Png, + RasterFormat::Jpg => image::ImageFormat::Jpeg, + RasterFormat::Gif => image::ImageFormat::Gif, + } + } +} + +impl From<ttf_parser::RasterImageFormat> for RasterFormat { + fn from(format: ttf_parser::RasterImageFormat) -> Self { + match format { + ttf_parser::RasterImageFormat::PNG => RasterFormat::Png, + } + } +} + +impl From<ttf_parser::RasterImageFormat> for ImageFormat { + fn from(format: ttf_parser::RasterImageFormat) -> Self { + Self::Raster(format.into()) + } +} + +/// A decoded image. +pub enum DecodedImage { + /// A decoded pixel raster with its ICC profile. + Raster(image::DynamicImage, Option<IccProfile>, RasterFormat), + /// An decoded SVG tree. + Svg(usvg::Tree), +} + +impl DecodedImage { + /// The size of the image in pixels. + pub fn size(&self) -> Axes<u32> { + Axes::new(self.width(), self.height()) + } + + /// The width of the image in pixels. + pub fn width(&self) -> u32 { + match self { + Self::Raster(dynamic, _, _) => dynamic.width(), + Self::Svg(tree) => tree.size.width().ceil() as u32, + } + } + + /// The height of the image in pixels. + pub fn height(&self) -> u32 { + match self { + Self::Raster(dynamic, _, _) => dynamic.height(), + Self::Svg(tree) => tree.size.height().ceil() as u32, + } + } +} + +/// Raw data for of an ICC profile. +pub struct IccProfile(pub Vec<u8>); + +/// Decode a raster image. +#[comemo::memoize] +fn decode_raster(data: &Bytes, format: RasterFormat) -> StrResult<Arc<DecodedImage>> { + fn decode_with<'a, T: ImageDecoder<'a>>( + decoder: ImageResult<T>, + ) -> ImageResult<(image::DynamicImage, Option<IccProfile>)> { + let mut decoder = decoder?; + let icc = decoder.icc_profile().filter(|data| !data.is_empty()).map(IccProfile); + decoder.set_limits(Limits::default())?; + let dynamic = image::DynamicImage::from_decoder(decoder)?; + Ok((dynamic, icc)) + } + + let cursor = io::Cursor::new(data); + let (dynamic, icc) = match format { + RasterFormat::Jpg => decode_with(JpegDecoder::new(cursor)), + RasterFormat::Png => decode_with(PngDecoder::new(cursor)), + RasterFormat::Gif => decode_with(GifDecoder::new(cursor)), + } + .map_err(format_image_error)?; + + Ok(Arc::new(DecodedImage::Raster(dynamic, icc, format))) +} + +/// Decode an SVG image. +#[comemo::memoize] +fn decode_svg( + data: &Bytes, + loader: Tracked<dyn SvgFontLoader + '_>, +) -> StrResult<Arc<DecodedImage>> { + // Disable usvg's default to "Times New Roman". Instead, we default to + // the empty family and later, when we traverse the SVG, we check for + // empty and non-existing family names and replace them with the true + // fallback family. This way, we can memoize SVG decoding with and without + // fonts if the SVG does not contain text. + let opts = usvg::Options { font_family: String::new(), ..Default::default() }; + let mut tree = usvg::Tree::from_data(data, &opts).map_err(format_usvg_error)?; + if tree.has_text_nodes() { + let fontdb = load_svg_fonts(&tree, loader); + tree.convert_text(&fontdb); + } + Ok(Arc::new(DecodedImage::Svg(tree))) +} + +/// Discover and load the fonts referenced by an SVG. +fn load_svg_fonts( + tree: &usvg::Tree, + loader: Tracked<dyn SvgFontLoader + '_>, +) -> fontdb::Database { + let mut referenced = BTreeMap::<EcoString, bool>::new(); + let mut fontdb = fontdb::Database::new(); + let mut load = |family_cased: &str| { + let family = EcoString::from(family_cased.trim()).to_lowercase(); + if let Some(&success) = referenced.get(&family) { + return success; + } + + // We load all variants for the family, since we don't know which will + // be used. + let mut success = false; + for font in loader.load(&family) { + let source = Arc::new(font.data().clone()); + fontdb.load_font_source(fontdb::Source::Binary(source)); + success = true; + } + + referenced.insert(family, success); + success + }; + + // Load fallback family. + let fallback_cased = loader.fallback(); + if let Some(family_cased) = fallback_cased { + load(family_cased); + } + + // Find out which font families are referenced by the SVG. + traverse_svg(&tree.root, &mut |node| { + let usvg::NodeKind::Text(text) = &mut *node.borrow_mut() else { return }; + for chunk in &mut text.chunks { + for span in &mut chunk.spans { + for family_cased in &mut span.font.families { + if family_cased.is_empty() || !load(family_cased) { + let Some(fallback) = fallback_cased else { continue }; + *family_cased = fallback.into(); + } + } + } + } + }); + + fontdb +} + +/// Search for all font families referenced by an SVG. +fn traverse_svg<F>(node: &usvg::Node, f: &mut F) +where + F: FnMut(&usvg::Node), +{ + f(node); + for child in node.children() { + traverse_svg(&child, f); + } +} + +/// Interface for loading fonts for an SVG. +/// +/// Can be backed by a `WorldLoader` or a `PreparedLoader`. The first is used +/// when the image is initially decoded. It records all required fonts and +/// produces a `PreparedLoader` from it. This loader can then be used to +/// redecode the image with a cache hit from the initial decoding. This way, we +/// can cheaply access the decoded version of an image. +/// +/// The alternative would be to store the decoded image directly in the image, +/// but that would make `Image` not `Send` because `usvg::Tree` is not `Send`. +/// The current design also has the added benefit that large decoded images can +/// be evicted if they are not used anymore. +#[comemo::track] +trait SvgFontLoader { + /// Load all fonts for the given lowercased font family. + fn load(&self, lower_family: &str) -> EcoVec<Font>; + + /// The case-sensitive name of the fallback family. + fn fallback(&self) -> Option<&str>; +} + +/// Loads fonts for an SVG from a world +struct WorldLoader<'a> { + world: Tracked<'a, dyn World + 'a>, + seen: RefCell<BTreeMap<EcoString, EcoVec<Font>>>, + fallback_family_cased: Option<String>, +} + +impl<'a> WorldLoader<'a> { + fn new(world: Tracked<'a, dyn World + 'a>, fallback_family: Option<&str>) -> Self { + // Recover the non-lowercased version of the family because + // usvg is case sensitive. + let book = world.book(); + let fallback_family_cased = fallback_family + .and_then(|lowercase| book.select_family(lowercase).next()) + .and_then(|index| book.info(index)) + .map(|info| info.family.clone()); + + Self { + world, + fallback_family_cased, + seen: Default::default(), + } + } + + fn into_prepared(self) -> PreparedLoader { + PreparedLoader { + families: self.seen.into_inner(), + fallback_family_cased: self.fallback_family_cased, + } + } +} + +impl SvgFontLoader for WorldLoader<'_> { + fn load(&self, family: &str) -> EcoVec<Font> { + self.seen + .borrow_mut() + .entry(family.into()) + .or_insert_with(|| { + self.world + .book() + .select_family(family) + .filter_map(|id| self.world.font(id)) + .collect() + }) + .clone() + } + + fn fallback(&self) -> Option<&str> { + self.fallback_family_cased.as_deref() + } +} + +/// Loads fonts for an SVG from a prepared list. +#[derive(Default, Hash)] +struct PreparedLoader { + families: BTreeMap<EcoString, EcoVec<Font>>, + fallback_family_cased: Option<String>, +} + +impl SvgFontLoader for PreparedLoader { + fn load(&self, family: &str) -> EcoVec<Font> { + self.families.get(family).cloned().unwrap_or_default() + } + + fn fallback(&self) -> Option<&str> { + self.fallback_family_cased.as_deref() + } +} + +/// Format the user-facing raster graphic decoding error message. +fn format_image_error(error: image::ImageError) -> EcoString { + match error { + image::ImageError::Limits(_) => "file is too large".into(), + _ => "failed to decode image".into(), + } +} + +/// Format the user-facing SVG decoding error message. +fn format_usvg_error(error: usvg::Error) -> EcoString { + match error { + usvg::Error::NotAnUtf8Str => "file is not valid utf-8".into(), + usvg::Error::MalformedGZip => "file is not compressed correctly".into(), + usvg::Error::ElementsLimitReached => "file is too large".into(), + usvg::Error::InvalidSize => { + "failed to parse svg: width, height, or viewbox is invalid".into() + } + usvg::Error::ParsingFailed(error) => format_xml_like_error("svg", error), + } +} diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs new file mode 100644 index 00000000..8b3d1d3d --- /dev/null +++ b/crates/typst/src/lib.rs @@ -0,0 +1,147 @@ +//! The compiler for the _Typst_ markup language. +//! +//! # Steps +//! - **Parsing:** +//! The compiler first transforms a plain string into an iterator of [tokens]. +//! This token stream is [parsed] into a [syntax tree]. The tree itself is +//! untyped, but the [AST] module provides a typed layer over it. +//! - **Evaluation:** +//! The next step is to [evaluate] the markup. This produces a [module], +//! consisting of a scope of values that were exported by the code and +//! [content], a hierarchical, styled representation of what was written in +//! the source file. The elements of the content tree are well structured and +//! order-independent and thus much better suited for further processing than +//! the raw markup. +//! - **Typesetting:** +//! Next, the content is [typeset] into a [document] containing one [frame] +//! per page with items at fixed positions. +//! - **Exporting:** +//! These frames can finally be exported into an output format (currently +//! supported are [PDF] and [raster images]). +//! +//! [tokens]: syntax::SyntaxKind +//! [parsed]: syntax::parse +//! [syntax tree]: syntax::SyntaxNode +//! [AST]: syntax::ast +//! [evaluate]: eval::eval +//! [module]: eval::Module +//! [content]: model::Content +//! [typeset]: model::typeset +//! [document]: doc::Document +//! [frame]: doc::Frame +//! [PDF]: export::pdf +//! [raster images]: export::render + +#![recursion_limit = "1000"] +#![allow(clippy::comparison_chain)] + +extern crate self as typst; + +#[macro_use] +pub mod util; +#[macro_use] +pub mod diag; +#[macro_use] +pub mod eval; +pub mod doc; +pub mod export; +pub mod file; +pub mod font; +pub mod geom; +pub mod ide; +pub mod image; +pub mod model; +pub mod syntax; + +use comemo::{Prehashed, Track, TrackedMut}; +use ecow::EcoString; + +use crate::diag::{FileResult, SourceResult}; +use crate::doc::Document; +use crate::eval::{Datetime, Library, Route, Tracer}; +use crate::file::{FileId, PackageSpec}; +use crate::font::{Font, FontBook}; +use crate::syntax::Source; +use crate::util::Bytes; + +/// Compile a source file into a fully layouted document. +#[tracing::instrument(skip(world))] +pub fn compile(world: &dyn World) -> SourceResult<Document> { + let route = Route::default(); + let mut tracer = Tracer::default(); + + // Call `track` just once to keep comemo's ID stable. + let world = world.track(); + let mut tracer = tracer.track_mut(); + + // Evaluate the source file into a module. + tracing::info!("Starting evaluation"); + let module = eval::eval( + world, + route.track(), + TrackedMut::reborrow_mut(&mut tracer), + &world.main(), + )?; + + // Typeset the module's contents. + model::typeset(world, tracer, &module.content()) +} + +/// The environment in which typesetting occurs. +/// +/// All loading functions (`main`, `source`, `file`, `font`) should perform +/// internal caching so that they are relatively cheap on repeated invocations +/// with the same argument. [`Source`], [`Bytes`], and [`Font`] are +/// all reference-counted and thus cheap to clone. +/// +/// The compiler doesn't do the caching itself because the world has much more +/// information on when something can change. For example, fonts typically don't +/// change and can thus even be cached across multiple compilations (for +/// long-running applications like `typst watch`). Source files on the other +/// hand can change and should thus be cleared after. Advanced clients like +/// language servers can also retain the source files and [edited](Source::edit) +/// them in-place to benefit from better incremental performance. +#[comemo::track] +pub trait World { + /// The standard library. + fn library(&self) -> &Prehashed<Library>; + + /// Metadata about all known fonts. + fn book(&self) -> &Prehashed<FontBook>; + + /// Access the main source file. + fn main(&self) -> Source; + + /// Try to access the specified source file. + /// + /// The returned `Source` file's [id](Source::id) does not have to match the + /// given `id`. Due to symlinks, two different file id's can point to the + /// same on-disk file. Implementors can deduplicate and return the same + /// `Source` if they want to, but do not have to. + fn source(&self, id: FileId) -> FileResult<Source>; + + /// Try to access the specified file. + fn file(&self, id: FileId) -> FileResult<Bytes>; + + /// Try to access the font with the given index in the font book. + fn font(&self, index: usize) -> Option<Font>; + + /// Get the current date. + /// + /// If no offset is specified, the local date should be chosen. Otherwise, + /// the UTC date should be chosen with the corresponding offset in hours. + /// + /// If this function returns `None`, Typst's `datetime` function will + /// return an error. + fn today(&self, offset: Option<i64>) -> Option<Datetime>; + + /// A list of all available packages and optionally descriptions for them. + /// + /// This function is optional to implement. It enhances the user experience + /// by enabling autocompletion for packages. Details about packages from the + /// `@preview` namespace are available from + /// `https://packages.typst.org/preview/index.json`. + fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] { + &[] + } +} diff --git a/crates/typst/src/model/content.rs b/crates/typst/src/model/content.rs new file mode 100644 index 00000000..015f8b76 --- /dev/null +++ b/crates/typst/src/model/content.rs @@ -0,0 +1,614 @@ +use std::any::TypeId; +use std::fmt::{self, Debug, Formatter, Write}; +use std::iter::Sum; +use std::ops::{Add, AddAssign}; + +use comemo::Prehashed; +use ecow::{eco_format, EcoString, EcoVec}; + +use super::{ + element, Behave, Behaviour, ElemFunc, Element, Fold, Guard, Label, Locatable, + Location, Recipe, Selector, Style, Styles, Synthesize, +}; +use crate::diag::{SourceResult, StrResult}; +use crate::doc::Meta; +use crate::eval::{Dict, FromValue, IntoValue, Str, Value, Vm}; +use crate::syntax::Span; +use crate::util::pretty_array_like; + +/// Composable representation of styled content. +#[derive(Clone, Hash)] +#[allow(clippy::derived_hash_with_manual_eq)] +pub struct Content { + func: ElemFunc, + attrs: EcoVec<Attr>, +} + +/// Attributes that can be attached to content. +#[derive(Debug, Clone, PartialEq, Hash)] +enum Attr { + Span(Span), + Field(EcoString), + Value(Prehashed<Value>), + Child(Prehashed<Content>), + Styles(Styles), + Prepared, + Guard(Guard), + Location(Location), +} + +impl Content { + /// Create an empty element. + pub fn new(func: ElemFunc) -> Self { + Self { func, attrs: EcoVec::new() } + } + + /// Create empty content. + pub fn empty() -> Self { + Self::new(SequenceElem::func()) + } + + /// Create a new sequence element from multiples elements. + pub fn sequence(iter: impl IntoIterator<Item = Self>) -> Self { + let mut iter = iter.into_iter(); + let Some(first) = iter.next() else { return Self::empty() }; + let Some(second) = iter.next() else { return first }; + let mut content = Content::empty(); + content.attrs.push(Attr::Child(Prehashed::new(first))); + content.attrs.push(Attr::Child(Prehashed::new(second))); + content + .attrs + .extend(iter.map(|child| Attr::Child(Prehashed::new(child)))); + content + } + + /// The element function of the contained content. + pub fn func(&self) -> ElemFunc { + self.func + } + + /// Whether the content is an empty sequence. + pub fn is_empty(&self) -> bool { + self.is::<SequenceElem>() && self.attrs.is_empty() + } + + /// Whether the contained element is of type `T`. + pub fn is<T: Element>(&self) -> bool { + self.func == T::func() + } + + /// Cast to `T` if the contained element is of type `T`. + pub fn to<T: Element>(&self) -> Option<&T> { + T::unpack(self) + } + + /// Access the children if this is a sequence. + pub fn to_sequence(&self) -> Option<impl Iterator<Item = &Self>> { + if !self.is::<SequenceElem>() { + return None; + } + Some(self.attrs.iter().filter_map(Attr::child)) + } + + /// Access the child and styles. + pub fn to_styled(&self) -> Option<(&Content, &Styles)> { + if !self.is::<StyledElem>() { + return None; + } + let child = self.attrs.iter().find_map(Attr::child)?; + let styles = self.attrs.iter().find_map(Attr::styles)?; + Some((child, styles)) + } + + /// Whether the contained element has the given capability. + pub fn can<C>(&self) -> bool + where + C: ?Sized + 'static, + { + (self.func.0.vtable)(TypeId::of::<C>()).is_some() + } + + /// Whether the contained element has the given capability. + /// Where the capability is given by a `TypeId`. + pub fn can_type_id(&self, type_id: TypeId) -> bool { + (self.func.0.vtable)(type_id).is_some() + } + + /// Cast to a trait object if the contained element has the given + /// capability. + pub fn with<C>(&self) -> Option<&C> + where + C: ?Sized + 'static, + { + let vtable = (self.func.0.vtable)(TypeId::of::<C>())?; + let data = self as *const Self as *const (); + Some(unsafe { &*crate::util::fat::from_raw_parts(data, vtable) }) + } + + /// Cast to a mutable trait object if the contained element has the given + /// capability. + pub fn with_mut<C>(&mut self) -> Option<&mut C> + where + C: ?Sized + 'static, + { + let vtable = (self.func.0.vtable)(TypeId::of::<C>())?; + let data = self as *mut Self as *mut (); + Some(unsafe { &mut *crate::util::fat::from_raw_parts_mut(data, vtable) }) + } + + /// The content's span. + pub fn span(&self) -> Span { + self.attrs.iter().find_map(Attr::span).unwrap_or(Span::detached()) + } + + /// Attach a span to the content if it doesn't already have one. + pub fn spanned(mut self, span: Span) -> Self { + if self.span().is_detached() { + self.attrs.push(Attr::Span(span)); + } + self + } + + /// Attach a field to the content. + pub fn with_field( + mut self, + name: impl Into<EcoString>, + value: impl IntoValue, + ) -> Self { + self.push_field(name, value); + self + } + + /// Attach a field to the content. + pub fn push_field(&mut self, name: impl Into<EcoString>, value: impl IntoValue) { + let name = name.into(); + if let Some(i) = self.attrs.iter().position(|attr| match attr { + Attr::Field(field) => *field == name, + _ => false, + }) { + self.attrs.make_mut()[i + 1] = + Attr::Value(Prehashed::new(value.into_value())); + } else { + self.attrs.push(Attr::Field(name)); + self.attrs.push(Attr::Value(Prehashed::new(value.into_value()))); + } + } + + /// Access a field on the content. + pub fn field(&self, name: &str) -> Option<Value> { + if let (Some(iter), "children") = (self.to_sequence(), name) { + Some(Value::Array(iter.cloned().map(Value::Content).collect())) + } else if let (Some((child, _)), "child") = (self.to_styled(), name) { + Some(Value::Content(child.clone())) + } else { + self.field_ref(name).cloned() + } + } + + /// Access a field on the content by reference. + /// + /// Does not include synthesized fields for sequence and styled elements. + pub fn field_ref(&self, name: &str) -> Option<&Value> { + self.fields_ref() + .find(|&(field, _)| field == name) + .map(|(_, value)| value) + } + + /// Iter over all fields on the content. + /// + /// Does not include synthesized fields for sequence and styled elements. + pub fn fields(&self) -> impl Iterator<Item = (&EcoString, Value)> { + static CHILD: EcoString = EcoString::inline("child"); + static CHILDREN: EcoString = EcoString::inline("children"); + + let option = if let Some(iter) = self.to_sequence() { + Some((&CHILDREN, Value::Array(iter.cloned().map(Value::Content).collect()))) + } else if let Some((child, _)) = self.to_styled() { + Some((&CHILD, Value::Content(child.clone()))) + } else { + None + }; + + self.fields_ref() + .map(|(name, value)| (name, value.clone())) + .chain(option) + } + + /// Iter over all fields on the content. + /// + /// Does not include synthesized fields for sequence and styled elements. + pub fn fields_ref(&self) -> impl Iterator<Item = (&EcoString, &Value)> { + let mut iter = self.attrs.iter(); + std::iter::from_fn(move || { + let field = iter.find_map(Attr::field)?; + let value = iter.next()?.value()?; + Some((field, value)) + }) + } + + /// Try to access a field on the content as a specified type. + pub fn cast_field<T: FromValue>(&self, name: &str) -> Option<T> { + match self.field(name) { + Some(value) => value.cast().ok(), + None => None, + } + } + + /// Expect a field on the content to exist as a specified type. + #[track_caller] + pub fn expect_field<T: FromValue>(&self, name: &str) -> T { + self.field(name).unwrap().cast().unwrap() + } + + /// Whether the content has the specified field. + pub fn has(&self, field: &str) -> bool { + self.field(field).is_some() + } + + /// Borrow the value of the given field. + pub fn at(&self, field: &str, default: Option<Value>) -> StrResult<Value> { + self.field(field) + .or(default) + .ok_or_else(|| missing_field_no_default(field)) + } + + /// Return the fields of the content as a dict. + pub fn dict(&self) -> Dict { + self.fields() + .map(|(key, value)| (key.to_owned().into(), value)) + .collect() + } + + /// The content's label. + pub fn label(&self) -> Option<&Label> { + match self.field_ref("label")? { + Value::Label(label) => Some(label), + _ => None, + } + } + + /// Attach a label to the content. + pub fn labelled(self, label: Label) -> Self { + self.with_field("label", label) + } + + /// Style this content with a style entry. + pub fn styled(mut self, style: impl Into<Style>) -> Self { + if self.is::<StyledElem>() { + let prev = + self.attrs.make_mut().iter_mut().find_map(Attr::styles_mut).unwrap(); + prev.apply_one(style.into()); + self + } else { + self.styled_with_map(style.into().into()) + } + } + + /// Style this content with a full style map. + pub fn styled_with_map(mut self, styles: Styles) -> Self { + if styles.is_empty() { + return self; + } + + if self.is::<StyledElem>() { + let prev = + self.attrs.make_mut().iter_mut().find_map(Attr::styles_mut).unwrap(); + prev.apply(styles); + self + } else { + let mut content = Content::new(StyledElem::func()); + content.attrs.push(Attr::Child(Prehashed::new(self))); + content.attrs.push(Attr::Styles(styles)); + content + } + } + + /// Style this content with a recipe, eagerly applying it if possible. + pub fn styled_with_recipe(self, vm: &mut Vm, recipe: Recipe) -> SourceResult<Self> { + if recipe.selector.is_none() { + recipe.apply_vm(vm, self) + } else { + Ok(self.styled(recipe)) + } + } + + /// Repeat this content `count` times. + pub fn repeat(&self, count: usize) -> Self { + Self::sequence(vec![self.clone(); count]) + } + + /// Disable a show rule recipe. + pub fn guarded(mut self, guard: Guard) -> Self { + self.attrs.push(Attr::Guard(guard)); + self + } + + /// Check whether a show rule recipe is disabled. + pub fn is_guarded(&self, guard: Guard) -> bool { + self.attrs.contains(&Attr::Guard(guard)) + } + + /// Whether no show rule was executed for this content so far. + pub fn is_pristine(&self) -> bool { + !self.attrs.iter().any(|modifier| matches!(modifier, Attr::Guard(_))) + } + + /// Whether this content has already been prepared. + pub fn is_prepared(&self) -> bool { + self.attrs.contains(&Attr::Prepared) + } + + /// Mark this content as prepared. + pub fn mark_prepared(&mut self) { + self.attrs.push(Attr::Prepared); + } + + /// Whether the content needs to be realized specially. + pub fn needs_preparation(&self) -> bool { + (self.can::<dyn Locatable>() + || self.can::<dyn Synthesize>() + || self.label().is_some()) + && !self.is_prepared() + } + + /// This content's location in the document flow. + pub fn location(&self) -> Option<Location> { + self.attrs.iter().find_map(|modifier| match modifier { + Attr::Location(location) => Some(*location), + _ => None, + }) + } + + /// Attach a location to this content. + pub fn set_location(&mut self, location: Location) { + self.attrs.push(Attr::Location(location)); + } + + /// Queries the content tree for all elements that match the given selector. + /// + /// Elements produced in `show` rules will not be included in the results. + #[tracing::instrument(skip_all)] + pub fn query(&self, selector: Selector) -> Vec<&Content> { + let mut results = Vec::new(); + self.traverse(&mut |element| { + if selector.matches(element) { + results.push(element); + } + }); + results + } + + /// Queries the content tree for the first element that match the given + /// selector. + /// + /// Elements produced in `show` rules will not be included in the results. + #[tracing::instrument(skip_all)] + pub fn query_first(&self, selector: Selector) -> Option<&Content> { + let mut result = None; + self.traverse(&mut |element| { + if result.is_none() && selector.matches(element) { + result = Some(element); + } + }); + result + } + + /// Extracts the plain text of this content. + pub fn plain_text(&self) -> EcoString { + let mut text = EcoString::new(); + self.traverse(&mut |element| { + if let Some(textable) = element.with::<dyn PlainText>() { + textable.plain_text(&mut text); + } + }); + text + } + + /// Traverse this content. + fn traverse<'a, F>(&'a self, f: &mut F) + where + F: FnMut(&'a Content), + { + f(self); + + for attr in &self.attrs { + match attr { + Attr::Child(child) => child.traverse(f), + Attr::Value(value) => walk_value(value, f), + _ => {} + } + } + + /// Walks a given value to find any content that matches the selector. + fn walk_value<'a, F>(value: &'a Value, f: &mut F) + where + F: FnMut(&'a Content), + { + match value { + Value::Content(content) => content.traverse(f), + Value::Array(array) => { + for value in array { + walk_value(value, f); + } + } + _ => {} + } + } + } +} + +impl Debug for Content { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let name = self.func.name(); + if let Some(text) = item!(text_str)(self) { + f.write_char('[')?; + f.write_str(&text)?; + f.write_char(']')?; + return Ok(()); + } else if name == "space" { + return f.write_str("[ ]"); + } + + let mut pieces: Vec<_> = self + .fields() + .map(|(name, value)| eco_format!("{name}: {value:?}")) + .collect(); + + if self.is::<StyledElem>() { + pieces.push(EcoString::from("..")); + } + + f.write_str(name)?; + f.write_str(&pretty_array_like(&pieces, false)) + } +} + +impl Default for Content { + fn default() -> Self { + Self::empty() + } +} + +impl PartialEq for Content { + fn eq(&self, other: &Self) -> bool { + if let (Some(left), Some(right)) = (self.to_sequence(), other.to_sequence()) { + left.eq(right) + } else if let (Some(left), Some(right)) = (self.to_styled(), other.to_styled()) { + left == right + } else { + self.func == other.func && self.fields_ref().eq(other.fields_ref()) + } + } +} + +impl Add for Content { + type Output = Self; + + fn add(self, mut rhs: Self) -> Self::Output { + let mut lhs = self; + match (lhs.is::<SequenceElem>(), rhs.is::<SequenceElem>()) { + (true, true) => { + lhs.attrs.extend(rhs.attrs); + lhs + } + (true, false) => { + lhs.attrs.push(Attr::Child(Prehashed::new(rhs))); + lhs + } + (false, true) => { + rhs.attrs.insert(0, Attr::Child(Prehashed::new(lhs))); + rhs + } + (false, false) => Self::sequence([lhs, rhs]), + } + } +} + +impl AddAssign for Content { + fn add_assign(&mut self, rhs: Self) { + *self = std::mem::take(self) + rhs; + } +} + +impl Sum for Content { + fn sum<I: Iterator<Item = Self>>(iter: I) -> Self { + Self::sequence(iter) + } +} + +impl Attr { + fn child(&self) -> Option<&Content> { + match self { + Self::Child(child) => Some(child), + _ => None, + } + } + + fn styles(&self) -> Option<&Styles> { + match self { + Self::Styles(styles) => Some(styles), + _ => None, + } + } + + fn styles_mut(&mut self) -> Option<&mut Styles> { + match self { + Self::Styles(styles) => Some(styles), + _ => None, + } + } + + fn field(&self) -> Option<&EcoString> { + match self { + Self::Field(field) => Some(field), + _ => None, + } + } + + fn value(&self) -> Option<&Value> { + match self { + Self::Value(value) => Some(value), + _ => None, + } + } + + fn span(&self) -> Option<Span> { + match self { + Self::Span(span) => Some(*span), + _ => None, + } + } +} + +/// Display: Sequence +/// Category: special +#[element] +struct SequenceElem {} + +/// Display: Sequence +/// Category: special +#[element] +struct StyledElem {} + +/// Hosts metadata and ensures metadata is produced even for empty elements. +/// +/// Display: Meta +/// Category: special +#[element(Behave)] +pub struct MetaElem { + /// Metadata that should be attached to all elements affected by this style + /// property. + #[fold] + pub data: Vec<Meta>, +} + +impl Behave for MetaElem { + fn behaviour(&self) -> Behaviour { + Behaviour::Ignorant + } +} + +impl Fold for Vec<Meta> { + type Output = Self; + + fn fold(mut self, outer: Self::Output) -> Self::Output { + self.extend(outer); + self + } +} + +/// Tries to extract the plain-text representation of the element. +pub trait PlainText { + /// Write this element's plain text into the given buffer. + fn plain_text(&self, text: &mut EcoString); +} + +/// The missing key access error message when no default value was given. +#[cold] +fn missing_field_no_default(key: &str) -> EcoString { + eco_format!( + "content does not contain field {:?} and \ + no default value was specified", + Str::from(key) + ) +} diff --git a/crates/typst/src/model/element.rs b/crates/typst/src/model/element.rs new file mode 100644 index 00000000..c673ee41 --- /dev/null +++ b/crates/typst/src/model/element.rs @@ -0,0 +1,134 @@ +use std::any::TypeId; +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; + +use once_cell::sync::Lazy; + +use super::{Content, Selector, Styles}; +use crate::diag::SourceResult; +use crate::eval::{cast, Args, Dict, Func, FuncInfo, Value, Vm}; + +/// A document element. +pub trait Element: Construct + Set + Sized + 'static { + /// Pack the element into type-erased content. + fn pack(self) -> Content; + + /// Extract this element from type-erased content. + fn unpack(content: &Content) -> Option<&Self>; + + /// The element's function. + fn func() -> ElemFunc; +} + +/// An element's constructor function. +pub trait Construct { + /// Construct an element from the arguments. + /// + /// This is passed only the arguments that remain after execution of the + /// element's set rule. + fn construct(vm: &mut Vm, args: &mut Args) -> SourceResult<Content>; +} + +/// An element's set rule. +pub trait Set { + /// Parse relevant arguments into style properties for this element. + fn set(args: &mut Args) -> SourceResult<Styles>; +} + +/// An element's function. +#[derive(Copy, Clone)] +pub struct ElemFunc(pub(super) &'static NativeElemFunc); + +impl ElemFunc { + /// The function's name. + pub fn name(self) -> &'static str { + self.0.name + } + + /// Apply the given arguments to the function. + pub fn with(self, args: Args) -> Func { + Func::from(self).with(args) + } + + /// Extract details about the function. + pub fn info(&self) -> &'static FuncInfo { + &self.0.info + } + + /// Construct an element. + pub fn construct(self, vm: &mut Vm, args: &mut Args) -> SourceResult<Content> { + (self.0.construct)(vm, args) + } + + /// Whether the contained element has the given capability. + pub fn can<C>(&self) -> bool + where + C: ?Sized + 'static, + { + (self.0.vtable)(TypeId::of::<C>()).is_some() + } + + /// Create a selector for elements of this function. + pub fn select(self) -> Selector { + Selector::Elem(self, None) + } + + /// Create a selector for elements of this function, filtering for those + /// whose [fields](super::Content::field) match the given arguments. + pub fn where_(self, fields: Dict) -> Selector { + Selector::Elem(self, Some(fields)) + } + + /// Execute the set rule for the element and return the resulting style map. + pub fn set(self, mut args: Args) -> SourceResult<Styles> { + let styles = (self.0.set)(&mut args)?; + args.finish()?; + Ok(styles) + } +} + +impl Debug for ElemFunc { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(self.name()) + } +} + +impl Eq for ElemFunc {} + +impl PartialEq for ElemFunc { + fn eq(&self, other: &Self) -> bool { + std::ptr::eq(self.0, other.0) + } +} + +impl Hash for ElemFunc { + fn hash<H: Hasher>(&self, state: &mut H) { + state.write_usize(self.0 as *const _ as usize); + } +} + +cast! { + ElemFunc, + self => Value::Func(self.into()), + v: Func => v.element().ok_or("expected element function")?, +} + +impl From<&'static NativeElemFunc> for ElemFunc { + fn from(native: &'static NativeElemFunc) -> Self { + Self(native) + } +} + +/// An element function backed by a Rust type. +pub struct NativeElemFunc { + /// The element's name. + pub name: &'static str, + /// The element's vtable for capability dispatch. + pub vtable: fn(of: TypeId) -> Option<*const ()>, + /// The element's constructor. + pub construct: fn(&mut Vm, &mut Args) -> SourceResult<Content>, + /// The element's set rule. + pub set: fn(&mut Args) -> SourceResult<Styles>, + /// Details about the function. + pub info: Lazy<FuncInfo>, +} diff --git a/crates/typst/src/model/introspect.rs b/crates/typst/src/model/introspect.rs new file mode 100644 index 00000000..42c1a9e1 --- /dev/null +++ b/crates/typst/src/model/introspect.rs @@ -0,0 +1,352 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::fmt::{self, Debug, Formatter}; +use std::hash::Hash; +use std::num::NonZeroUsize; + +use comemo::{Prehashed, Track, Tracked, Validate}; +use ecow::EcoVec; +use indexmap::IndexMap; + +use super::{Content, Selector}; +use crate::diag::{bail, StrResult}; +use crate::doc::{Frame, FrameItem, Meta, Position}; +use crate::eval::{cast, Value}; +use crate::geom::{Point, Transform}; +use crate::model::Label; +use crate::util::NonZeroExt; + +/// Identifies the location of an element in the document. +/// +/// This struct is created by [`Locator::locate`]. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct Location { + /// The hash of the element. + hash: u128, + /// An unique number among elements with the same hash. This is the reason + /// we need a `Locator` everywhere. + disambiguator: usize, + /// A synthetic location created from another one. This is used for example + /// in bibliography management to create individual linkable locations for + /// reference entries from the bibliography's location. + variant: usize, +} + +impl Location { + /// Produce a variant of this location. + pub fn variant(mut self, n: usize) -> Self { + self.variant = n; + self + } +} + +impl Debug for Location { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("..") + } +} + +cast! { + type Location: "location", +} + +/// Provides locations for elements in the document. +/// +/// A [`Location`] consists of an element's hash plus a disambiguator. Just the +/// hash is not enough because we can have multiple equal elements with the same +/// hash (not a hash collision, just equal elements!). Between these, we +/// disambiguate with an increasing number. In principle, the disambiguator +/// could just be counted up. However, counting is an impure operation and as +/// such we can't count across a memoization boundary. [^1] +/// +/// Instead, we only mutate within a single "layout run" and combine the results +/// with disambiguators from an outer tracked locator. Thus, the locators form a +/// "tracked chain". When a layout run ends, its mutations are discarded and, on +/// the other side of the memoization boundary, we +/// [reconstruct](Self::visit_frame) them from the resulting [frames](Frame). +/// +/// [^1]: Well, we could with [`TrackedMut`](comemo::TrackedMut), but the +/// overhead is quite high, especially since we need to save & undo the counting +/// when only measuring. +#[derive(Default)] +pub struct Locator<'a> { + /// Maps from a hash to the maximum number we've seen for this hash. This + /// number becomes the `disambiguator`. + hashes: RefCell<HashMap<u128, usize>>, + /// An outer `Locator`, from which we can get disambiguator for hashes + /// outside of the current "layout run". + /// + /// We need to override the constraint's lifetime here so that `Tracked` is + /// covariant over the constraint. If it becomes invariant, we're in for a + /// world of lifetime pain. + outer: Option<Tracked<'a, Self, <Locator<'static> as Validate>::Constraint>>, +} + +impl<'a> Locator<'a> { + /// Create a new locator. + pub fn new() -> Self { + Self::default() + } + + /// Create a new chained locator. + pub fn chained(outer: Tracked<'a, Self>) -> Self { + Self { outer: Some(outer), ..Default::default() } + } + + /// Start tracking this locator. + /// + /// In comparison to [`Track::track`], this method skips this chain link + /// if it does not contribute anything. + pub fn track(&self) -> Tracked<'_, Self> { + match self.outer { + Some(outer) if self.hashes.borrow().is_empty() => outer, + _ => Track::track(self), + } + } + + /// Produce a stable identifier for this call site. + pub fn locate(&mut self, hash: u128) -> Location { + // Get the current disambiguator for this hash. + let disambiguator = self.disambiguator_impl(hash); + + // Bump the next disambiguator up by one. + self.hashes.borrow_mut().insert(hash, disambiguator + 1); + + // Create the location in its default variant. + Location { hash, disambiguator, variant: 0 } + } + + /// Advance past a frame. + pub fn visit_frame(&mut self, frame: &Frame) { + for (_, item) in frame.items() { + match item { + FrameItem::Group(group) => self.visit_frame(&group.frame), + FrameItem::Meta(Meta::Elem(elem), _) => { + let mut hashes = self.hashes.borrow_mut(); + let loc = elem.location().unwrap(); + let entry = hashes.entry(loc.hash).or_default(); + + // Next disambiguator needs to be at least one larger than + // the maximum we've seen so far. + *entry = (*entry).max(loc.disambiguator + 1); + } + _ => {} + } + } + } + + /// Advance past a number of frames. + pub fn visit_frames<'b>(&mut self, frames: impl IntoIterator<Item = &'b Frame>) { + for frame in frames { + self.visit_frame(frame); + } + } + + /// The current disambiguator for the given hash. + fn disambiguator_impl(&self, hash: u128) -> usize { + *self + .hashes + .borrow_mut() + .entry(hash) + .or_insert_with(|| self.outer.map_or(0, |outer| outer.disambiguator(hash))) + } +} + +#[comemo::track] +impl<'a> Locator<'a> { + /// The current disambiguator for the hash. + fn disambiguator(&self, hash: u128) -> usize { + self.disambiguator_impl(hash) + } +} + +/// Can be queried for elements and their positions. +pub struct Introspector { + /// The number of pages in the document. + pages: usize, + /// All introspectable elements. + elems: IndexMap<Location, (Prehashed<Content>, Position)>, + /// The page numberings, indexed by page number minus 1. + page_numberings: Vec<Value>, + /// Caches queries done on the introspector. This is important because + /// even if all top-level queries are distinct, they often have shared + /// subqueries. Example: Individual counter queries with `before` that + /// all depend on a global counter query. + queries: RefCell<HashMap<u128, EcoVec<Prehashed<Content>>>>, +} + +impl Introspector { + /// Create a new introspector. + #[tracing::instrument(skip(frames))] + pub fn new(frames: &[Frame]) -> Self { + let mut introspector = Self { + pages: frames.len(), + elems: IndexMap::new(), + page_numberings: vec![], + queries: RefCell::default(), + }; + for (i, frame) in frames.iter().enumerate() { + let page = NonZeroUsize::new(1 + i).unwrap(); + introspector.extract(frame, page, Transform::identity()); + } + introspector + } + + /// Extract metadata from a frame. + #[tracing::instrument(skip_all)] + fn extract(&mut self, frame: &Frame, page: NonZeroUsize, ts: Transform) { + for (pos, item) in frame.items() { + match item { + FrameItem::Group(group) => { + let ts = ts + .pre_concat(Transform::translate(pos.x, pos.y)) + .pre_concat(group.transform); + self.extract(&group.frame, page, ts); + } + FrameItem::Meta(Meta::Elem(content), _) + if !self.elems.contains_key(&content.location().unwrap()) => + { + let pos = pos.transform(ts); + let ret = self.elems.insert( + content.location().unwrap(), + (Prehashed::new(content.clone()), Position { page, point: pos }), + ); + assert!(ret.is_none(), "duplicate locations"); + } + FrameItem::Meta(Meta::PageNumbering(numbering), _) => { + self.page_numberings.push(numbering.clone()); + } + _ => {} + } + } + } + + /// Iterate over all locatable elements. + pub fn all(&self) -> impl Iterator<Item = &Prehashed<Content>> + '_ { + self.elems.values().map(|(c, _)| c) + } + + /// Get an element by its location. + fn get(&self, location: &Location) -> Option<&Prehashed<Content>> { + self.elems.get(location).map(|(elem, _)| elem) + } + + /// Get the index of this element among all. + fn index(&self, elem: &Content) -> usize { + self.elems + .get_index_of(&elem.location().unwrap()) + .unwrap_or(usize::MAX) + } +} + +#[comemo::track] +impl Introspector { + /// Query for all matching elements. + pub fn query(&self, selector: &Selector) -> EcoVec<Prehashed<Content>> { + let hash = crate::util::hash128(selector); + if let Some(output) = self.queries.borrow().get(&hash) { + return output.clone(); + } + + let output = match selector { + Selector::Elem(..) + | Selector::Label(_) + | Selector::Regex(_) + | Selector::Can(_) + | Selector::Or(_) + | Selector::And(_) => { + self.all().filter(|elem| selector.matches(elem)).cloned().collect() + } + + Selector::Location(location) => { + self.get(location).cloned().into_iter().collect() + } + Selector::Before { selector, end, inclusive } => { + let mut list = self.query(selector); + if let Some(end) = self.query_first(end) { + // Determine which elements are before `end`. + let split = match list + .binary_search_by_key(&self.index(&end), |elem| self.index(elem)) + { + // Element itself is contained. + Ok(i) => i + *inclusive as usize, + // Element itself is not contained. + Err(i) => i, + }; + list = list[..split].into(); + } + list + } + Selector::After { selector, start, inclusive } => { + let mut list = self.query(selector); + if let Some(start) = self.query_first(start) { + // Determine which elements are after `start`. + let split = match list + .binary_search_by_key(&self.index(&start), |elem| { + self.index(elem) + }) { + // Element itself is contained. + Ok(i) => i + !*inclusive as usize, + // Element itself is not contained. + Err(i) => i, + }; + list = list[split..].into(); + } + list + } + }; + + self.queries.borrow_mut().insert(hash, output.clone()); + output + } + + /// Query for the first element that matches the selector. + pub fn query_first(&self, selector: &Selector) -> Option<Prehashed<Content>> { + match selector { + Selector::Location(location) => self.get(location).cloned(), + _ => self.query(selector).first().cloned(), + } + } + + /// Query for a unique element with the label. + pub fn query_label(&self, label: &Label) -> StrResult<Prehashed<Content>> { + let mut found = None; + for elem in self.all().filter(|elem| elem.label() == Some(label)) { + if found.is_some() { + bail!("label occurs multiple times in the document"); + } + found = Some(elem.clone()); + } + found.ok_or_else(|| "label does not exist in the document".into()) + } + + /// The total number pages. + pub fn pages(&self) -> NonZeroUsize { + NonZeroUsize::new(self.pages).unwrap_or(NonZeroUsize::ONE) + } + + /// Gets the page numbering for the given location, if any. + pub fn page_numbering(&self, location: Location) -> Value { + let page = self.page(location); + self.page_numberings.get(page.get() - 1).cloned().unwrap_or_default() + } + + /// Find the page number for the given location. + pub fn page(&self, location: Location) -> NonZeroUsize { + self.position(location).page + } + + /// Find the position for the given location. + pub fn position(&self, location: Location) -> Position { + self.elems + .get(&location) + .map(|(_, loc)| *loc) + .unwrap_or(Position { page: NonZeroUsize::ONE, point: Point::zero() }) + } +} + +impl Default for Introspector { + fn default() -> Self { + Self::new(&[]) + } +} diff --git a/crates/typst/src/model/label.rs b/crates/typst/src/model/label.rs new file mode 100644 index 00000000..ef8f3edd --- /dev/null +++ b/crates/typst/src/model/label.rs @@ -0,0 +1,16 @@ +use std::fmt::{self, Debug, Formatter}; + +use ecow::EcoString; + +/// A label for an element. +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Label(pub EcoString); + +impl Debug for Label { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "<{}>", self.0) + } +} + +/// Indicates that an element cannot be labelled. +pub trait Unlabellable {} diff --git a/crates/typst/src/model/mod.rs b/crates/typst/src/model/mod.rs new file mode 100644 index 00000000..ee940236 --- /dev/null +++ b/crates/typst/src/model/mod.rs @@ -0,0 +1,148 @@ +//! The document model. + +mod content; +mod element; +mod introspect; +mod label; +mod realize; +mod selector; +mod styles; + +#[doc(inline)] +pub use typst_macros::element; + +pub use self::content::{Content, MetaElem, PlainText}; +pub use self::element::{Construct, ElemFunc, Element, NativeElemFunc, Set}; +pub use self::introspect::{Introspector, Location, Locator}; +pub use self::label::{Label, Unlabellable}; +pub use self::realize::{ + applicable, realize, Behave, Behaviour, Finalize, Guard, Locatable, Show, Synthesize, +}; +pub use self::selector::{LocatableSelector, Selector, ShowableSelector}; +pub use self::styles::{ + Fold, Property, Recipe, Resolve, Style, StyleChain, StyleVec, StyleVecBuilder, + Styles, Transform, +}; + +use std::mem::ManuallyDrop; + +use comemo::{Track, Tracked, TrackedMut, Validate}; + +use crate::diag::{SourceError, SourceResult}; +use crate::doc::Document; +use crate::eval::Tracer; +use crate::World; + +/// Typeset content into a fully layouted document. +#[comemo::memoize] +#[tracing::instrument(skip(world, tracer, content))] +pub fn typeset( + world: Tracked<dyn World + '_>, + mut tracer: TrackedMut<Tracer>, + content: &Content, +) -> SourceResult<Document> { + tracing::info!("Starting typesetting"); + + let library = world.library(); + let styles = StyleChain::new(&library.styles); + + let mut iter = 0; + let mut document; + let mut delayed; + + // We need `ManuallyDrop` until this lands in stable: + // https://github.com/rust-lang/rust/issues/70919 + let mut introspector = ManuallyDrop::new(Introspector::new(&[])); + + // Relayout until all introspections stabilize. + // If that doesn't happen within five attempts, we give up. + loop { + tracing::info!("Layout iteration {iter}"); + + delayed = DelayedErrors::default(); + + let constraint = <Introspector as Validate>::Constraint::new(); + let mut locator = Locator::new(); + let mut vt = Vt { + world, + tracer: TrackedMut::reborrow_mut(&mut tracer), + locator: &mut locator, + introspector: introspector.track_with(&constraint), + delayed: delayed.track_mut(), + }; + + // Layout! + let result = (library.items.layout)(&mut vt, content, styles)?; + + // Drop the old introspector. + ManuallyDrop::into_inner(introspector); + + // Only now assign the document and construct the new introspector. + document = result; + introspector = ManuallyDrop::new(Introspector::new(&document.pages)); + iter += 1; + + if iter >= 5 || introspector.validate(&constraint) { + break; + } + } + + // Drop the introspector. + ManuallyDrop::into_inner(introspector); + + // Promote delayed errors. + if !delayed.0.is_empty() { + return Err(Box::new(delayed.0)); + } + + Ok(document) +} + +/// A virtual typesetter. +/// +/// Holds the state needed to [typeset] content. +pub struct Vt<'a> { + /// The compilation environment. + pub world: Tracked<'a, dyn World + 'a>, + /// Provides access to information about the document. + pub introspector: Tracked<'a, Introspector>, + /// Provides stable identities to elements. + pub locator: &'a mut Locator<'a>, + /// Delayed errors that do not immediately terminate execution. + pub delayed: TrackedMut<'a, DelayedErrors>, + /// The tracer for inspection of the values an expression produces. + pub tracer: TrackedMut<'a, Tracer>, +} + +impl Vt<'_> { + /// Perform a fallible operation that does not immediately terminate further + /// execution. Instead it produces a delayed error that is only promoted to + /// a fatal one if it remains at the end of the introspection loop. + pub fn delayed<F, T>(&mut self, f: F) -> T + where + F: FnOnce(&mut Self) -> SourceResult<T>, + T: Default, + { + match f(self) { + Ok(value) => value, + Err(errors) => { + for error in *errors { + self.delayed.push(error); + } + T::default() + } + } + } +} + +/// Holds delayed errors. +#[derive(Default, Clone)] +pub struct DelayedErrors(Vec<SourceError>); + +#[comemo::track] +impl DelayedErrors { + /// Push a delayed error. + fn push(&mut self, error: SourceError) { + self.0.push(error); + } +} diff --git a/crates/typst/src/model/realize.rs b/crates/typst/src/model/realize.rs new file mode 100644 index 00000000..01c46b81 --- /dev/null +++ b/crates/typst/src/model/realize.rs @@ -0,0 +1,228 @@ +use super::{Content, ElemFunc, Element, MetaElem, Recipe, Selector, StyleChain, Vt}; +use crate::diag::SourceResult; +use crate::doc::Meta; +use crate::util::hash128; + +/// Whether the target is affected by show rules in the given style chain. +pub fn applicable(target: &Content, styles: StyleChain) -> bool { + if target.needs_preparation() { + return true; + } + + if target.can::<dyn Show>() && target.is_pristine() { + return true; + } + + // Find out how many recipes there are. + let mut n = styles.recipes().count(); + + // Find out whether any recipe matches and is unguarded. + for recipe in styles.recipes() { + if recipe.applicable(target) && !target.is_guarded(Guard::Nth(n)) { + return true; + } + n -= 1; + } + + false +} + +/// Apply the show rules in the given style chain to a target. +pub fn realize( + vt: &mut Vt, + target: &Content, + styles: StyleChain, +) -> SourceResult<Option<Content>> { + // Pre-process. + if target.needs_preparation() { + let mut elem = target.clone(); + if target.can::<dyn Locatable>() || target.label().is_some() { + let location = vt.locator.locate(hash128(target)); + elem.set_location(location); + } + + if let Some(elem) = elem.with_mut::<dyn Synthesize>() { + elem.synthesize(vt, styles)?; + } + + elem.mark_prepared(); + + if elem.location().is_some() { + let span = elem.span(); + let meta = Meta::Elem(elem.clone()); + return Ok(Some( + (elem + MetaElem::new().pack().spanned(span)) + .styled(MetaElem::set_data(vec![meta])), + )); + } + + return Ok(Some(elem)); + } + + // Find out how many recipes there are. + let mut n = styles.recipes().count(); + + // Find an applicable recipe. + let mut realized = None; + for recipe in styles.recipes() { + let guard = Guard::Nth(n); + if recipe.applicable(target) && !target.is_guarded(guard) { + if let Some(content) = try_apply(vt, target, recipe, guard)? { + realized = Some(content); + break; + } + } + n -= 1; + } + + // Realize if there was no matching recipe. + if let Some(showable) = target.with::<dyn Show>() { + let guard = Guard::Base(target.func()); + if realized.is_none() && !target.is_guarded(guard) { + realized = Some(showable.show(vt, styles)?); + } + } + + // Finalize only if this is the first application for this element. + if let Some(elem) = target.with::<dyn Finalize>() { + if target.is_pristine() { + if let Some(already) = realized { + realized = Some(elem.finalize(already, styles)); + } + } + } + + Ok(realized) +} + +/// Try to apply a recipe to the target. +fn try_apply( + vt: &mut Vt, + target: &Content, + recipe: &Recipe, + guard: Guard, +) -> SourceResult<Option<Content>> { + match &recipe.selector { + Some(Selector::Elem(element, _)) => { + if target.func() != *element { + return Ok(None); + } + + recipe.apply_vt(vt, target.clone().guarded(guard)).map(Some) + } + + Some(Selector::Label(label)) => { + if target.label() != Some(label) { + return Ok(None); + } + + recipe.apply_vt(vt, target.clone().guarded(guard)).map(Some) + } + + Some(Selector::Regex(regex)) => { + let Some(text) = item!(text_str)(target) else { + return Ok(None); + }; + + let make = |s: &str| target.clone().with_field("text", s); + let mut result = vec![]; + let mut cursor = 0; + + for m in regex.find_iter(&text) { + let start = m.start(); + if cursor < start { + result.push(make(&text[cursor..start])); + } + + let piece = make(m.as_str()).guarded(guard); + let transformed = recipe.apply_vt(vt, piece)?; + result.push(transformed); + cursor = m.end(); + } + + if result.is_empty() { + return Ok(None); + } + + if cursor < text.len() { + result.push(make(&text[cursor..])); + } + + Ok(Some(Content::sequence(result))) + } + + // Not supported here. + Some( + Selector::Or(_) + | Selector::And(_) + | Selector::Location(_) + | Selector::Can(_) + | Selector::Before { .. } + | Selector::After { .. }, + ) => Ok(None), + + None => Ok(None), + } +} + +/// Makes this element locatable through `vt.locate`. +pub trait Locatable {} + +/// Synthesize fields on an element. This happens before execution of any show +/// rule. +pub trait Synthesize { + /// Prepare the element for show rule application. + fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()>; +} + +/// The base recipe for an element. +pub trait Show { + /// Execute the base recipe for this element. + fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content>; +} + +/// Post-process an element after it was realized. +pub trait Finalize { + /// Finalize the fully realized form of the element. Use this for effects + /// that should work even in the face of a user-defined show rule. + fn finalize(&self, realized: Content, styles: StyleChain) -> Content; +} + +/// How the element interacts with other elements. +pub trait Behave { + /// The element's interaction behaviour. + fn behaviour(&self) -> Behaviour; + + /// Whether this weak element is larger than a previous one and thus picked + /// as the maximum when the levels are the same. + #[allow(unused_variables)] + fn larger(&self, prev: &Content) -> bool { + false + } +} + +/// How an element interacts with other elements in a stream. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Behaviour { + /// A weak element which only survives when a supportive element is before + /// and after it. Furthermore, per consecutive run of weak elements, only + /// one survives: The one with the lowest weakness level (or the larger one + /// if there is a tie). + Weak(usize), + /// An element that enables adjacent weak elements to exist. The default. + Supportive, + /// An element that destroys adjacent weak elements. + Destructive, + /// An element that does not interact at all with other elements, having the + /// same effect as if it didn't exist. + Ignorant, +} + +/// Guards content against being affected by the same show rule multiple times. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Guard { + /// The nth recipe from the top of the chain. + Nth(usize), + /// The [base recipe](Show) for a kind of element. + Base(ElemFunc), +} diff --git a/crates/typst/src/model/selector.rs b/crates/typst/src/model/selector.rs new file mode 100644 index 00000000..9723ee4f --- /dev/null +++ b/crates/typst/src/model/selector.rs @@ -0,0 +1,296 @@ +use std::any::{Any, TypeId}; +use std::fmt::{self, Debug, Formatter, Write}; +use std::sync::Arc; + +use ecow::{eco_format, EcoString, EcoVec}; + +use super::{Content, ElemFunc, Label, Location}; +use crate::diag::{bail, StrResult}; +use crate::eval::{ + cast, CastInfo, Dict, FromValue, Func, IntoValue, Reflect, Regex, Value, +}; +use crate::model::Locatable; +use crate::util::pretty_array_like; + +/// A selector in a show rule. +#[derive(Clone, PartialEq, Hash)] +pub enum Selector { + /// Matches a specific type of element. + /// + /// If there is a dictionary, only elements with the fields from the + /// dictionary match. + Elem(ElemFunc, Option<Dict>), + /// Matches the element at the specified location. + Location(Location), + /// Matches elements with a specific label. + Label(Label), + /// Matches text elements through a regular expression. + Regex(Regex), + /// Matches elements with a specific capability. + Can(TypeId), + /// Matches if any of the subselectors match. + Or(EcoVec<Self>), + /// Matches if all of the subselectors match. + And(EcoVec<Self>), + /// Matches all matches of `selector` before `end`. + Before { selector: Arc<Self>, end: Arc<Self>, inclusive: bool }, + /// Matches all matches of `selector` after `start`. + After { selector: Arc<Self>, start: Arc<Self>, inclusive: bool }, +} + +impl Selector { + /// Define a simple text selector. + pub fn text(text: &str) -> Self { + Self::Regex(Regex::new(®ex::escape(text)).unwrap()) + } + + /// Define a simple [`Selector::Can`] selector. + pub fn can<T: ?Sized + Any>() -> Self { + Self::Can(TypeId::of::<T>()) + } + + /// Transforms this selector and an iterator of other selectors into a + /// [`Selector::Or`] selector. + pub fn and(self, others: impl IntoIterator<Item = Self>) -> Self { + Self::And(others.into_iter().chain(Some(self)).collect()) + } + + /// Transforms this selector and an iterator of other selectors into a + /// [`Selector::And`] selector. + pub fn or(self, others: impl IntoIterator<Item = Self>) -> Self { + Self::Or(others.into_iter().chain(Some(self)).collect()) + } + + /// Transforms this selector into a [`Selector::Before`] selector. + pub fn before(self, location: impl Into<Self>, inclusive: bool) -> Self { + Self::Before { + selector: Arc::new(self), + end: Arc::new(location.into()), + inclusive, + } + } + + /// Transforms this selector into a [`Selector::After`] selector. + pub fn after(self, location: impl Into<Self>, inclusive: bool) -> Self { + Self::After { + selector: Arc::new(self), + start: Arc::new(location.into()), + inclusive, + } + } + + /// Whether the selector matches for the target. + pub fn matches(&self, target: &Content) -> bool { + match self { + Self::Elem(element, dict) => { + target.func() == *element + && dict + .iter() + .flat_map(|dict| dict.iter()) + .all(|(name, value)| target.field_ref(name) == Some(value)) + } + Self::Label(label) => target.label() == Some(label), + Self::Regex(regex) => { + target.func() == item!(text_func) + && item!(text_str)(target).map_or(false, |text| regex.is_match(&text)) + } + Self::Can(cap) => target.can_type_id(*cap), + Self::Or(selectors) => selectors.iter().any(move |sel| sel.matches(target)), + Self::And(selectors) => selectors.iter().all(move |sel| sel.matches(target)), + Self::Location(location) => target.location() == Some(*location), + // Not supported here. + Self::Before { .. } | Self::After { .. } => false, + } + } +} + +impl From<Location> for Selector { + fn from(value: Location) -> Self { + Self::Location(value) + } +} + +impl Debug for Selector { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Elem(elem, dict) => { + f.write_str(elem.name())?; + if let Some(dict) = dict { + f.write_str(".where")?; + dict.fmt(f)?; + } + Ok(()) + } + Self::Label(label) => label.fmt(f), + Self::Regex(regex) => regex.fmt(f), + Self::Can(cap) => cap.fmt(f), + Self::Or(selectors) | Self::And(selectors) => { + f.write_str(if matches!(self, Self::Or(_)) { "or" } else { "and" })?; + let pieces: Vec<_> = + selectors.iter().map(|sel| eco_format!("{sel:?}")).collect(); + f.write_str(&pretty_array_like(&pieces, false)) + } + Self::Location(loc) => loc.fmt(f), + Self::Before { selector, end: split, inclusive } + | Self::After { selector, start: split, inclusive } => { + selector.fmt(f)?; + + if matches!(self, Self::Before { .. }) { + f.write_str(".before(")?; + } else { + f.write_str(".after(")?; + } + + split.fmt(f)?; + if !*inclusive { + f.write_str(", inclusive: false")?; + } + f.write_char(')') + } + } + } +} + +cast! { + type Selector: "selector", + func: Func => func + .element() + .ok_or("only element functions can be used as selectors")? + .select(), + label: Label => Self::Label(label), + text: EcoString => Self::text(&text), + regex: Regex => Self::Regex(regex), + location: Location => Self::Location(location), +} + +/// A selector that can be used with `query`. +/// +/// Hopefully, this is made obsolete by a more powerful query mechanism in the +/// future. +#[derive(Clone, PartialEq, Hash)] +pub struct LocatableSelector(pub Selector); + +impl Reflect for LocatableSelector { + fn describe() -> CastInfo { + CastInfo::Union(vec![ + CastInfo::Type("function"), + CastInfo::Type("label"), + CastInfo::Type("selector"), + ]) + } + + fn castable(value: &Value) -> bool { + matches!(value.type_name(), "function" | "label" | "selector") + } +} + +impl IntoValue for LocatableSelector { + fn into_value(self) -> Value { + self.0.into_value() + } +} + +impl FromValue for LocatableSelector { + fn from_value(value: Value) -> StrResult<Self> { + fn validate(selector: &Selector) -> StrResult<()> { + match selector { + Selector::Elem(elem, _) => { + if !elem.can::<dyn Locatable>() { + Err(eco_format!("{} is not locatable", elem.name()))? + } + } + Selector::Location(_) => {} + Selector::Label(_) => {} + Selector::Regex(_) => bail!("text is not locatable"), + Selector::Can(_) => bail!("capability is not locatable"), + Selector::Or(list) | Selector::And(list) => { + for selector in list { + validate(selector)?; + } + } + Selector::Before { selector, end: split, .. } + | Selector::After { selector, start: split, .. } => { + for selector in [selector, split] { + validate(selector)?; + } + } + } + Ok(()) + } + + if !Self::castable(&value) { + return Err(Self::error(&value)); + } + + let selector = Selector::from_value(value)?; + validate(&selector)?; + Ok(Self(selector)) + } +} + +/// A selector that can be used with show rules. +/// +/// Hopefully, this is made obsolete by a more powerful showing mechanism in the +/// future. +#[derive(Clone, PartialEq, Hash)] +pub struct ShowableSelector(pub Selector); + +impl Reflect for ShowableSelector { + fn describe() -> CastInfo { + CastInfo::Union(vec![ + CastInfo::Type("function"), + CastInfo::Type("label"), + CastInfo::Type("string"), + CastInfo::Type("regular expression"), + CastInfo::Type("symbol"), + CastInfo::Type("selector"), + ]) + } + + fn castable(value: &Value) -> bool { + matches!( + value.type_name(), + "symbol" + | "string" + | "label" + | "function" + | "regular expression" + | "selector" + ) + } +} + +impl IntoValue for ShowableSelector { + fn into_value(self) -> Value { + self.0.into_value() + } +} + +impl FromValue for ShowableSelector { + fn from_value(value: Value) -> StrResult<Self> { + fn validate(selector: &Selector) -> StrResult<()> { + match selector { + Selector::Elem(_, _) => {} + Selector::Label(_) => {} + Selector::Regex(_) => {} + Selector::Or(_) + | Selector::And(_) + | Selector::Location(_) + | Selector::Can(_) + | Selector::Before { .. } + | Selector::After { .. } => { + bail!("this selector cannot be used with show") + } + } + Ok(()) + } + + if !Self::castable(&value) { + return Err(Self::error(&value)); + } + + let selector = Selector::from_value(value)?; + validate(&selector)?; + Ok(Self(selector)) + } +} diff --git a/crates/typst/src/model/styles.rs b/crates/typst/src/model/styles.rs new file mode 100644 index 00000000..23748a3f --- /dev/null +++ b/crates/typst/src/model/styles.rs @@ -0,0 +1,750 @@ +use std::fmt::{self, Debug, Formatter, Write}; +use std::iter; +use std::mem; +use std::ptr; + +use comemo::Prehashed; +use ecow::{eco_vec, EcoString, EcoVec}; + +use super::{Content, ElemFunc, Element, Selector, Vt}; +use crate::diag::{SourceResult, Trace, Tracepoint}; +use crate::eval::{cast, Args, FromValue, Func, IntoValue, Value, Vm}; +use crate::syntax::Span; + +/// A list of style properties. +#[derive(Default, PartialEq, Clone, Hash)] +pub struct Styles(EcoVec<Prehashed<Style>>); + +impl Styles { + /// Create a new, empty style list. + pub fn new() -> Self { + Self::default() + } + + /// Whether this contains no styles. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Set an inner value for a style property. + /// + /// If the property needs folding and the value is already contained in the + /// style map, `self` contributes the outer values and `value` is the inner + /// one. + pub fn set(&mut self, style: impl Into<Style>) { + self.0.push(Prehashed::new(style.into())); + } + + /// Remove the style that was last set. + pub fn unset(&mut self) { + self.0.pop(); + } + + /// Apply outer styles. Like [`chain`](StyleChain::chain), but in-place. + pub fn apply(&mut self, mut outer: Self) { + outer.0.extend(mem::take(self).0.into_iter()); + *self = outer; + } + + /// Apply one outer styles. + pub fn apply_one(&mut self, outer: Style) { + self.0.insert(0, Prehashed::new(outer)); + } + + /// Apply a slice of outer styles. + pub fn apply_slice(&mut self, outer: &[Prehashed<Style>]) { + self.0 = outer.iter().cloned().chain(mem::take(self).0.into_iter()).collect(); + } + + /// Add an origin span to all contained properties. + pub fn spanned(mut self, span: Span) -> Self { + for entry in self.0.make_mut() { + entry.update(|entry| { + if let Style::Property(property) = entry { + property.span = Some(span); + } + }); + } + self + } + + /// Returns `Some(_)` with an optional span if this list contains + /// styles for the given element. + pub fn interruption<T: Element>(&self) -> Option<Option<Span>> { + let func = T::func(); + self.0.iter().find_map(|entry| match &**entry { + Style::Property(property) => property.is_of(func).then_some(property.span), + Style::Recipe(recipe) => recipe.is_of(func).then_some(Some(recipe.span)), + }) + } +} + +impl From<Style> for Styles { + fn from(entry: Style) -> Self { + Self(eco_vec![Prehashed::new(entry)]) + } +} + +impl Debug for Styles { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("..") + } +} + +/// A single style property or recipe. +#[derive(Clone, PartialEq, Hash)] +pub enum Style { + /// A style property originating from a set rule or constructor. + Property(Property), + /// A show rule recipe. + Recipe(Recipe), +} + +impl Style { + /// If this is a property, return it. + pub fn property(&self) -> Option<&Property> { + match self { + Self::Property(property) => Some(property), + _ => None, + } + } + + /// If this is a recipe, return it. + pub fn recipe(&self) -> Option<&Recipe> { + match self { + Self::Recipe(recipe) => Some(recipe), + _ => None, + } + } +} + +impl Debug for Style { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Property(property) => property.fmt(f), + Self::Recipe(recipe) => recipe.fmt(f), + } + } +} + +impl From<Property> for Style { + fn from(property: Property) -> Self { + Self::Property(property) + } +} + +impl From<Recipe> for Style { + fn from(recipe: Recipe) -> Self { + Self::Recipe(recipe) + } +} + +/// A style property originating from a set rule or constructor. +#[derive(Clone, PartialEq, Hash)] +pub struct Property { + /// The element the property belongs to. + element: ElemFunc, + /// The property's name. + name: EcoString, + /// The property's value. + value: Value, + /// The span of the set rule the property stems from. + span: Option<Span>, +} + +impl Property { + /// Create a new property from a key-value pair. + pub fn new( + element: ElemFunc, + name: impl Into<EcoString>, + value: impl IntoValue, + ) -> Self { + Self { + element, + name: name.into(), + value: value.into_value(), + span: None, + } + } + + /// Whether this property is the given one. + pub fn is(&self, element: ElemFunc, name: &str) -> bool { + self.element == element && self.name == name + } + + /// Whether this property belongs to the given element. + pub fn is_of(&self, element: ElemFunc) -> bool { + self.element == element + } +} + +impl Debug for Property { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "set {}({}: {:?})", self.element.name(), self.name, self.value)?; + Ok(()) + } +} + +/// A show rule recipe. +#[derive(Clone, PartialEq, Hash)] +pub struct Recipe { + /// The span errors are reported with. + pub span: Span, + /// Determines whether the recipe applies to an element. + pub selector: Option<Selector>, + /// The transformation to perform on the match. + pub transform: Transform, +} + +impl Recipe { + /// Whether this recipe is for the given type of element. + pub fn is_of(&self, element: ElemFunc) -> bool { + match self.selector { + Some(Selector::Elem(own, _)) => own == element, + _ => false, + } + } + + /// Whether the recipe is applicable to the target. + pub fn applicable(&self, target: &Content) -> bool { + self.selector + .as_ref() + .map_or(false, |selector| selector.matches(target)) + } + + /// Apply the recipe to the given content. + pub fn apply_vm(&self, vm: &mut Vm, content: Content) -> SourceResult<Content> { + match &self.transform { + Transform::Content(content) => Ok(content.clone()), + Transform::Func(func) => { + let args = Args::new(self.span, [Value::Content(content.clone())]); + let mut result = func.call_vm(vm, args); + // For selector-less show rules, a tracepoint makes no sense. + if self.selector.is_some() { + let point = || Tracepoint::Show(content.func().name().into()); + result = result.trace(vm.world(), point, content.span()); + } + Ok(result?.display()) + } + Transform::Style(styles) => Ok(content.styled_with_map(styles.clone())), + } + } + + /// Apply the recipe to the given content. + pub fn apply_vt(&self, vt: &mut Vt, content: Content) -> SourceResult<Content> { + match &self.transform { + Transform::Content(content) => Ok(content.clone()), + Transform::Func(func) => { + let mut result = func.call_vt(vt, [Value::Content(content.clone())]); + if self.selector.is_some() { + let point = || Tracepoint::Show(content.func().name().into()); + result = result.trace(vt.world, point, content.span()); + } + Ok(result?.display()) + } + Transform::Style(styles) => Ok(content.styled_with_map(styles.clone())), + } + } +} + +impl Debug for Recipe { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("show")?; + if let Some(selector) = &self.selector { + f.write_char(' ')?; + selector.fmt(f)?; + } + f.write_str(": ")?; + self.transform.fmt(f) + } +} + +/// A show rule transformation that can be applied to a match. +#[derive(Clone, PartialEq, Hash)] +pub enum Transform { + /// Replacement content. + Content(Content), + /// A function to apply to the match. + Func(Func), + /// Apply styles to the content. + Style(Styles), +} + +impl Debug for Transform { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Content(content) => content.fmt(f), + Self::Func(func) => func.fmt(f), + Self::Style(styles) => styles.fmt(f), + } + } +} + +cast! { + Transform, + content: Content => Self::Content(content), + func: Func => Self::Func(func), +} + +/// A chain of styles, similar to a linked list. +/// +/// A style chain allows to combine properties from multiple style lists in a +/// element hierarchy in a non-allocating way. Rather than eagerly merging the +/// lists, each access walks the hierarchy from the innermost to the outermost +/// map, trying to find a match and then folding it with matches further up the +/// chain. +#[derive(Default, Clone, Copy, Hash)] +pub struct StyleChain<'a> { + /// The first link of this chain. + head: &'a [Prehashed<Style>], + /// The remaining links in the chain. + tail: Option<&'a Self>, +} + +impl<'a> StyleChain<'a> { + /// Start a new style chain with root styles. + pub fn new(root: &'a Styles) -> Self { + Self { head: &root.0, tail: None } + } + + /// Make the given style list the first link of this chain. + /// + /// The resulting style chain contains styles from `local` as well as + /// `self`. The ones from `local` take precedence over the ones from + /// `self`. For folded properties `local` contributes the inner value. + pub fn chain<'b>(&'b self, local: &'b Styles) -> StyleChain<'b> { + if local.is_empty() { + *self + } else { + StyleChain { head: &local.0, tail: Some(self) } + } + } + + /// Cast the first value for the given property in the chain. + pub fn get<T: FromValue>( + self, + func: ElemFunc, + name: &'a str, + inherent: Option<Value>, + default: impl Fn() -> T, + ) -> T { + self.properties::<T>(func, name, inherent) + .next() + .unwrap_or_else(default) + } + + /// Cast the first value for the given property in the chain. + pub fn get_resolve<T: FromValue + Resolve>( + self, + func: ElemFunc, + name: &'a str, + inherent: Option<Value>, + default: impl Fn() -> T, + ) -> T::Output { + self.get(func, name, inherent, default).resolve(self) + } + + /// Cast the first value for the given property in the chain. + pub fn get_fold<T: FromValue + Fold>( + self, + func: ElemFunc, + name: &'a str, + inherent: Option<Value>, + default: impl Fn() -> T::Output, + ) -> T::Output { + fn next<T: Fold>( + mut values: impl Iterator<Item = T>, + _styles: StyleChain, + default: &impl Fn() -> T::Output, + ) -> T::Output { + values + .next() + .map(|value| value.fold(next(values, _styles, default))) + .unwrap_or_else(default) + } + next(self.properties::<T>(func, name, inherent), self, &default) + } + + /// Cast the first value for the given property in the chain. + pub fn get_resolve_fold<T>( + self, + func: ElemFunc, + name: &'a str, + inherent: Option<Value>, + default: impl Fn() -> <T::Output as Fold>::Output, + ) -> <T::Output as Fold>::Output + where + T: FromValue + Resolve, + T::Output: Fold, + { + fn next<T>( + mut values: impl Iterator<Item = T>, + styles: StyleChain, + default: &impl Fn() -> <T::Output as Fold>::Output, + ) -> <T::Output as Fold>::Output + where + T: Resolve, + T::Output: Fold, + { + values + .next() + .map(|value| value.resolve(styles).fold(next(values, styles, default))) + .unwrap_or_else(default) + } + next(self.properties::<T>(func, name, inherent), self, &default) + } + + /// Iterate over all style recipes in the chain. + pub fn recipes(self) -> impl Iterator<Item = &'a Recipe> { + self.entries().filter_map(Style::recipe) + } + + /// Iterate over all values for the given property in the chain. + pub fn properties<T: FromValue + 'a>( + self, + func: ElemFunc, + name: &'a str, + inherent: Option<Value>, + ) -> impl Iterator<Item = T> + '_ { + inherent + .into_iter() + .chain( + self.entries() + .filter_map(Style::property) + .filter(move |property| property.is(func, name)) + .map(|property| property.value.clone()), + ) + .map(move |value| { + value.cast().unwrap_or_else(|err| { + panic!("{} (for {}.{})", err, func.name(), name) + }) + }) + } + + /// Convert to a style map. + pub fn to_map(self) -> Styles { + let mut suffix = Styles::new(); + for link in self.links() { + suffix.apply_slice(link); + } + suffix + } + + /// Iterate over the entries of the chain. + fn entries(self) -> Entries<'a> { + Entries { inner: [].as_slice().iter(), links: self.links() } + } + + /// Iterate over the links of the chain. + fn links(self) -> Links<'a> { + Links(Some(self)) + } + + /// Build owned styles from the suffix (all links beyond the `len`) of the + /// chain. + fn suffix(self, len: usize) -> Styles { + let mut suffix = Styles::new(); + let take = self.links().count().saturating_sub(len); + for link in self.links().take(take) { + suffix.apply_slice(link); + } + suffix + } + + /// Remove the last link from the chain. + fn pop(&mut self) { + *self = self.tail.copied().unwrap_or_default(); + } +} + +impl Debug for StyleChain<'_> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + for entry in self.entries().collect::<Vec<_>>().into_iter().rev() { + writeln!(f, "{:?}", entry)?; + } + Ok(()) + } +} + +impl PartialEq for StyleChain<'_> { + fn eq(&self, other: &Self) -> bool { + ptr::eq(self.head, other.head) + && match (self.tail, other.tail) { + (Some(a), Some(b)) => ptr::eq(a, b), + (None, None) => true, + _ => false, + } + } +} + +/// An iterator over the entries in a style chain. +struct Entries<'a> { + inner: std::slice::Iter<'a, Prehashed<Style>>, + links: Links<'a>, +} + +impl<'a> Iterator for Entries<'a> { + type Item = &'a Style; + + fn next(&mut self) -> Option<Self::Item> { + loop { + if let Some(entry) = self.inner.next_back() { + return Some(entry); + } + + match self.links.next() { + Some(next) => self.inner = next.iter(), + None => return None, + } + } + } +} + +/// An iterator over the links of a style chain. +struct Links<'a>(Option<StyleChain<'a>>); + +impl<'a> Iterator for Links<'a> { + type Item = &'a [Prehashed<Style>]; + + fn next(&mut self) -> Option<Self::Item> { + let StyleChain { head, tail } = self.0?; + self.0 = tail.copied(); + Some(head) + } +} + +/// A sequence of items with associated styles. +#[derive(Clone, Hash)] +pub struct StyleVec<T> { + items: Vec<T>, + styles: Vec<(Styles, usize)>, +} + +impl<T> StyleVec<T> { + /// Whether there are any items in the sequence. + pub fn is_empty(&self) -> bool { + self.items.is_empty() + } + + /// Number of items in the sequence. + pub fn len(&self) -> usize { + self.items.len() + } + + /// Insert an item in the front. The item will share the style of the + /// current first item. + /// + /// This method has no effect if the vector is empty. + pub fn push_front(&mut self, item: T) { + if !self.styles.is_empty() { + self.items.insert(0, item); + self.styles[0].1 += 1; + } + } + + /// Map the contained items. + pub fn map<F, U>(&self, f: F) -> StyleVec<U> + where + F: FnMut(&T) -> U, + { + StyleVec { + items: self.items.iter().map(f).collect(), + styles: self.styles.clone(), + } + } + + /// Iterate over references to the contained items and associated styles. + pub fn iter(&self) -> impl Iterator<Item = (&T, &Styles)> + '_ { + self.items().zip( + self.styles + .iter() + .flat_map(|(map, count)| iter::repeat(map).take(*count)), + ) + } + + /// Iterate over the contained items. + pub fn items(&self) -> std::slice::Iter<'_, T> { + self.items.iter() + } + + /// Iterate over the contained style lists. Note that zipping this with + /// `items()` does not yield the same result as calling `iter()` because + /// this method only returns lists once that are shared by consecutive + /// items. This method is designed for use cases where you want to check, + /// for example, whether any of the lists fulfills a specific property. + pub fn styles(&self) -> impl Iterator<Item = &Styles> { + self.styles.iter().map(|(map, _)| map) + } +} + +impl StyleVec<Content> { + pub fn to_vec(self) -> Vec<Content> { + self.items + .into_iter() + .zip( + self.styles + .iter() + .flat_map(|(map, count)| iter::repeat(map).take(*count)), + ) + .map(|(content, styles)| content.styled_with_map(styles.clone())) + .collect() + } +} + +impl<T> Default for StyleVec<T> { + fn default() -> Self { + Self { items: vec![], styles: vec![] } + } +} + +impl<T> FromIterator<T> for StyleVec<T> { + fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self { + let items: Vec<_> = iter.into_iter().collect(); + let styles = vec![(Styles::new(), items.len())]; + Self { items, styles } + } +} + +impl<T: Debug> Debug for StyleVec<T> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_list() + .entries(self.iter().map(|(item, styles)| { + crate::util::debug(|f| { + styles.fmt(f)?; + item.fmt(f) + }) + })) + .finish() + } +} + +/// Assists in the construction of a [`StyleVec`]. +#[derive(Debug)] +pub struct StyleVecBuilder<'a, T> { + items: Vec<T>, + chains: Vec<(StyleChain<'a>, usize)>, +} + +impl<'a, T> StyleVecBuilder<'a, T> { + /// Create a new style-vec builder. + pub fn new() -> Self { + Self { items: vec![], chains: vec![] } + } + + /// Whether the builder is empty. + pub fn is_empty(&self) -> bool { + self.items.is_empty() + } + + /// Push a new item into the style vector. + pub fn push(&mut self, item: T, styles: StyleChain<'a>) { + self.items.push(item); + + if let Some((prev, count)) = self.chains.last_mut() { + if *prev == styles { + *count += 1; + return; + } + } + + self.chains.push((styles, 1)); + } + + /// Iterate over the contained items. + pub fn elems(&self) -> std::slice::Iter<'_, T> { + self.items.iter() + } + + /// Finish building, returning a pair of two things: + /// - a style vector of items with the non-shared styles + /// - a shared prefix chain of styles that apply to all items + pub fn finish(self) -> (StyleVec<T>, StyleChain<'a>) { + let mut iter = self.chains.iter(); + let mut trunk = match iter.next() { + Some(&(chain, _)) => chain, + None => return Default::default(), + }; + + let mut shared = trunk.links().count(); + for &(mut chain, _) in iter { + let len = chain.links().count(); + if len < shared { + for _ in 0..shared - len { + trunk.pop(); + } + shared = len; + } else if len > shared { + for _ in 0..len - shared { + chain.pop(); + } + } + + while shared > 0 && chain != trunk { + trunk.pop(); + chain.pop(); + shared -= 1; + } + } + + let styles = self + .chains + .into_iter() + .map(|(chain, count)| (chain.suffix(shared), count)) + .collect(); + + (StyleVec { items: self.items, styles }, trunk) + } +} + +impl<'a, T> Default for StyleVecBuilder<'a, T> { + fn default() -> Self { + Self::new() + } +} + +/// A property that is resolved with other properties from the style chain. +pub trait Resolve { + /// The type of the resolved output. + type Output; + + /// Resolve the value using the style chain. + fn resolve(self, styles: StyleChain) -> Self::Output; +} + +impl<T: Resolve> Resolve for Option<T> { + type Output = Option<T::Output>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.map(|v| v.resolve(styles)) + } +} + +/// A property that is folded to determine its final value. +/// +/// In the example below, the chain of stroke values is folded into a single +/// value: `4pt + red`. +/// +/// ```example +/// #set rect(stroke: red) +/// #set rect(stroke: 4pt) +/// #rect() +/// ``` +pub trait Fold { + /// The type of the folded output. + type Output; + + /// Fold this inner value with an outer folded value. + fn fold(self, outer: Self::Output) -> Self::Output; +} + +impl<T> Fold for Option<T> +where + T: Fold, + T::Output: Default, +{ + type Output = Option<T::Output>; + + fn fold(self, outer: Self::Output) -> Self::Output { + self.map(|inner| inner.fold(outer.unwrap_or_default())) + } +} diff --git a/crates/typst/src/syntax/ast.rs b/crates/typst/src/syntax/ast.rs new file mode 100644 index 00000000..7d5e2989 --- /dev/null +++ b/crates/typst/src/syntax/ast.rs @@ -0,0 +1,1994 @@ +//! A typed layer over the untyped syntax tree. +//! +//! The AST is rooted in the [`Markup`] node. + +use std::num::NonZeroUsize; +use std::ops::Deref; + +use ecow::EcoString; +use unscanny::Scanner; + +use super::{ + is_id_continue, is_id_start, is_newline, split_newlines, Span, SyntaxKind, SyntaxNode, +}; +use crate::geom::{AbsUnit, AngleUnit}; +use crate::util::NonZeroExt; + +/// A typed AST node. +pub trait AstNode: Sized { + /// Convert a node into its typed variant. + fn from_untyped(node: &SyntaxNode) -> Option<Self>; + + /// A reference to the underlying syntax node. + fn as_untyped(&self) -> &SyntaxNode; + + /// The source code location. + fn span(&self) -> Span { + self.as_untyped().span() + } +} + +macro_rules! node { + ($(#[$attr:meta])* $name:ident) => { + #[derive(Debug, Default, Clone, Hash)] + #[repr(transparent)] + $(#[$attr])* + pub struct $name(SyntaxNode); + + impl AstNode for $name { + fn from_untyped(node: &SyntaxNode) -> Option<Self> { + if matches!(node.kind(), SyntaxKind::$name) { + Some(Self(node.clone())) + } else { + Option::None + } + } + + fn as_untyped(&self) -> &SyntaxNode { + &self.0 + } + } + }; +} + +node! { + /// The syntactical root capable of representing a full parsed document. + Markup +} + +impl Markup { + /// The expressions. + pub fn exprs(&self) -> impl DoubleEndedIterator<Item = Expr> + '_ { + let mut was_stmt = false; + self.0 + .children() + .filter(move |node| { + // Ignore newline directly after statements without semicolons. + let kind = node.kind(); + let keep = !was_stmt || node.kind() != SyntaxKind::Space; + was_stmt = kind.is_stmt(); + keep + }) + .filter_map(Expr::cast_with_space) + } +} + +/// An expression in markup, math or code. +#[derive(Debug, Clone, Hash)] +pub enum Expr { + /// Plain text without markup. + Text(Text), + /// Whitespace in markup or math. Has at most one newline in markup, as more + /// indicate a paragraph break. + Space(Space), + /// A forced line break: `\`. + Linebreak(Linebreak), + /// A paragraph break, indicated by one or multiple blank lines. + Parbreak(Parbreak), + /// An escape sequence: `\#`, `\u{1F5FA}`. + Escape(Escape), + /// A shorthand for a unicode codepoint. For example, `~` for non-breaking + /// space or `-?` for a soft hyphen. + Shorthand(Shorthand), + /// A smart quote: `'` or `"`. + SmartQuote(SmartQuote), + /// Strong content: `*Strong*`. + Strong(Strong), + /// Emphasized content: `_Emphasized_`. + Emph(Emph), + /// Raw text with optional syntax highlighting: `` `...` ``. + Raw(Raw), + /// A hyperlink: `https://typst.org`. + Link(Link), + /// A label: `<intro>`. + Label(Label), + /// A reference: `@target`, `@target[..]`. + Ref(Ref), + /// A section heading: `= Introduction`. + Heading(Heading), + /// An item in a bullet list: `- ...`. + List(ListItem), + /// An item in an enumeration (numbered list): `+ ...` or `1. ...`. + Enum(EnumItem), + /// An item in a term list: `/ Term: Details`. + Term(TermItem), + /// A mathematical equation: `$x$`, `$ x^2 $`. + Equation(Equation), + /// The contents of a mathematical equation: `x^2 + 1`. + Math(Math), + /// An identifier in math: `pi`. + MathIdent(MathIdent), + /// An alignment point in math: `&`. + MathAlignPoint(MathAlignPoint), + /// Matched delimiters in math: `[x + y]`. + MathDelimited(MathDelimited), + /// A base with optional attachments in math: `a_1^2`. + MathAttach(MathAttach), + /// A fraction in math: `x/2`. + MathFrac(MathFrac), + /// A root in math: `√x`, `∛x` or `∜x`. + MathRoot(MathRoot), + /// An identifier: `left`. + Ident(Ident), + /// The `none` literal. + None(None), + /// The `auto` literal. + Auto(Auto), + /// A boolean: `true`, `false`. + Bool(Bool), + /// An integer: `120`. + Int(Int), + /// A floating-point number: `1.2`, `10e-4`. + Float(Float), + /// A numeric value with a unit: `12pt`, `3cm`, `2em`, `90deg`, `50%`. + Numeric(Numeric), + /// A quoted string: `"..."`. + Str(Str), + /// A code block: `{ let x = 1; x + 2 }`. + Code(CodeBlock), + /// A content block: `[*Hi* there!]`. + Content(ContentBlock), + /// A grouped expression: `(1 + 2)`. + Parenthesized(Parenthesized), + /// An array: `(1, "hi", 12cm)`. + Array(Array), + /// A dictionary: `(thickness: 3pt, pattern: dashed)`. + Dict(Dict), + /// A unary operation: `-x`. + Unary(Unary), + /// A binary operation: `a + b`. + Binary(Binary), + /// A field access: `properties.age`. + FieldAccess(FieldAccess), + /// An invocation of a function or method: `f(x, y)`. + FuncCall(FuncCall), + /// A closure: `(x, y) => z`. + Closure(Closure), + /// A let binding: `let x = 1`. + Let(LetBinding), + //// A destructuring assignment: `(x, y) = (1, 2)`. + DestructAssign(DestructAssignment), + /// A set rule: `set text(...)`. + Set(SetRule), + /// A show rule: `show heading: it => emph(it.body)`. + Show(ShowRule), + /// An if-else conditional: `if x { y } else { z }`. + Conditional(Conditional), + /// A while loop: `while x { y }`. + While(WhileLoop), + /// A for loop: `for x in y { z }`. + For(ForLoop), + /// A module import: `import "utils.typ": a, b, c`. + Import(ModuleImport), + /// A module include: `include "chapter1.typ"`. + Include(ModuleInclude), + /// A break from a loop: `break`. + Break(LoopBreak), + /// A continue in a loop: `continue`. + Continue(LoopContinue), + /// A return from a function: `return`, `return x + 1`. + Return(FuncReturn), +} + +impl Expr { + fn cast_with_space(node: &SyntaxNode) -> Option<Self> { + match node.kind() { + SyntaxKind::Space => node.cast().map(Self::Space), + _ => Self::from_untyped(node), + } + } +} + +impl AstNode for Expr { + fn from_untyped(node: &SyntaxNode) -> Option<Self> { + match node.kind() { + SyntaxKind::Linebreak => node.cast().map(Self::Linebreak), + SyntaxKind::Parbreak => node.cast().map(Self::Parbreak), + SyntaxKind::Text => node.cast().map(Self::Text), + SyntaxKind::Escape => node.cast().map(Self::Escape), + SyntaxKind::Shorthand => node.cast().map(Self::Shorthand), + SyntaxKind::SmartQuote => node.cast().map(Self::SmartQuote), + SyntaxKind::Strong => node.cast().map(Self::Strong), + SyntaxKind::Emph => node.cast().map(Self::Emph), + SyntaxKind::Raw => node.cast().map(Self::Raw), + SyntaxKind::Link => node.cast().map(Self::Link), + SyntaxKind::Label => node.cast().map(Self::Label), + SyntaxKind::Ref => node.cast().map(Self::Ref), + SyntaxKind::Heading => node.cast().map(Self::Heading), + SyntaxKind::ListItem => node.cast().map(Self::List), + SyntaxKind::EnumItem => node.cast().map(Self::Enum), + SyntaxKind::TermItem => node.cast().map(Self::Term), + SyntaxKind::Equation => node.cast().map(Self::Equation), + SyntaxKind::Math => node.cast().map(Self::Math), + SyntaxKind::MathIdent => node.cast().map(Self::MathIdent), + SyntaxKind::MathAlignPoint => node.cast().map(Self::MathAlignPoint), + SyntaxKind::MathDelimited => node.cast().map(Self::MathDelimited), + SyntaxKind::MathAttach => node.cast().map(Self::MathAttach), + SyntaxKind::MathFrac => node.cast().map(Self::MathFrac), + SyntaxKind::MathRoot => node.cast().map(Self::MathRoot), + SyntaxKind::Ident => node.cast().map(Self::Ident), + SyntaxKind::None => node.cast().map(Self::None), + SyntaxKind::Auto => node.cast().map(Self::Auto), + SyntaxKind::Bool => node.cast().map(Self::Bool), + SyntaxKind::Int => node.cast().map(Self::Int), + SyntaxKind::Float => node.cast().map(Self::Float), + SyntaxKind::Numeric => node.cast().map(Self::Numeric), + SyntaxKind::Str => node.cast().map(Self::Str), + SyntaxKind::CodeBlock => node.cast().map(Self::Code), + SyntaxKind::ContentBlock => node.cast().map(Self::Content), + SyntaxKind::Parenthesized => node.cast().map(Self::Parenthesized), + SyntaxKind::Array => node.cast().map(Self::Array), + SyntaxKind::Dict => node.cast().map(Self::Dict), + SyntaxKind::Unary => node.cast().map(Self::Unary), + SyntaxKind::Binary => node.cast().map(Self::Binary), + SyntaxKind::FieldAccess => node.cast().map(Self::FieldAccess), + SyntaxKind::FuncCall => node.cast().map(Self::FuncCall), + SyntaxKind::Closure => node.cast().map(Self::Closure), + SyntaxKind::LetBinding => node.cast().map(Self::Let), + SyntaxKind::DestructAssignment => node.cast().map(Self::DestructAssign), + SyntaxKind::SetRule => node.cast().map(Self::Set), + SyntaxKind::ShowRule => node.cast().map(Self::Show), + SyntaxKind::Conditional => node.cast().map(Self::Conditional), + SyntaxKind::WhileLoop => node.cast().map(Self::While), + SyntaxKind::ForLoop => node.cast().map(Self::For), + SyntaxKind::ModuleImport => node.cast().map(Self::Import), + SyntaxKind::ModuleInclude => node.cast().map(Self::Include), + SyntaxKind::LoopBreak => node.cast().map(Self::Break), + SyntaxKind::LoopContinue => node.cast().map(Self::Continue), + SyntaxKind::FuncReturn => node.cast().map(Self::Return), + _ => Option::None, + } + } + + fn as_untyped(&self) -> &SyntaxNode { + match self { + Self::Text(v) => v.as_untyped(), + Self::Space(v) => v.as_untyped(), + Self::Linebreak(v) => v.as_untyped(), + Self::Parbreak(v) => v.as_untyped(), + Self::Escape(v) => v.as_untyped(), + Self::Shorthand(v) => v.as_untyped(), + Self::SmartQuote(v) => v.as_untyped(), + Self::Strong(v) => v.as_untyped(), + Self::Emph(v) => v.as_untyped(), + Self::Raw(v) => v.as_untyped(), + Self::Link(v) => v.as_untyped(), + Self::Label(v) => v.as_untyped(), + Self::Ref(v) => v.as_untyped(), + Self::Heading(v) => v.as_untyped(), + Self::List(v) => v.as_untyped(), + Self::Enum(v) => v.as_untyped(), + Self::Term(v) => v.as_untyped(), + Self::Equation(v) => v.as_untyped(), + Self::Math(v) => v.as_untyped(), + Self::MathIdent(v) => v.as_untyped(), + Self::MathAlignPoint(v) => v.as_untyped(), + Self::MathDelimited(v) => v.as_untyped(), + Self::MathAttach(v) => v.as_untyped(), + Self::MathFrac(v) => v.as_untyped(), + Self::MathRoot(v) => v.as_untyped(), + Self::Ident(v) => v.as_untyped(), + Self::None(v) => v.as_untyped(), + Self::Auto(v) => v.as_untyped(), + Self::Bool(v) => v.as_untyped(), + Self::Int(v) => v.as_untyped(), + Self::Float(v) => v.as_untyped(), + Self::Numeric(v) => v.as_untyped(), + Self::Str(v) => v.as_untyped(), + Self::Code(v) => v.as_untyped(), + Self::Content(v) => v.as_untyped(), + Self::Array(v) => v.as_untyped(), + Self::Dict(v) => v.as_untyped(), + Self::Parenthesized(v) => v.as_untyped(), + Self::Unary(v) => v.as_untyped(), + Self::Binary(v) => v.as_untyped(), + Self::FieldAccess(v) => v.as_untyped(), + Self::FuncCall(v) => v.as_untyped(), + Self::Closure(v) => v.as_untyped(), + Self::Let(v) => v.as_untyped(), + Self::DestructAssign(v) => v.as_untyped(), + Self::Set(v) => v.as_untyped(), + Self::Show(v) => v.as_untyped(), + Self::Conditional(v) => v.as_untyped(), + Self::While(v) => v.as_untyped(), + Self::For(v) => v.as_untyped(), + Self::Import(v) => v.as_untyped(), + Self::Include(v) => v.as_untyped(), + Self::Break(v) => v.as_untyped(), + Self::Continue(v) => v.as_untyped(), + Self::Return(v) => v.as_untyped(), + } + } +} + +impl Expr { + /// Can this expression be embedded into markup with a hashtag? + pub fn hashtag(&self) -> bool { + matches!( + self, + Self::Ident(_) + | Self::None(_) + | Self::Auto(_) + | Self::Bool(_) + | Self::Int(_) + | Self::Float(_) + | Self::Numeric(_) + | Self::Str(_) + | Self::Code(_) + | Self::Content(_) + | Self::Array(_) + | Self::Dict(_) + | Self::Parenthesized(_) + | Self::FieldAccess(_) + | Self::FuncCall(_) + | Self::Let(_) + | Self::Set(_) + | Self::Show(_) + | Self::Conditional(_) + | Self::While(_) + | Self::For(_) + | Self::Import(_) + | Self::Include(_) + | Self::Break(_) + | Self::Continue(_) + | Self::Return(_) + ) + } + + /// Is this a literal? + pub fn is_literal(&self) -> bool { + matches!( + self, + Self::None(_) + | Self::Auto(_) + | Self::Bool(_) + | Self::Int(_) + | Self::Float(_) + | Self::Numeric(_) + | Self::Str(_) + ) + } +} + +impl Default for Expr { + fn default() -> Self { + Expr::Space(Space::default()) + } +} + +node! { + /// Plain text without markup. + Text +} + +impl Text { + /// Get the text. + pub fn get(&self) -> &EcoString { + self.0.text() + } +} + +node! { + /// Whitespace in markup or math. Has at most one newline in markup, as more + /// indicate a paragraph break. + Space +} + +node! { + /// A forced line break: `\`. + Linebreak +} + +node! { + /// A paragraph break, indicated by one or multiple blank lines. + Parbreak +} + +node! { + /// An escape sequence: `\#`, `\u{1F5FA}`. + Escape +} + +impl Escape { + /// Get the escaped character. + pub fn get(&self) -> char { + let mut s = Scanner::new(self.0.text()); + s.expect('\\'); + if s.eat_if("u{") { + let hex = s.eat_while(char::is_ascii_hexdigit); + u32::from_str_radix(hex, 16) + .ok() + .and_then(std::char::from_u32) + .unwrap_or_default() + } else { + s.eat().unwrap_or_default() + } + } +} + +node! { + /// A shorthand for a unicode codepoint. For example, `~` for a non-breaking + /// space or `-?` for a soft hyphen. + Shorthand +} + +impl Shorthand { + /// A list of all shorthands. + pub const LIST: &[(&'static str, char)] = &[ + // Both. + ("...", '…'), + // Text only. + ("~", '\u{00A0}'), + ("--", '\u{2013}'), + ("---", '\u{2014}'), + ("-?", '\u{00AD}'), + // Math only. + ("-", '\u{2212}'), + ("'", '′'), + ("*", '∗'), + ("!=", '≠'), + (":=", '≔'), + ("::=", '⩴'), + ("=:", '≕'), + ("<<", '≪'), + ("<<<", '⋘'), + (">>", '≫'), + (">>>", '⋙'), + ("<=", '≤'), + (">=", '≥'), + ("->", '→'), + ("-->", '⟶'), + ("|->", '↦'), + (">->", '↣'), + ("->>", '↠'), + ("<-", '←'), + ("<--", '⟵'), + ("<-<", '↢'), + ("<<-", '↞'), + ("<->", '↔'), + ("<-->", '⟷'), + ("~>", '⇝'), + ("~~>", '⟿'), + ("<~", '⇜'), + ("<~~", '⬳'), + ("=>", '⇒'), + ("|=>", '⤇'), + ("==>", '⟹'), + ("<==", '⟸'), + ("<=>", '⇔'), + ("<==>", '⟺'), + ("[|", '⟦'), + ("|]", '⟧'), + ("||", '‖'), + ]; + + /// Get the shorthanded character. + pub fn get(&self) -> char { + let text = self.0.text(); + Self::LIST + .iter() + .find(|&&(s, _)| s == text) + .map_or_else(char::default, |&(_, c)| c) + } +} + +node! { + /// A smart quote: `'` or `"`. + SmartQuote +} + +impl SmartQuote { + /// Whether this is a double quote. + pub fn double(&self) -> bool { + self.0.text() == "\"" + } +} + +node! { + /// Strong content: `*Strong*`. + Strong +} + +impl Strong { + /// The contents of the strong node. + pub fn body(&self) -> Markup { + self.0.cast_first_match().unwrap_or_default() + } +} + +node! { + /// Emphasized content: `_Emphasized_`. + Emph +} + +impl Emph { + /// The contents of the emphasis node. + pub fn body(&self) -> Markup { + self.0.cast_first_match().unwrap_or_default() + } +} + +node! { + /// Raw text with optional syntax highlighting: `` `...` ``. + Raw +} + +impl Raw { + /// The trimmed raw text. + pub fn text(&self) -> EcoString { + let mut text = self.0.text().as_str(); + let blocky = text.starts_with("```"); + text = text.trim_matches('`'); + + // Trim tag, one space at the start, and one space at the end if the + // last non-whitespace char is a backtick. + if blocky { + let mut s = Scanner::new(text); + if s.eat_if(is_id_start) { + s.eat_while(is_id_continue); + } + text = s.after(); + text = text.strip_prefix(' ').unwrap_or(text); + if text.trim_end().ends_with('`') { + text = text.strip_suffix(' ').unwrap_or(text); + } + } + + // Split into lines. + let mut lines = split_newlines(text); + + if blocky { + let dedent = lines + .iter() + .skip(1) + .map(|line| line.chars().take_while(|c| c.is_whitespace()).count()) + .min() + .unwrap_or(0); + + // Dedent based on column, but not for the first line. + for line in lines.iter_mut().skip(1) { + let offset = line.chars().take(dedent).map(char::len_utf8).sum(); + *line = &line[offset..]; + } + + let is_whitespace = |line: &&str| line.chars().all(char::is_whitespace); + + // Trims a sequence of whitespace followed by a newline at the start. + if lines.first().map_or(false, is_whitespace) { + lines.remove(0); + } + + // Trims a newline followed by a sequence of whitespace at the end. + if lines.last().map_or(false, is_whitespace) { + lines.pop(); + } + } + + lines.join("\n").into() + } + + /// An optional identifier specifying the language to syntax-highlight in. + pub fn lang(&self) -> Option<&str> { + let text = self.0.text(); + + // Only blocky literals are supposed to contain a language. + if !text.starts_with("```") { + return Option::None; + } + + let inner = text.trim_start_matches('`'); + let mut s = Scanner::new(inner); + s.eat_if(is_id_start).then(|| { + s.eat_while(is_id_continue); + s.before() + }) + } + + /// Whether the raw text should be displayed in a separate block. + pub fn block(&self) -> bool { + let text = self.0.text(); + text.starts_with("```") && text.chars().any(is_newline) + } +} + +node! { + /// A hyperlink: `https://typst.org`. + Link +} + +impl Link { + /// Get the URL. + pub fn get(&self) -> &EcoString { + self.0.text() + } +} + +node! { + /// A label: `<intro>`. + Label +} + +impl Label { + /// Get the label's text. + pub fn get(&self) -> &str { + self.0.text().trim_start_matches('<').trim_end_matches('>') + } +} + +node! { + /// A reference: `@target`, `@target[..]`. + Ref +} + +impl Ref { + /// Get the target. + pub fn target(&self) -> &str { + self.0 + .children() + .find(|node| node.kind() == SyntaxKind::RefMarker) + .map(|node| node.text().trim_start_matches('@')) + .unwrap_or_default() + } + + /// Get the supplement. + pub fn supplement(&self) -> Option<ContentBlock> { + self.0.cast_last_match() + } +} + +node! { + /// A section heading: `= Introduction`. + Heading +} + +impl Heading { + /// The contents of the heading. + pub fn body(&self) -> Markup { + self.0.cast_first_match().unwrap_or_default() + } + + /// The section depth (number of equals signs). + pub fn level(&self) -> NonZeroUsize { + self.0 + .children() + .find(|node| node.kind() == SyntaxKind::HeadingMarker) + .and_then(|node| node.len().try_into().ok()) + .unwrap_or(NonZeroUsize::ONE) + } +} + +node! { + /// An item in a bullet list: `- ...`. + ListItem +} + +impl ListItem { + /// The contents of the list item. + pub fn body(&self) -> Markup { + self.0.cast_first_match().unwrap_or_default() + } +} + +node! { + /// An item in an enumeration (numbered list): `+ ...` or `1. ...`. + EnumItem +} + +impl EnumItem { + /// The explicit numbering, if any: `23.`. + pub fn number(&self) -> Option<usize> { + self.0.children().find_map(|node| match node.kind() { + SyntaxKind::EnumMarker => node.text().trim_end_matches('.').parse().ok(), + _ => Option::None, + }) + } + + /// The contents of the list item. + pub fn body(&self) -> Markup { + self.0.cast_first_match().unwrap_or_default() + } +} + +node! { + /// An item in a term list: `/ Term: Details`. + TermItem +} + +impl TermItem { + /// The term described by the item. + pub fn term(&self) -> Markup { + self.0.cast_first_match().unwrap_or_default() + } + + /// The description of the term. + pub fn description(&self) -> Markup { + self.0.cast_last_match().unwrap_or_default() + } +} + +node! { + /// A mathemathical equation: `$x$`, `$ x^2 $`. + Equation +} + +impl Equation { + /// The contained math. + pub fn body(&self) -> Math { + self.0.cast_first_match().unwrap_or_default() + } + + /// Whether the equation should be displayed as a separate block. + pub fn block(&self) -> bool { + let is_space = |node: Option<&SyntaxNode>| { + node.map(SyntaxNode::kind) == Some(SyntaxKind::Space) + }; + is_space(self.0.children().nth(1)) && is_space(self.0.children().nth_back(1)) + } +} + +node! { + /// The contents of a mathematical equation: `x^2 + 1`. + Math +} + +impl Math { + /// The expressions the mathematical content consists of. + pub fn exprs(&self) -> impl DoubleEndedIterator<Item = Expr> + '_ { + self.0.children().filter_map(Expr::cast_with_space) + } +} + +node! { + /// An identifier in math: `pi`. + MathIdent +} + +impl MathIdent { + /// Get the identifier. + pub fn get(&self) -> &EcoString { + self.0.text() + } + + /// Take out the contained identifier. + pub fn take(self) -> EcoString { + self.0.into_text() + } + + /// Get the identifier as a string slice. + pub fn as_str(&self) -> &str { + self.get() + } +} + +impl Deref for MathIdent { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_str() + } +} + +node! { + /// An alignment point in math: `&`. + MathAlignPoint +} + +node! { + /// Matched delimiters in math: `[x + y]`. + MathDelimited +} + +impl MathDelimited { + /// The opening delimiter. + pub fn open(&self) -> Expr { + self.0.cast_first_match().unwrap_or_default() + } + + /// The contents, including the delimiters. + pub fn body(&self) -> Math { + self.0.cast_first_match().unwrap_or_default() + } + + /// The closing delimiter. + pub fn close(&self) -> Expr { + self.0.cast_last_match().unwrap_or_default() + } +} + +node! { + /// A base with optional attachments in math: `a_1^2`. + MathAttach +} + +impl MathAttach { + /// The base, to which things are attached. + pub fn base(&self) -> Expr { + self.0.cast_first_match().unwrap_or_default() + } + + /// The bottom attachment. + pub fn bottom(&self) -> Option<Expr> { + self.0 + .children() + .skip_while(|node| !matches!(node.kind(), SyntaxKind::Underscore)) + .find_map(SyntaxNode::cast) + } + + /// The top attachment. + pub fn top(&self) -> Option<Expr> { + self.0 + .children() + .skip_while(|node| !matches!(node.kind(), SyntaxKind::Hat)) + .find_map(SyntaxNode::cast) + } +} + +node! { + /// A fraction in math: `x/2` + MathFrac +} + +impl MathFrac { + /// The numerator. + pub fn num(&self) -> Expr { + self.0.cast_first_match().unwrap_or_default() + } + + /// The denominator. + pub fn denom(&self) -> Expr { + self.0.cast_last_match().unwrap_or_default() + } +} + +node! { + /// A root in math: `√x`, `∛x` or `∜x`. + MathRoot +} + +impl MathRoot { + /// The index of the root. + pub fn index(&self) -> Option<usize> { + match self.0.children().next().map(|node| node.text().as_str()) { + Some("∜") => Some(4), + Some("∛") => Some(3), + Some("√") => Option::None, + _ => Option::None, + } + } + + /// The radicand. + pub fn radicand(&self) -> Expr { + self.0.cast_first_match().unwrap_or_default() + } +} + +node! { + /// An identifier: `it`. + Ident +} + +impl Ident { + /// Get the identifier. + pub fn get(&self) -> &EcoString { + self.0.text() + } + + /// Take out the contained identifier. + pub fn take(self) -> EcoString { + self.0.into_text() + } + + /// Get the identifier as a string slice. + pub fn as_str(&self) -> &str { + self.get() + } +} + +impl Deref for Ident { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_str() + } +} + +node! { + /// The `none` literal. + None +} + +node! { + /// The `auto` literal. + Auto +} + +node! { + /// A boolean: `true`, `false`. + Bool +} + +impl Bool { + /// Get the boolean value. + pub fn get(&self) -> bool { + self.0.text() == "true" + } +} + +node! { + /// An integer: `120`. + Int +} + +impl Int { + /// Get the integer value. + pub fn get(&self) -> i64 { + let text = self.0.text(); + if let Some(rest) = text.strip_prefix("0x") { + i64::from_str_radix(rest, 16) + } else if let Some(rest) = text.strip_prefix("0o") { + i64::from_str_radix(rest, 8) + } else if let Some(rest) = text.strip_prefix("0b") { + i64::from_str_radix(rest, 2) + } else { + text.parse() + } + .unwrap_or_default() + } +} + +node! { + /// A floating-point number: `1.2`, `10e-4`. + Float +} + +impl Float { + /// Get the floating-point value. + pub fn get(&self) -> f64 { + self.0.text().parse().unwrap_or_default() + } +} + +node! { + /// A numeric value with a unit: `12pt`, `3cm`, `2em`, `90deg`, `50%`. + Numeric +} + +impl Numeric { + /// Get the numeric value and unit. + pub fn get(&self) -> (f64, Unit) { + let text = self.0.text(); + let count = text + .chars() + .rev() + .take_while(|c| matches!(c, 'a'..='z' | '%')) + .count(); + + let split = text.len() - count; + let value = text[..split].parse().unwrap_or_default(); + let unit = match &text[split..] { + "pt" => Unit::Length(AbsUnit::Pt), + "mm" => Unit::Length(AbsUnit::Mm), + "cm" => Unit::Length(AbsUnit::Cm), + "in" => Unit::Length(AbsUnit::In), + "deg" => Unit::Angle(AngleUnit::Deg), + "rad" => Unit::Angle(AngleUnit::Rad), + "em" => Unit::Em, + "fr" => Unit::Fr, + "%" => Unit::Percent, + _ => Unit::Percent, + }; + + (value, unit) + } +} + +/// Unit of a numeric value. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Unit { + /// An absolute length unit. + Length(AbsUnit), + /// An angular unit. + Angle(AngleUnit), + /// Font-relative: `1em` is the same as the font size. + Em, + /// Fractions: `fr`. + Fr, + /// Percentage: `%`. + Percent, +} + +node! { + /// A quoted string: `"..."`. + Str +} + +impl Str { + /// Get the string value with resolved escape sequences. + pub fn get(&self) -> EcoString { + let text = self.0.text(); + let unquoted = &text[1..text.len() - 1]; + if !unquoted.contains('\\') { + return unquoted.into(); + } + + let mut out = EcoString::with_capacity(unquoted.len()); + let mut s = Scanner::new(unquoted); + + while let Some(c) = s.eat() { + if c != '\\' { + out.push(c); + continue; + } + + let start = s.locate(-1); + match s.eat() { + Some('\\') => out.push('\\'), + Some('"') => out.push('"'), + Some('n') => out.push('\n'), + Some('r') => out.push('\r'), + Some('t') => out.push('\t'), + Some('u') if s.eat_if('{') => { + let sequence = s.eat_while(char::is_ascii_hexdigit); + s.eat_if('}'); + + match u32::from_str_radix(sequence, 16) + .ok() + .and_then(std::char::from_u32) + { + Some(c) => out.push(c), + Option::None => out.push_str(s.from(start)), + } + } + _ => out.push_str(s.from(start)), + } + } + + out + } +} + +node! { + /// A code block: `{ let x = 1; x + 2 }`. + CodeBlock +} + +impl CodeBlock { + /// The contained code. + pub fn body(&self) -> Code { + self.0.cast_first_match().unwrap_or_default() + } +} + +node! { + /// Code. + Code +} + +impl Code { + /// The list of expressions contained in the code. + pub fn exprs(&self) -> impl DoubleEndedIterator<Item = Expr> + '_ { + self.0.children().filter_map(SyntaxNode::cast) + } +} + +node! { + /// A content block: `[*Hi* there!]`. + ContentBlock +} + +impl ContentBlock { + /// The contained markup. + pub fn body(&self) -> Markup { + self.0.cast_first_match().unwrap_or_default() + } +} + +node! { + /// A grouped expression: `(1 + 2)`. + Parenthesized +} + +impl Parenthesized { + /// The wrapped expression. + pub fn expr(&self) -> Expr { + self.0.cast_first_match().unwrap_or_default() + } +} + +node! { + /// An array: `(1, "hi", 12cm)`. + Array +} + +impl Array { + /// The array's items. + pub fn items(&self) -> impl DoubleEndedIterator<Item = ArrayItem> + '_ { + self.0.children().filter_map(SyntaxNode::cast) + } +} + +/// An item in an array. +#[derive(Debug, Clone, Hash)] +pub enum ArrayItem { + /// A bare expression: `12`. + Pos(Expr), + /// A spread expression: `..things`. + Spread(Expr), +} + +impl AstNode for ArrayItem { + fn from_untyped(node: &SyntaxNode) -> Option<Self> { + match node.kind() { + SyntaxKind::Spread => node.cast_first_match().map(Self::Spread), + _ => node.cast().map(Self::Pos), + } + } + + fn as_untyped(&self) -> &SyntaxNode { + match self { + Self::Pos(v) => v.as_untyped(), + Self::Spread(v) => v.as_untyped(), + } + } +} + +node! { + /// A dictionary: `(thickness: 3pt, pattern: dashed)`. + Dict +} + +impl Dict { + /// The dictionary's items. + pub fn items(&self) -> impl DoubleEndedIterator<Item = DictItem> + '_ { + self.0.children().filter_map(SyntaxNode::cast) + } +} + +/// An item in an dictionary expression. +#[derive(Debug, Clone, Hash)] +pub enum DictItem { + /// A named pair: `thickness: 3pt`. + Named(Named), + /// A keyed pair: `"spacy key": true`. + Keyed(Keyed), + /// A spread expression: `..things`. + Spread(Expr), +} + +impl AstNode for DictItem { + fn from_untyped(node: &SyntaxNode) -> Option<Self> { + match node.kind() { + SyntaxKind::Named => node.cast().map(Self::Named), + SyntaxKind::Keyed => node.cast().map(Self::Keyed), + SyntaxKind::Spread => node.cast_first_match().map(Self::Spread), + _ => Option::None, + } + } + + fn as_untyped(&self) -> &SyntaxNode { + match self { + Self::Named(v) => v.as_untyped(), + Self::Keyed(v) => v.as_untyped(), + Self::Spread(v) => v.as_untyped(), + } + } +} + +node! { + /// A named pair: `thickness: 3pt`. + Named +} + +impl Named { + /// The name: `thickness`. + pub fn name(&self) -> Ident { + self.0.cast_first_match().unwrap_or_default() + } + + /// The right-hand side of the pair: `3pt`. + pub fn expr(&self) -> Expr { + self.0.cast_last_match().unwrap_or_default() + } + + /// The right-hand side of the pair as an identifier. + pub fn expr_ident(&self) -> Option<Ident> { + self.0.cast_last_match() + } +} + +node! { + /// A keyed pair: `"spacy key": true`. + Keyed +} + +impl Keyed { + /// The key: `"spacy key"`. + pub fn key(&self) -> Str { + self.0 + .children() + .find_map(|node| node.cast::<Str>()) + .unwrap_or_default() + } + + /// The right-hand side of the pair: `true`. + pub fn expr(&self) -> Expr { + self.0.cast_last_match().unwrap_or_default() + } +} + +node! { + /// A unary operation: `-x`. + Unary +} + +impl Unary { + /// The operator: `-`. + pub fn op(&self) -> UnOp { + self.0 + .children() + .find_map(|node| UnOp::from_kind(node.kind())) + .unwrap_or(UnOp::Pos) + } + + /// The expression to operate on: `x`. + pub fn expr(&self) -> Expr { + self.0.cast_last_match().unwrap_or_default() + } +} + +/// A unary operator. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum UnOp { + /// The plus operator: `+`. + Pos, + /// The negation operator: `-`. + Neg, + /// The boolean `not`. + Not, +} + +impl UnOp { + /// Try to convert the token into a unary operation. + pub fn from_kind(token: SyntaxKind) -> Option<Self> { + Some(match token { + SyntaxKind::Plus => Self::Pos, + SyntaxKind::Minus => Self::Neg, + SyntaxKind::Not => Self::Not, + _ => return Option::None, + }) + } + + /// The precedence of this operator. + pub fn precedence(self) -> usize { + match self { + Self::Pos | Self::Neg => 7, + Self::Not => 4, + } + } + + /// The string representation of this operation. + pub fn as_str(self) -> &'static str { + match self { + Self::Pos => "+", + Self::Neg => "-", + Self::Not => "not", + } + } +} + +node! { + /// A binary operation: `a + b`. + Binary +} + +impl Binary { + /// The binary operator: `+`. + pub fn op(&self) -> BinOp { + let mut not = false; + self.0 + .children() + .find_map(|node| match node.kind() { + SyntaxKind::Not => { + not = true; + Option::None + } + SyntaxKind::In if not => Some(BinOp::NotIn), + _ => BinOp::from_kind(node.kind()), + }) + .unwrap_or(BinOp::Add) + } + + /// The left-hand side of the operation: `a`. + pub fn lhs(&self) -> Expr { + self.0.cast_first_match().unwrap_or_default() + } + + /// The right-hand side of the operation: `b`. + pub fn rhs(&self) -> Expr { + self.0.cast_last_match().unwrap_or_default() + } +} + +/// A binary operator. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum BinOp { + /// The addition operator: `+`. + Add, + /// The subtraction operator: `-`. + Sub, + /// The multiplication operator: `*`. + Mul, + /// The division operator: `/`. + Div, + /// The short-circuiting boolean `and`. + And, + /// The short-circuiting boolean `or`. + Or, + /// The equality operator: `==`. + Eq, + /// The inequality operator: `!=`. + Neq, + /// The less-than operator: `<`. + Lt, + /// The less-than or equal operator: `<=`. + Leq, + /// The greater-than operator: `>`. + Gt, + /// The greater-than or equal operator: `>=`. + Geq, + /// The assignment operator: `=`. + Assign, + /// The containment operator: `in`. + In, + /// The inversed containment operator: `not in`. + NotIn, + /// The add-assign operator: `+=`. + AddAssign, + /// The subtract-assign oeprator: `-=`. + SubAssign, + /// The multiply-assign operator: `*=`. + MulAssign, + /// The divide-assign operator: `/=`. + DivAssign, +} + +impl BinOp { + /// Try to convert the token into a binary operation. + pub fn from_kind(token: SyntaxKind) -> Option<Self> { + Some(match token { + SyntaxKind::Plus => Self::Add, + SyntaxKind::Minus => Self::Sub, + SyntaxKind::Star => Self::Mul, + SyntaxKind::Slash => Self::Div, + SyntaxKind::And => Self::And, + SyntaxKind::Or => Self::Or, + SyntaxKind::EqEq => Self::Eq, + SyntaxKind::ExclEq => Self::Neq, + SyntaxKind::Lt => Self::Lt, + SyntaxKind::LtEq => Self::Leq, + SyntaxKind::Gt => Self::Gt, + SyntaxKind::GtEq => Self::Geq, + SyntaxKind::Eq => Self::Assign, + SyntaxKind::In => Self::In, + SyntaxKind::PlusEq => Self::AddAssign, + SyntaxKind::HyphEq => Self::SubAssign, + SyntaxKind::StarEq => Self::MulAssign, + SyntaxKind::SlashEq => Self::DivAssign, + _ => return Option::None, + }) + } + + /// The precedence of this operator. + pub fn precedence(self) -> usize { + match self { + Self::Mul => 6, + Self::Div => 6, + Self::Add => 5, + Self::Sub => 5, + Self::Eq => 4, + Self::Neq => 4, + Self::Lt => 4, + Self::Leq => 4, + Self::Gt => 4, + Self::Geq => 4, + Self::In => 4, + Self::NotIn => 4, + Self::And => 3, + Self::Or => 2, + Self::Assign => 1, + Self::AddAssign => 1, + Self::SubAssign => 1, + Self::MulAssign => 1, + Self::DivAssign => 1, + } + } + + /// The associativity of this operator. + pub fn assoc(self) -> Assoc { + match self { + Self::Add => Assoc::Left, + Self::Sub => Assoc::Left, + Self::Mul => Assoc::Left, + Self::Div => Assoc::Left, + Self::And => Assoc::Left, + Self::Or => Assoc::Left, + Self::Eq => Assoc::Left, + Self::Neq => Assoc::Left, + Self::Lt => Assoc::Left, + Self::Leq => Assoc::Left, + Self::Gt => Assoc::Left, + Self::Geq => Assoc::Left, + Self::In => Assoc::Left, + Self::NotIn => Assoc::Left, + Self::Assign => Assoc::Right, + Self::AddAssign => Assoc::Right, + Self::SubAssign => Assoc::Right, + Self::MulAssign => Assoc::Right, + Self::DivAssign => Assoc::Right, + } + } + + /// The string representation of this operation. + pub fn as_str(self) -> &'static str { + match self { + Self::Add => "+", + Self::Sub => "-", + Self::Mul => "*", + Self::Div => "/", + Self::And => "and", + Self::Or => "or", + Self::Eq => "==", + Self::Neq => "!=", + Self::Lt => "<", + Self::Leq => "<=", + Self::Gt => ">", + Self::Geq => ">=", + Self::In => "in", + Self::NotIn => "not in", + Self::Assign => "=", + Self::AddAssign => "+=", + Self::SubAssign => "-=", + Self::MulAssign => "*=", + Self::DivAssign => "/=", + } + } +} + +/// The associativity of a binary operator. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Assoc { + /// Left-associative: `a + b + c` is equivalent to `(a + b) + c`. + Left, + /// Right-associative: `a = b = c` is equivalent to `a = (b = c)`. + Right, +} + +node! { + /// A field access: `properties.age`. + FieldAccess +} + +impl FieldAccess { + /// The expression to access the field on. + pub fn target(&self) -> Expr { + self.0.cast_first_match().unwrap_or_default() + } + + /// The name of the field. + pub fn field(&self) -> Ident { + self.0.cast_last_match().unwrap_or_default() + } +} + +node! { + /// An invocation of a function or method: `f(x, y)`. + FuncCall +} + +impl FuncCall { + /// The function to call. + pub fn callee(&self) -> Expr { + self.0.cast_first_match().unwrap_or_default() + } + + /// The arguments to the function. + pub fn args(&self) -> Args { + self.0.cast_last_match().unwrap_or_default() + } +} + +node! { + /// A function call's argument list: `(12pt, y)`. + Args +} + +impl Args { + /// The positional and named arguments. + pub fn items(&self) -> impl DoubleEndedIterator<Item = Arg> + '_ { + self.0.children().filter_map(SyntaxNode::cast) + } +} + +/// An argument to a function call. +#[derive(Debug, Clone, Hash)] +pub enum Arg { + /// A positional argument: `12`. + Pos(Expr), + /// A named argument: `draw: false`. + Named(Named), + /// A spread argument: `..things`. + Spread(Expr), +} + +impl AstNode for Arg { + fn from_untyped(node: &SyntaxNode) -> Option<Self> { + match node.kind() { + SyntaxKind::Named => node.cast().map(Self::Named), + SyntaxKind::Spread => node.cast_first_match().map(Self::Spread), + _ => node.cast().map(Self::Pos), + } + } + + fn as_untyped(&self) -> &SyntaxNode { + match self { + Self::Pos(v) => v.as_untyped(), + Self::Named(v) => v.as_untyped(), + Self::Spread(v) => v.as_untyped(), + } + } +} + +node! { + /// A closure: `(x, y) => z`. + Closure +} + +impl Closure { + /// The name of the closure. + /// + /// This only exists if you use the function syntax sugar: `let f(x) = y`. + pub fn name(&self) -> Option<Ident> { + self.0.children().next()?.cast() + } + + /// The parameter bindings. + pub fn params(&self) -> Params { + self.0.cast_first_match().unwrap_or_default() + } + + /// The body of the closure. + pub fn body(&self) -> Expr { + self.0.cast_last_match().unwrap_or_default() + } +} + +node! { + /// A closure's parameters: `(x, y)`. + Params +} + +impl Params { + /// The parameter bindings. + pub fn children(&self) -> impl DoubleEndedIterator<Item = Param> + '_ { + self.0.children().filter_map(SyntaxNode::cast) + } +} + +node! { + /// A spread: `..x` or `..x.at(0)`. + Spread +} + +impl Spread { + /// Try to get an identifier. + pub fn name(&self) -> Option<Ident> { + self.0.cast_first_match() + } + + /// Try to get an expression. + pub fn expr(&self) -> Option<Expr> { + self.0.cast_first_match() + } +} + +node! { + /// An underscore: `_` + Underscore +} + +/// A parameter to a closure. +#[derive(Debug, Clone, Hash)] +pub enum Param { + /// A positional parameter: `x`. + Pos(Pattern), + /// A named parameter with a default value: `draw: false`. + Named(Named), + /// An argument sink: `..args`. + Sink(Spread), +} + +impl AstNode for Param { + fn from_untyped(node: &SyntaxNode) -> Option<Self> { + match node.kind() { + SyntaxKind::Named => node.cast().map(Self::Named), + SyntaxKind::Spread => node.cast().map(Self::Sink), + _ => node.cast().map(Self::Pos), + } + } + + fn as_untyped(&self) -> &SyntaxNode { + match self { + Self::Pos(v) => v.as_untyped(), + Self::Named(v) => v.as_untyped(), + Self::Sink(v) => v.as_untyped(), + } + } +} + +node! { + /// A destructuring pattern: `x` or `(x, _, ..y)`. + Destructuring +} + +impl Destructuring { + /// The bindings of the destructuring. + pub fn bindings(&self) -> impl Iterator<Item = DestructuringKind> + '_ { + self.0.children().filter_map(SyntaxNode::cast) + } + + // Returns a list of all identifiers in the pattern. + pub fn idents(&self) -> impl Iterator<Item = Ident> + '_ { + self.bindings().filter_map(|binding| match binding { + DestructuringKind::Normal(Expr::Ident(ident)) => Some(ident), + DestructuringKind::Sink(spread) => spread.name(), + DestructuringKind::Named(named) => named.expr_ident(), + _ => Option::None, + }) + } +} + +/// The kind of an element in a destructuring pattern. +#[derive(Debug, Clone, Hash)] +pub enum DestructuringKind { + /// An expression: `x`. + Normal(Expr), + /// An argument sink: `..y`. + Sink(Spread), + /// Named arguments: `x: 1`. + Named(Named), + /// A placeholder: `_`. + Placeholder(Underscore), +} + +impl AstNode for DestructuringKind { + fn from_untyped(node: &SyntaxNode) -> Option<Self> { + match node.kind() { + SyntaxKind::Named => node.cast().map(Self::Named), + SyntaxKind::Spread => node.cast().map(Self::Sink), + SyntaxKind::Underscore => node.cast().map(Self::Placeholder), + _ => node.cast().map(Self::Normal), + } + } + + fn as_untyped(&self) -> &SyntaxNode { + match self { + Self::Normal(v) => v.as_untyped(), + Self::Named(v) => v.as_untyped(), + Self::Sink(v) => v.as_untyped(), + Self::Placeholder(v) => v.as_untyped(), + } + } +} + +/// The kind of a pattern. +#[derive(Debug, Clone, Hash)] +pub enum Pattern { + /// A single expression: `x`. + Normal(Expr), + /// A placeholder: `_`. + Placeholder(Underscore), + /// A destructuring pattern: `(x, _, ..y)`. + Destructuring(Destructuring), +} + +impl AstNode for Pattern { + fn from_untyped(node: &SyntaxNode) -> Option<Self> { + match node.kind() { + SyntaxKind::Destructuring => node.cast().map(Self::Destructuring), + SyntaxKind::Underscore => node.cast().map(Self::Placeholder), + _ => node.cast().map(Self::Normal), + } + } + + fn as_untyped(&self) -> &SyntaxNode { + match self { + Self::Normal(v) => v.as_untyped(), + Self::Destructuring(v) => v.as_untyped(), + Self::Placeholder(v) => v.as_untyped(), + } + } +} + +impl Pattern { + // Returns a list of all identifiers in the pattern. + pub fn idents(&self) -> Vec<Ident> { + match self { + Pattern::Normal(Expr::Ident(ident)) => vec![ident.clone()], + Pattern::Destructuring(destruct) => destruct.idents().collect(), + _ => vec![], + } + } +} + +impl Default for Pattern { + fn default() -> Self { + Self::Normal(Expr::default()) + } +} + +node! { + /// A let binding: `let x = 1`. + LetBinding +} + +#[derive(Debug)] +pub enum LetBindingKind { + /// A normal binding: `let x = 1`. + Normal(Pattern), + /// A closure binding: `let f(x) = 1`. + Closure(Ident), +} + +impl LetBindingKind { + // Returns a list of all identifiers in the pattern. + pub fn idents(&self) -> Vec<Ident> { + match self { + LetBindingKind::Normal(pattern) => pattern.idents(), + LetBindingKind::Closure(ident) => { + vec![ident.clone()] + } + } + } +} + +impl LetBinding { + /// The kind of the let binding. + pub fn kind(&self) -> LetBindingKind { + match self.0.cast_first_match::<Pattern>() { + Some(Pattern::Normal(Expr::Closure(closure))) => { + LetBindingKind::Closure(closure.name().unwrap_or_default()) + } + pattern => LetBindingKind::Normal(pattern.unwrap_or_default()), + } + } + + /// The expression the binding is initialized with. + pub fn init(&self) -> Option<Expr> { + match self.kind() { + LetBindingKind::Normal(Pattern::Normal(_)) => { + self.0.children().filter_map(SyntaxNode::cast).nth(1) + } + LetBindingKind::Normal(_) => self.0.cast_first_match(), + LetBindingKind::Closure(_) => self.0.cast_first_match(), + } + } +} + +node! { + /// An assignment expression `(x, y) = (1, 2)`. + DestructAssignment +} + +impl DestructAssignment { + /// The pattern of the assignment. + pub fn pattern(&self) -> Pattern { + self.0.cast_first_match::<Pattern>().unwrap_or_default() + } + + /// The expression that is assigned. + pub fn value(&self) -> Expr { + self.0.cast_last_match().unwrap_or_default() + } +} + +node! { + /// A set rule: `set text(...)`. + SetRule +} + +impl SetRule { + /// The function to set style properties for. + pub fn target(&self) -> Expr { + self.0.cast_first_match().unwrap_or_default() + } + + /// The style properties to set. + pub fn args(&self) -> Args { + self.0.cast_last_match().unwrap_or_default() + } + + /// A condition under which the set rule applies. + pub fn condition(&self) -> Option<Expr> { + self.0 + .children() + .skip_while(|child| child.kind() != SyntaxKind::If) + .find_map(SyntaxNode::cast) + } +} + +node! { + /// A show rule: `show heading: it => emph(it.body)`. + ShowRule +} + +impl ShowRule { + /// Defines which nodes the show rule applies to. + pub fn selector(&self) -> Option<Expr> { + self.0 + .children() + .rev() + .skip_while(|child| child.kind() != SyntaxKind::Colon) + .find_map(SyntaxNode::cast) + } + + /// The transformation recipe. + pub fn transform(&self) -> Expr { + self.0.cast_last_match().unwrap_or_default() + } +} + +node! { + /// An if-else conditional: `if x { y } else { z }`. + Conditional +} + +impl Conditional { + /// The condition which selects the body to evaluate. + pub fn condition(&self) -> Expr { + self.0.cast_first_match().unwrap_or_default() + } + + /// The expression to evaluate if the condition is true. + pub fn if_body(&self) -> Expr { + self.0 + .children() + .filter_map(SyntaxNode::cast) + .nth(1) + .unwrap_or_default() + } + + /// The expression to evaluate if the condition is false. + pub fn else_body(&self) -> Option<Expr> { + self.0.children().filter_map(SyntaxNode::cast).nth(2) + } +} + +node! { + /// A while loop: `while x { y }`. + WhileLoop +} + +impl WhileLoop { + /// The condition which selects whether to evaluate the body. + pub fn condition(&self) -> Expr { + self.0.cast_first_match().unwrap_or_default() + } + + /// The expression to evaluate while the condition is true. + pub fn body(&self) -> Expr { + self.0.cast_last_match().unwrap_or_default() + } +} + +node! { + /// A for loop: `for x in y { z }`. + ForLoop +} + +impl ForLoop { + /// The pattern to assign to. + pub fn pattern(&self) -> Pattern { + self.0.cast_first_match().unwrap_or_default() + } + + /// The expression to iterate over. + pub fn iter(&self) -> Expr { + self.0 + .children() + .skip_while(|&c| c.kind() != SyntaxKind::In) + .find_map(SyntaxNode::cast) + .unwrap_or_default() + } + + /// The expression to evaluate for each iteration. + pub fn body(&self) -> Expr { + self.0.cast_last_match().unwrap_or_default() + } +} + +node! { + /// A module import: `import "utils.typ": a, b, c`. + ModuleImport +} + +impl ModuleImport { + /// The module or path from which the items should be imported. + pub fn source(&self) -> Expr { + self.0.cast_first_match().unwrap_or_default() + } + + /// The items to be imported. + pub fn imports(&self) -> Option<Imports> { + self.0.children().find_map(|node| match node.kind() { + SyntaxKind::Star => Some(Imports::Wildcard), + SyntaxKind::ImportItems => { + let items = node.children().filter_map(SyntaxNode::cast).collect(); + Some(Imports::Items(items)) + } + _ => Option::None, + }) + } +} + +/// The items that ought to be imported from a file. +#[derive(Debug, Clone, Hash)] +pub enum Imports { + /// All items in the scope of the file should be imported. + Wildcard, + /// The specified items from the file should be imported. + Items(Vec<Ident>), +} + +node! { + /// A module include: `include "chapter1.typ"`. + ModuleInclude +} + +impl ModuleInclude { + /// The module or path from which the content should be included. + pub fn source(&self) -> Expr { + self.0.cast_last_match().unwrap_or_default() + } +} + +node! { + /// A break from a loop: `break`. + LoopBreak +} + +node! { + /// A continue in a loop: `continue`. + LoopContinue +} + +node! { + /// A return from a function: `return`, `return x + 1`. + FuncReturn +} + +impl FuncReturn { + /// The expression to return. + pub fn body(&self) -> Option<Expr> { + self.0.cast_last_match() + } +} diff --git a/crates/typst/src/syntax/kind.rs b/crates/typst/src/syntax/kind.rs new file mode 100644 index 00000000..26e949ca --- /dev/null +++ b/crates/typst/src/syntax/kind.rs @@ -0,0 +1,448 @@ +/// A syntactical building block of a Typst file. +/// +/// Can be created by the lexer or by the parser. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[repr(u8)] +pub enum SyntaxKind { + /// Markup. + Markup, + /// Plain text without markup. + Text, + /// Whitespace. Contains at most one newline in markup, as more indicate a + /// paragraph break. + Space, + /// A forced line break: `\`. + Linebreak, + /// A paragraph break, indicated by one or multiple blank lines. + Parbreak, + /// An escape sequence: `\#`, `\u{1F5FA}`. + Escape, + /// A shorthand for a unicode codepoint. For example, `~` for non-breaking + /// space or `-?` for a soft hyphen. + Shorthand, + /// A smart quote: `'` or `"`. + SmartQuote, + /// Strong content: `*Strong*`. + Strong, + /// Emphasized content: `_Emphasized_`. + Emph, + /// Raw text with optional syntax highlighting: `` `...` ``. + Raw, + /// A hyperlink: `https://typst.org`. + Link, + /// A label: `<intro>`. + Label, + /// A reference: `@target`, `@target[..]`. + Ref, + /// Introduces a reference: `@target`. + RefMarker, + /// A section heading: `= Introduction`. + Heading, + /// Introduces a section heading: `=`, `==`, ... + HeadingMarker, + /// An item in a bullet list: `- ...`. + ListItem, + /// Introduces a list item: `-`. + ListMarker, + /// An item in an enumeration (numbered list): `+ ...` or `1. ...`. + EnumItem, + /// Introduces an enumeration item: `+`, `1.`. + EnumMarker, + /// An item in a term list: `/ Term: Details`. + TermItem, + /// Introduces a term item: `/`. + TermMarker, + /// A mathematical equation: `$x$`, `$ x^2 $`. + Equation, + + /// The contents of a mathematical equation: `x^2 + 1`. + Math, + /// An identifier in math: `pi`. + MathIdent, + /// An alignment point in math: `&`. + MathAlignPoint, + /// Matched delimiters in math: `[x + y]`. + MathDelimited, + /// A base with optional attachments in math: `a_1^2`. + MathAttach, + /// A fraction in math: `x/2`. + MathFrac, + /// A root in math: `√x`, `∛x` or `∜x`. + MathRoot, + + /// A hashtag that switches into code mode: `#`. + Hashtag, + /// A left curly brace, starting a code block: `{`. + LeftBrace, + /// A right curly brace, terminating a code block: `}`. + RightBrace, + /// A left square bracket, starting a content block: `[`. + LeftBracket, + /// A right square bracket, terminating a content block: `]`. + RightBracket, + /// A left round parenthesis, starting a grouped expression, collection, + /// argument or parameter list: `(`. + LeftParen, + /// A right round parenthesis, terminating a grouped expression, collection, + /// argument or parameter list: `)`. + RightParen, + /// A comma separator in a sequence: `,`. + Comma, + /// A semicolon terminating an expression: `;`. + Semicolon, + /// A colon between name/key and value in a dictionary, argument or + /// parameter list, or between the term and body of a term list term: `:`. + Colon, + /// The strong text toggle, multiplication operator, and wildcard import + /// symbol: `*`. + Star, + /// Toggles emphasized text and indicates a subscript in math: `_`. + Underscore, + /// Starts and ends a mathematical equation: `$`. + Dollar, + /// The unary plus and binary addition operator: `+`. + Plus, + /// The unary negation and binary subtraction operator: `-`. + Minus, + /// The division operator and fraction operator in math: `/`. + Slash, + /// The superscript operator in math: `^`. + Hat, + /// The field access and method call operator: `.`. + Dot, + /// The assignment operator: `=`. + Eq, + /// The equality operator: `==`. + EqEq, + /// The inequality operator: `!=`. + ExclEq, + /// The less-than operator: `<`. + Lt, + /// The less-than or equal operator: `<=`. + LtEq, + /// The greater-than operator: `>`. + Gt, + /// The greater-than or equal operator: `>=`. + GtEq, + /// The add-assign operator: `+=`. + PlusEq, + /// The subtract-assign operator: `-=`. + HyphEq, + /// The multiply-assign operator: `*=`. + StarEq, + /// The divide-assign operator: `/=`. + SlashEq, + /// The spread operator: `..`. + Dots, + /// An arrow between a closure's parameters and body: `=>`. + Arrow, + /// A root: `√`, `∛` or `∜`. + Root, + + /// The `not` operator. + Not, + /// The `and` operator. + And, + /// The `or` operator. + Or, + /// The `none` literal. + None, + /// The `auto` literal. + Auto, + /// The `let` keyword. + Let, + /// The `set` keyword. + Set, + /// The `show` keyword. + Show, + /// The `if` keyword. + If, + /// The `else` keyword. + Else, + /// The `for` keyword. + For, + /// The `in` keyword. + In, + /// The `while` keyword. + While, + /// The `break` keyword. + Break, + /// The `continue` keyword. + Continue, + /// The `return` keyword. + Return, + /// The `import` keyword. + Import, + /// The `include` keyword. + Include, + /// The `as` keyword. + As, + + /// Code. + Code, + /// An identifier: `it`. + Ident, + /// A boolean: `true`, `false`. + Bool, + /// An integer: `120`. + Int, + /// A floating-point number: `1.2`, `10e-4`. + Float, + /// A numeric value with a unit: `12pt`, `3cm`, `2em`, `90deg`, `50%`. + Numeric, + /// A quoted string: `"..."`. + Str, + /// A code block: `{ let x = 1; x + 2 }`. + CodeBlock, + /// A content block: `[*Hi* there!]`. + ContentBlock, + /// A grouped expression: `(1 + 2)`. + Parenthesized, + /// An array: `(1, "hi", 12cm)`. + Array, + /// A dictionary: `(thickness: 3pt, pattern: dashed)`. + Dict, + /// A named pair: `thickness: 3pt`. + Named, + /// A keyed pair: `"spacy key": true`. + Keyed, + /// A unary operation: `-x`. + Unary, + /// A binary operation: `a + b`. + Binary, + /// A field access: `properties.age`. + FieldAccess, + /// An invocation of a function or method: `f(x, y)`. + FuncCall, + /// A function call's argument list: `(12pt, y)`. + Args, + /// Spread arguments or an argument sink: `..x`. + Spread, + /// A closure: `(x, y) => z`. + Closure, + /// A closure's parameters: `(x, y)`. + Params, + /// A let binding: `let x = 1`. + LetBinding, + /// A set rule: `set text(...)`. + SetRule, + /// A show rule: `show heading: it => emph(it.body)`. + ShowRule, + /// An if-else conditional: `if x { y } else { z }`. + Conditional, + /// A while loop: `while x { y }`. + WhileLoop, + /// A for loop: `for x in y { z }`. + ForLoop, + /// A module import: `import "utils.typ": a, b, c`. + ModuleImport, + /// Items to import from a module: `a, b, c`. + ImportItems, + /// A module include: `include "chapter1.typ"`. + ModuleInclude, + /// A break from a loop: `break`. + LoopBreak, + /// A continue in a loop: `continue`. + LoopContinue, + /// A return from a function: `return`, `return x + 1`. + FuncReturn, + /// A destructuring pattern: `(x, _, ..y)`. + Destructuring, + /// A destructuring assignment expression: `(x, y) = (1, 2)`. + DestructAssignment, + + /// A line comment: `// ...`. + LineComment, + /// A block comment: `/* ... */`. + BlockComment, + /// An invalid sequence of characters. + Error, + /// The end of the file. + Eof, +} + +impl SyntaxKind { + /// Is this a bracket, brace, or parenthesis? + pub fn is_grouping(self) -> bool { + matches!( + self, + Self::LeftBracket + | Self::LeftBrace + | Self::LeftParen + | Self::RightBracket + | Self::RightBrace + | Self::RightParen + ) + } + + /// Does this node terminate a preceding expression? + pub fn is_terminator(self) -> bool { + matches!( + self, + Self::Eof + | Self::Semicolon + | Self::RightBrace + | Self::RightParen + | Self::RightBracket + ) + } + + /// Is this a code or content block. + pub fn is_block(self) -> bool { + matches!(self, Self::CodeBlock | Self::ContentBlock) + } + + /// Does this node need termination through a semicolon or linebreak? + pub fn is_stmt(self) -> bool { + matches!( + self, + Self::LetBinding + | Self::SetRule + | Self::ShowRule + | Self::ModuleImport + | Self::ModuleInclude + ) + } + + /// Whether this kind of node is automatically skipped by the parser in + /// code and math mode. + pub fn is_trivia(self) -> bool { + matches!( + self, + Self::Space | Self::Parbreak | Self::LineComment | Self::BlockComment + ) + } + + /// Whether this is an error. + pub fn is_error(self) -> bool { + self == Self::Error + } + + /// A human-readable name for the kind. + pub fn name(self) -> &'static str { + match self { + Self::Markup => "markup", + Self::Text => "text", + Self::Space => "space", + Self::Linebreak => "line break", + Self::Parbreak => "paragraph break", + Self::Escape => "escape sequence", + Self::Shorthand => "shorthand", + Self::SmartQuote => "smart quote", + Self::Strong => "strong content", + Self::Emph => "emphasized content", + Self::Raw => "raw block", + Self::Link => "link", + Self::Label => "label", + Self::Ref => "reference", + Self::RefMarker => "reference marker", + Self::Heading => "heading", + Self::HeadingMarker => "heading marker", + Self::ListItem => "list item", + Self::ListMarker => "list marker", + Self::EnumItem => "enum item", + Self::EnumMarker => "enum marker", + Self::TermItem => "term list item", + Self::TermMarker => "term marker", + Self::Equation => "equation", + Self::Math => "math", + Self::MathIdent => "math identifier", + Self::MathAlignPoint => "math alignment point", + Self::MathDelimited => "delimited math", + Self::MathAttach => "math attachments", + Self::MathFrac => "math fraction", + Self::MathRoot => "math root", + Self::Hashtag => "hashtag", + Self::LeftBrace => "opening brace", + Self::RightBrace => "closing brace", + Self::LeftBracket => "opening bracket", + Self::RightBracket => "closing bracket", + Self::LeftParen => "opening paren", + Self::RightParen => "closing paren", + Self::Comma => "comma", + Self::Semicolon => "semicolon", + Self::Colon => "colon", + Self::Star => "star", + Self::Underscore => "underscore", + Self::Dollar => "dollar sign", + Self::Plus => "plus", + Self::Minus => "minus", + Self::Slash => "slash", + Self::Hat => "hat", + Self::Dot => "dot", + Self::Eq => "equals sign", + Self::EqEq => "equality operator", + Self::ExclEq => "inequality operator", + Self::Lt => "less-than operator", + Self::LtEq => "less-than or equal operator", + Self::Gt => "greater-than operator", + Self::GtEq => "greater-than or equal operator", + Self::PlusEq => "add-assign operator", + Self::HyphEq => "subtract-assign operator", + Self::StarEq => "multiply-assign operator", + Self::SlashEq => "divide-assign operator", + Self::Dots => "dots", + Self::Arrow => "arrow", + Self::Root => "root", + Self::Not => "operator `not`", + Self::And => "operator `and`", + Self::Or => "operator `or`", + Self::None => "`none`", + Self::Auto => "`auto`", + Self::Let => "keyword `let`", + Self::Set => "keyword `set`", + Self::Show => "keyword `show`", + Self::If => "keyword `if`", + Self::Else => "keyword `else`", + Self::For => "keyword `for`", + Self::In => "keyword `in`", + Self::While => "keyword `while`", + Self::Break => "keyword `break`", + Self::Continue => "keyword `continue`", + Self::Return => "keyword `return`", + Self::Import => "keyword `import`", + Self::Include => "keyword `include`", + Self::As => "keyword `as`", + Self::Code => "code", + Self::Ident => "identifier", + Self::Bool => "boolean", + Self::Int => "integer", + Self::Float => "float", + Self::Numeric => "numeric value", + Self::Str => "string", + Self::CodeBlock => "code block", + Self::ContentBlock => "content block", + Self::Parenthesized => "group", + Self::Array => "array", + Self::Dict => "dictionary", + Self::Named => "named pair", + Self::Keyed => "keyed pair", + Self::Unary => "unary expression", + Self::Binary => "binary expression", + Self::FieldAccess => "field access", + Self::FuncCall => "function call", + Self::Args => "call arguments", + Self::Spread => "spread", + Self::Closure => "closure", + Self::Params => "closure parameters", + Self::LetBinding => "`let` expression", + Self::SetRule => "`set` expression", + Self::ShowRule => "`show` expression", + Self::Conditional => "`if` expression", + Self::WhileLoop => "while-loop expression", + Self::ForLoop => "for-loop expression", + Self::ModuleImport => "`import` expression", + Self::ImportItems => "import items", + Self::ModuleInclude => "`include` expression", + Self::LoopBreak => "`break` expression", + Self::LoopContinue => "`continue` expression", + Self::FuncReturn => "`return` expression", + Self::Destructuring => "destructuring pattern", + Self::DestructAssignment => "destructuring assignment expression", + Self::LineComment => "line comment", + Self::BlockComment => "block comment", + Self::Error => "syntax error", + Self::Eof => "end of file", + } + } +} diff --git a/crates/typst/src/syntax/lexer.rs b/crates/typst/src/syntax/lexer.rs new file mode 100644 index 00000000..d95b5b7b --- /dev/null +++ b/crates/typst/src/syntax/lexer.rs @@ -0,0 +1,738 @@ +use ecow::{eco_format, EcoString}; +use unicode_ident::{is_xid_continue, is_xid_start}; +use unicode_segmentation::UnicodeSegmentation; +use unscanny::Scanner; + +use super::SyntaxKind; + +/// Splits up a string of source code into tokens. +#[derive(Clone)] +pub(super) struct Lexer<'s> { + /// The underlying scanner. + s: Scanner<'s>, + /// The mode the lexer is in. This determines which kinds of tokens it + /// produces. + mode: LexMode, + /// Whether the last token contained a newline. + newline: bool, + /// An error for the last token. + error: Option<EcoString>, +} + +/// What kind of tokens to emit. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(super) enum LexMode { + /// Text and markup. + Markup, + /// Math atoms, operators, etc. + Math, + /// Keywords, literals and operators. + Code, +} + +impl<'s> Lexer<'s> { + /// Create a new lexer with the given mode and a prefix to offset column + /// calculations. + pub fn new(text: &'s str, mode: LexMode) -> Self { + Self { + s: Scanner::new(text), + mode, + newline: false, + error: None, + } + } + + /// Get the current lexing mode. + pub fn mode(&self) -> LexMode { + self.mode + } + + /// Change the lexing mode. + pub fn set_mode(&mut self, mode: LexMode) { + self.mode = mode; + } + + /// The index in the string at which the last token ends and next token + /// will start. + pub fn cursor(&self) -> usize { + self.s.cursor() + } + + /// Jump to the given index in the string. + pub fn jump(&mut self, index: usize) { + self.s.jump(index); + } + + /// Whether the last token contained a newline. + pub fn newline(&self) -> bool { + self.newline + } + + /// Take out the last error, if any. + pub fn take_error(&mut self) -> Option<EcoString> { + self.error.take() + } +} + +impl Lexer<'_> { + /// Construct a full-positioned syntax error. + fn error(&mut self, message: impl Into<EcoString>) -> SyntaxKind { + self.error = Some(message.into()); + SyntaxKind::Error + } +} + +/// Shared. +impl Lexer<'_> { + pub fn next(&mut self) -> SyntaxKind { + self.newline = false; + self.error = None; + let start = self.s.cursor(); + match self.s.eat() { + Some(c) if c.is_whitespace() => self.whitespace(start, c), + Some('/') if self.s.eat_if('/') => self.line_comment(), + Some('/') if self.s.eat_if('*') => self.block_comment(), + Some('*') if self.s.eat_if('/') => { + self.error("unexpected end of block comment") + } + + Some(c) => match self.mode { + LexMode::Markup => self.markup(start, c), + LexMode::Math => self.math(start, c), + LexMode::Code => self.code(start, c), + }, + + None => SyntaxKind::Eof, + } + } + + fn whitespace(&mut self, start: usize, c: char) -> SyntaxKind { + let more = self.s.eat_while(char::is_whitespace); + let newlines = match c { + ' ' if more.is_empty() => 0, + _ => count_newlines(self.s.from(start)), + }; + + self.newline = newlines > 0; + if self.mode == LexMode::Markup && newlines >= 2 { + SyntaxKind::Parbreak + } else { + SyntaxKind::Space + } + } + + fn line_comment(&mut self) -> SyntaxKind { + self.s.eat_until(is_newline); + SyntaxKind::LineComment + } + + fn block_comment(&mut self) -> SyntaxKind { + let mut state = '_'; + let mut depth = 1; + + // Find the first `*/` that does not correspond to a nested `/*`. + while let Some(c) = self.s.eat() { + state = match (state, c) { + ('*', '/') => { + depth -= 1; + if depth == 0 { + break; + } + '_' + } + ('/', '*') => { + depth += 1; + '_' + } + ('/', '/') => { + self.line_comment(); + '_' + } + _ => c, + } + } + + SyntaxKind::BlockComment + } +} + +/// Markup. +impl Lexer<'_> { + fn markup(&mut self, start: usize, c: char) -> SyntaxKind { + match c { + '\\' => self.backslash(), + '`' => self.raw(), + 'h' if self.s.eat_if("ttp://") => self.link(), + 'h' if self.s.eat_if("ttps://") => self.link(), + '<' if self.s.at(is_id_continue) => self.label(), + '@' => self.ref_marker(), + + '.' if self.s.eat_if("..") => SyntaxKind::Shorthand, + '-' if self.s.eat_if("--") => SyntaxKind::Shorthand, + '-' if self.s.eat_if('-') => SyntaxKind::Shorthand, + '-' if self.s.eat_if('?') => SyntaxKind::Shorthand, + '*' if !self.in_word() => SyntaxKind::Star, + '_' if !self.in_word() => SyntaxKind::Underscore, + + '#' => SyntaxKind::Hashtag, + '[' => SyntaxKind::LeftBracket, + ']' => SyntaxKind::RightBracket, + '\'' => SyntaxKind::SmartQuote, + '"' => SyntaxKind::SmartQuote, + '$' => SyntaxKind::Dollar, + '~' => SyntaxKind::Shorthand, + ':' => SyntaxKind::Colon, + '=' => { + self.s.eat_while('='); + if self.space_or_end() { + SyntaxKind::HeadingMarker + } else { + self.text() + } + } + '-' if self.space_or_end() => SyntaxKind::ListMarker, + '+' if self.space_or_end() => SyntaxKind::EnumMarker, + '/' if self.space_or_end() => SyntaxKind::TermMarker, + '0'..='9' => self.numbering(start), + + _ => self.text(), + } + } + + fn backslash(&mut self) -> SyntaxKind { + if self.s.eat_if("u{") { + let hex = self.s.eat_while(char::is_ascii_alphanumeric); + if !self.s.eat_if('}') { + return self.error("unclosed Unicode escape sequence"); + } + + if u32::from_str_radix(hex, 16) + .ok() + .and_then(std::char::from_u32) + .is_none() + { + return self.error(eco_format!("invalid Unicode codepoint: {}", hex)); + } + + return SyntaxKind::Escape; + } + + if self.s.done() || self.s.at(char::is_whitespace) { + SyntaxKind::Linebreak + } else { + self.s.eat(); + SyntaxKind::Escape + } + } + + fn raw(&mut self) -> SyntaxKind { + let mut backticks = 1; + while self.s.eat_if('`') { + backticks += 1; + } + + if backticks == 2 { + return SyntaxKind::Raw; + } + + let mut found = 0; + while found < backticks { + match self.s.eat() { + Some('`') => found += 1, + Some(_) => found = 0, + None => break, + } + } + + if found != backticks { + return self.error("unclosed raw text"); + } + + SyntaxKind::Raw + } + + fn link(&mut self) -> SyntaxKind { + let mut brackets = Vec::new(); + + #[rustfmt::skip] + self.s.eat_while(|c: char| { + match c { + | '0' ..= '9' + | 'a' ..= 'z' + | 'A' ..= 'Z' + | '!' | '#' | '$' | '%' | '&' | '*' | '+' + | ',' | '-' | '.' | '/' | ':' | ';' | '=' + | '?' | '@' | '_' | '~' | '\'' => true, + '[' => { + brackets.push(SyntaxKind::LeftBracket); + true + } + '(' => { + brackets.push(SyntaxKind::LeftParen); + true + } + ']' => brackets.pop() == Some(SyntaxKind::LeftBracket), + ')' => brackets.pop() == Some(SyntaxKind::LeftParen), + _ => false, + } + }); + + if !brackets.is_empty() { + return self.error( + "automatic links cannot contain unbalanced brackets, \ + use the `link` function instead", + ); + } + + // Don't include the trailing characters likely to be part of text. + while matches!(self.s.scout(-1), Some('!' | ',' | '.' | ':' | ';' | '?' | '\'')) { + self.s.uneat(); + } + + SyntaxKind::Link + } + + fn numbering(&mut self, start: usize) -> SyntaxKind { + self.s.eat_while(char::is_ascii_digit); + + let read = self.s.from(start); + if self.s.eat_if('.') && self.space_or_end() && read.parse::<usize>().is_ok() { + return SyntaxKind::EnumMarker; + } + + self.text() + } + + fn ref_marker(&mut self) -> SyntaxKind { + self.s.eat_while(|c| is_id_continue(c) || matches!(c, ':' | '.')); + + // Don't include the trailing characters likely to be part of text. + while matches!(self.s.scout(-1), Some('.' | ':')) { + self.s.uneat(); + } + + SyntaxKind::RefMarker + } + + fn label(&mut self) -> SyntaxKind { + let label = self.s.eat_while(|c| is_id_continue(c) || matches!(c, ':' | '.')); + if label.is_empty() { + return self.error("label cannot be empty"); + } + + if !self.s.eat_if('>') { + return self.error("unclosed label"); + } + + SyntaxKind::Label + } + + fn text(&mut self) -> SyntaxKind { + macro_rules! table { + ($(|$c:literal)*) => { + static TABLE: [bool; 128] = { + let mut t = [false; 128]; + $(t[$c as usize] = true;)* + t + }; + }; + } + + table! { + | ' ' | '\t' | '\n' | '\x0b' | '\x0c' | '\r' | '\\' | '/' + | '[' | ']' | '{' | '}' | '~' | '-' | '.' | '\'' | '"' + | '*' | '_' | ':' | 'h' | '`' | '$' | '<' | '>' | '@' | '#' + }; + + loop { + self.s.eat_until(|c: char| { + TABLE.get(c as usize).copied().unwrap_or_else(|| c.is_whitespace()) + }); + + // Continue with the same text node if the thing would become text + // anyway. + let mut s = self.s; + match s.eat() { + Some(' ') if s.at(char::is_alphanumeric) => {} + Some('/') if !s.at(['/', '*']) => {} + Some('-') if !s.at(['-', '?']) => {} + Some('.') if !s.at("..") => {} + Some('h') if !s.at("ttp://") && !s.at("ttps://") => {} + Some('@') if !s.at(is_id_start) => {} + _ => break, + } + + self.s = s; + } + + SyntaxKind::Text + } + + fn in_word(&self) -> bool { + let alphanum = |c: Option<char>| c.map_or(false, |c| c.is_alphanumeric()); + let prev = self.s.scout(-2); + let next = self.s.peek(); + alphanum(prev) && alphanum(next) + } + + fn space_or_end(&self) -> bool { + self.s.done() || self.s.at(char::is_whitespace) + } +} + +/// Math. +impl Lexer<'_> { + fn math(&mut self, start: usize, c: char) -> SyntaxKind { + match c { + '\\' => self.backslash(), + '"' => self.string(), + + '-' if self.s.eat_if(">>") => SyntaxKind::Shorthand, + '-' if self.s.eat_if('>') => SyntaxKind::Shorthand, + '-' if self.s.eat_if("->") => SyntaxKind::Shorthand, + ':' if self.s.eat_if('=') => SyntaxKind::Shorthand, + ':' if self.s.eat_if(":=") => SyntaxKind::Shorthand, + '!' if self.s.eat_if('=') => SyntaxKind::Shorthand, + '.' if self.s.eat_if("..") => SyntaxKind::Shorthand, + '[' if self.s.eat_if('|') => SyntaxKind::Shorthand, + '<' if self.s.eat_if("==>") => SyntaxKind::Shorthand, + '<' if self.s.eat_if("-->") => SyntaxKind::Shorthand, + '<' if self.s.eat_if("--") => SyntaxKind::Shorthand, + '<' if self.s.eat_if("-<") => SyntaxKind::Shorthand, + '<' if self.s.eat_if("->") => SyntaxKind::Shorthand, + '<' if self.s.eat_if("<-") => SyntaxKind::Shorthand, + '<' if self.s.eat_if("<<") => SyntaxKind::Shorthand, + '<' if self.s.eat_if("=>") => SyntaxKind::Shorthand, + '<' if self.s.eat_if("==") => SyntaxKind::Shorthand, + '<' if self.s.eat_if("~~") => SyntaxKind::Shorthand, + '<' if self.s.eat_if('=') => SyntaxKind::Shorthand, + '<' if self.s.eat_if('<') => SyntaxKind::Shorthand, + '<' if self.s.eat_if('-') => SyntaxKind::Shorthand, + '<' if self.s.eat_if('~') => SyntaxKind::Shorthand, + '>' if self.s.eat_if("->") => SyntaxKind::Shorthand, + '>' if self.s.eat_if(">>") => SyntaxKind::Shorthand, + '=' if self.s.eat_if("=>") => SyntaxKind::Shorthand, + '=' if self.s.eat_if('>') => SyntaxKind::Shorthand, + '=' if self.s.eat_if(':') => SyntaxKind::Shorthand, + '>' if self.s.eat_if('=') => SyntaxKind::Shorthand, + '>' if self.s.eat_if('>') => SyntaxKind::Shorthand, + '|' if self.s.eat_if("->") => SyntaxKind::Shorthand, + '|' if self.s.eat_if("=>") => SyntaxKind::Shorthand, + '|' if self.s.eat_if(']') => SyntaxKind::Shorthand, + '|' if self.s.eat_if('|') => SyntaxKind::Shorthand, + '~' if self.s.eat_if("~>") => SyntaxKind::Shorthand, + '~' if self.s.eat_if('>') => SyntaxKind::Shorthand, + '*' | '\'' | '-' => SyntaxKind::Shorthand, + + '#' => SyntaxKind::Hashtag, + '_' => SyntaxKind::Underscore, + '$' => SyntaxKind::Dollar, + '/' => SyntaxKind::Slash, + '^' => SyntaxKind::Hat, + '&' => SyntaxKind::MathAlignPoint, + '√' | '∛' | '∜' => SyntaxKind::Root, + + // Identifiers. + c if is_math_id_start(c) && self.s.at(is_math_id_continue) => { + self.s.eat_while(is_math_id_continue); + SyntaxKind::MathIdent + } + + // Other math atoms. + _ => self.math_text(start, c), + } + } + + fn math_text(&mut self, start: usize, c: char) -> SyntaxKind { + // Keep numbers and grapheme clusters together. + if c.is_numeric() { + self.s.eat_while(char::is_numeric); + let mut s = self.s; + if s.eat_if('.') && !s.eat_while(char::is_numeric).is_empty() { + self.s = s; + } + } else { + let len = self + .s + .get(start..self.s.string().len()) + .graphemes(true) + .next() + .map_or(0, str::len); + self.s.jump(start + len); + } + SyntaxKind::Text + } +} + +/// Code. +impl Lexer<'_> { + fn code(&mut self, start: usize, c: char) -> SyntaxKind { + match c { + '`' => self.raw(), + '<' if self.s.at(is_id_continue) => self.label(), + '0'..='9' => self.number(start, c), + '.' if self.s.at(char::is_ascii_digit) => self.number(start, c), + '"' => self.string(), + + '=' if self.s.eat_if('=') => SyntaxKind::EqEq, + '!' if self.s.eat_if('=') => SyntaxKind::ExclEq, + '<' if self.s.eat_if('=') => SyntaxKind::LtEq, + '>' if self.s.eat_if('=') => SyntaxKind::GtEq, + '+' if self.s.eat_if('=') => SyntaxKind::PlusEq, + '-' if self.s.eat_if('=') => SyntaxKind::HyphEq, + '*' if self.s.eat_if('=') => SyntaxKind::StarEq, + '/' if self.s.eat_if('=') => SyntaxKind::SlashEq, + '.' if self.s.eat_if('.') => SyntaxKind::Dots, + '=' if self.s.eat_if('>') => SyntaxKind::Arrow, + + '{' => SyntaxKind::LeftBrace, + '}' => SyntaxKind::RightBrace, + '[' => SyntaxKind::LeftBracket, + ']' => SyntaxKind::RightBracket, + '(' => SyntaxKind::LeftParen, + ')' => SyntaxKind::RightParen, + '$' => SyntaxKind::Dollar, + ',' => SyntaxKind::Comma, + ';' => SyntaxKind::Semicolon, + ':' => SyntaxKind::Colon, + '.' => SyntaxKind::Dot, + '+' => SyntaxKind::Plus, + '-' => SyntaxKind::Minus, + '*' => SyntaxKind::Star, + '/' => SyntaxKind::Slash, + '=' => SyntaxKind::Eq, + '<' => SyntaxKind::Lt, + '>' => SyntaxKind::Gt, + + c if is_id_start(c) => self.ident(start), + + c => self.error(eco_format!("the character `{c}` is not valid in code")), + } + } + + fn ident(&mut self, start: usize) -> SyntaxKind { + self.s.eat_while(is_id_continue); + let ident = self.s.from(start); + + let prev = self.s.get(0..start); + if !prev.ends_with(['.', '@']) || prev.ends_with("..") { + if let Some(keyword) = keyword(ident) { + return keyword; + } + } + + if ident == "_" { + SyntaxKind::Underscore + } else { + SyntaxKind::Ident + } + } + + fn number(&mut self, mut start: usize, c: char) -> SyntaxKind { + // Handle alternative integer bases. + let mut base = 10; + if c == '0' { + if self.s.eat_if('b') { + base = 2; + } else if self.s.eat_if('o') { + base = 8; + } else if self.s.eat_if('x') { + base = 16; + } + if base != 10 { + start = self.s.cursor(); + } + } + + // Read the first part (integer or fractional depending on `first`). + self.s.eat_while(if base == 16 { + char::is_ascii_alphanumeric + } else { + char::is_ascii_digit + }); + + // Read the fractional part if not already done. + // Make sure not to confuse a range for the decimal separator. + if c != '.' + && !self.s.at("..") + && !self.s.scout(1).map_or(false, is_id_start) + && self.s.eat_if('.') + && base == 10 + { + self.s.eat_while(char::is_ascii_digit); + } + + // Read the exponent. + if !self.s.at("em") && self.s.eat_if(['e', 'E']) && base == 10 { + self.s.eat_if(['+', '-']); + self.s.eat_while(char::is_ascii_digit); + } + + // Read the suffix. + let suffix_start = self.s.cursor(); + if !self.s.eat_if('%') { + self.s.eat_while(char::is_ascii_alphanumeric); + } + + let number = self.s.get(start..suffix_start); + let suffix = self.s.from(suffix_start); + + let kind = if i64::from_str_radix(number, base).is_ok() { + SyntaxKind::Int + } else if base == 10 && number.parse::<f64>().is_ok() { + SyntaxKind::Float + } else { + return self.error(match base { + 2 => eco_format!("invalid binary number: 0b{}", number), + 8 => eco_format!("invalid octal number: 0o{}", number), + 16 => eco_format!("invalid hexadecimal number: 0x{}", number), + _ => eco_format!("invalid number: {}", number), + }); + }; + + if suffix.is_empty() { + return kind; + } + + if !matches!( + suffix, + "pt" | "mm" | "cm" | "in" | "deg" | "rad" | "em" | "fr" | "%" + ) { + return self.error(eco_format!("invalid number suffix: {}", suffix)); + } + + SyntaxKind::Numeric + } + + fn string(&mut self) -> SyntaxKind { + let mut escaped = false; + self.s.eat_until(|c| { + let stop = c == '"' && !escaped; + escaped = c == '\\' && !escaped; + stop + }); + + if !self.s.eat_if('"') { + return self.error("unclosed string"); + } + + SyntaxKind::Str + } +} + +/// Try to parse an identifier into a keyword. +fn keyword(ident: &str) -> Option<SyntaxKind> { + Some(match ident { + "none" => SyntaxKind::None, + "auto" => SyntaxKind::Auto, + "true" => SyntaxKind::Bool, + "false" => SyntaxKind::Bool, + "not" => SyntaxKind::Not, + "and" => SyntaxKind::And, + "or" => SyntaxKind::Or, + "let" => SyntaxKind::Let, + "set" => SyntaxKind::Set, + "show" => SyntaxKind::Show, + "if" => SyntaxKind::If, + "else" => SyntaxKind::Else, + "for" => SyntaxKind::For, + "in" => SyntaxKind::In, + "while" => SyntaxKind::While, + "break" => SyntaxKind::Break, + "continue" => SyntaxKind::Continue, + "return" => SyntaxKind::Return, + "import" => SyntaxKind::Import, + "include" => SyntaxKind::Include, + "as" => SyntaxKind::As, + _ => return None, + }) +} + +/// Whether this character denotes a newline. +#[inline] +pub fn is_newline(character: char) -> bool { + matches!( + character, + // Line Feed, Vertical Tab, Form Feed, Carriage Return. + '\n' | '\x0B' | '\x0C' | '\r' | + // Next Line, Line Separator, Paragraph Separator. + '\u{0085}' | '\u{2028}' | '\u{2029}' + ) +} + +/// Split text at newlines. +pub(super) fn split_newlines(text: &str) -> Vec<&str> { + let mut s = Scanner::new(text); + let mut lines = Vec::new(); + let mut start = 0; + let mut end = 0; + + while let Some(c) = s.eat() { + if is_newline(c) { + if c == '\r' { + s.eat_if('\n'); + } + + lines.push(&text[start..end]); + start = s.cursor(); + } + end = s.cursor(); + } + + lines.push(&text[start..]); + lines +} + +/// Count the number of newlines in text. +fn count_newlines(text: &str) -> usize { + let mut newlines = 0; + let mut s = Scanner::new(text); + while let Some(c) = s.eat() { + if is_newline(c) { + if c == '\r' { + s.eat_if('\n'); + } + newlines += 1; + } + } + newlines +} + +/// Whether a string is a valid Typst identifier. +/// +/// In addition to what is specified in the [Unicode Standard][uax31], we allow: +/// - `_` as a starting character, +/// - `_` and `-` as continuing characters. +/// +/// [uax31]: http://www.unicode.org/reports/tr31/ +#[inline] +pub fn is_ident(string: &str) -> bool { + let mut chars = string.chars(); + chars + .next() + .map_or(false, |c| is_id_start(c) && chars.all(is_id_continue)) +} + +/// Whether a character can start an identifier. +#[inline] +pub(crate) fn is_id_start(c: char) -> bool { + is_xid_start(c) || c == '_' +} + +/// Whether a character can continue an identifier. +#[inline] +pub(crate) fn is_id_continue(c: char) -> bool { + is_xid_continue(c) || c == '_' || c == '-' +} + +/// Whether a character can start an identifier in math. +#[inline] +fn is_math_id_start(c: char) -> bool { + is_xid_start(c) +} + +/// Whether a character can continue an identifier in math. +#[inline] +fn is_math_id_continue(c: char) -> bool { + is_xid_continue(c) && c != '_' +} diff --git a/crates/typst/src/syntax/mod.rs b/crates/typst/src/syntax/mod.rs new file mode 100644 index 00000000..1ce1e4c0 --- /dev/null +++ b/crates/typst/src/syntax/mod.rs @@ -0,0 +1,23 @@ +//! Syntax definition, parsing, and highlighting. + +pub mod ast; + +mod kind; +mod lexer; +mod node; +mod parser; +mod reparser; +mod source; +mod span; + +pub use self::kind::SyntaxKind; +pub use self::lexer::{is_ident, is_newline}; +pub use self::node::{LinkedChildren, LinkedNode, SyntaxNode}; +pub use self::parser::{parse, parse_code}; +pub use self::source::Source; +pub use self::span::{Span, Spanned}; + +pub(crate) use self::lexer::{is_id_continue, is_id_start}; + +use self::lexer::{split_newlines, LexMode, Lexer}; +use self::parser::{reparse_block, reparse_markup}; diff --git a/crates/typst/src/syntax/node.rs b/crates/typst/src/syntax/node.rs new file mode 100644 index 00000000..6a66416d --- /dev/null +++ b/crates/typst/src/syntax/node.rs @@ -0,0 +1,889 @@ +use std::fmt::{self, Debug, Display, Formatter}; +use std::ops::{Deref, Range}; +use std::rc::Rc; +use std::sync::Arc; + +use ecow::EcoString; + +use super::ast::AstNode; +use super::{Span, SyntaxKind}; +use crate::diag::SourceError; +use crate::file::FileId; + +/// A node in the untyped syntax tree. +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct SyntaxNode(Repr); + +/// The three internal representations. +#[derive(Clone, Eq, PartialEq, Hash)] +enum Repr { + /// A leaf node. + Leaf(LeafNode), + /// A reference-counted inner node. + Inner(Arc<InnerNode>), + /// An error node. + Error(Arc<ErrorNode>), +} + +impl SyntaxNode { + /// Create a new leaf node. + pub fn leaf(kind: SyntaxKind, text: impl Into<EcoString>) -> Self { + Self(Repr::Leaf(LeafNode::new(kind, text))) + } + + /// Create a new inner node with children. + pub fn inner(kind: SyntaxKind, children: Vec<SyntaxNode>) -> Self { + Self(Repr::Inner(Arc::new(InnerNode::new(kind, children)))) + } + + /// Create a new error node. + pub fn error(message: impl Into<EcoString>, text: impl Into<EcoString>) -> Self { + Self(Repr::Error(Arc::new(ErrorNode::new(message, text)))) + } + + /// The type of the node. + pub fn kind(&self) -> SyntaxKind { + match &self.0 { + Repr::Leaf(leaf) => leaf.kind, + Repr::Inner(inner) => inner.kind, + Repr::Error(_) => SyntaxKind::Error, + } + } + + /// Return `true` if the length is 0. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// The byte length of the node in the source text. + pub fn len(&self) -> usize { + match &self.0 { + Repr::Leaf(leaf) => leaf.len(), + Repr::Inner(inner) => inner.len, + Repr::Error(error) => error.len(), + } + } + + /// The span of the node. + pub fn span(&self) -> Span { + match &self.0 { + Repr::Leaf(leaf) => leaf.span, + Repr::Inner(inner) => inner.span, + Repr::Error(error) => error.span, + } + } + + /// The text of the node if it is a leaf node. + /// + /// Returns the empty string if this is an inner node. + pub fn text(&self) -> &EcoString { + static EMPTY: EcoString = EcoString::new(); + match &self.0 { + Repr::Leaf(leaf) => &leaf.text, + Repr::Error(error) => &error.text, + Repr::Inner(_) => &EMPTY, + } + } + + /// Extract the text from the node. + /// + /// Builds the string if this is an inner node. + pub fn into_text(self) -> EcoString { + match self.0 { + Repr::Leaf(leaf) => leaf.text, + Repr::Error(error) => error.text.clone(), + Repr::Inner(node) => { + node.children.iter().cloned().map(Self::into_text).collect() + } + } + } + + /// The node's children. + pub fn children(&self) -> std::slice::Iter<'_, SyntaxNode> { + match &self.0 { + Repr::Leaf(_) | Repr::Error(_) => [].iter(), + Repr::Inner(inner) => inner.children.iter(), + } + } + + /// Whether the node can be cast to the given AST node. + pub fn is<T: AstNode>(&self) -> bool { + self.cast::<T>().is_some() + } + + /// Try to convert the node to a typed AST node. + pub fn cast<T: AstNode>(&self) -> Option<T> { + T::from_untyped(self) + } + + /// Cast the first child that can cast to the AST type `T`. + pub fn cast_first_match<T: AstNode>(&self) -> Option<T> { + self.children().find_map(Self::cast) + } + + /// Cast the last child that can cast to the AST type `T`. + pub fn cast_last_match<T: AstNode>(&self) -> Option<T> { + self.children().rev().find_map(Self::cast) + } + + /// Whether the node or its children contain an error. + pub fn erroneous(&self) -> bool { + match &self.0 { + Repr::Leaf(_) => false, + Repr::Inner(node) => node.erroneous, + Repr::Error(_) => true, + } + } + + /// The error messages for this node and its descendants. + pub fn errors(&self) -> Vec<SourceError> { + if !self.erroneous() { + return vec![]; + } + + if let Repr::Error(error) = &self.0 { + vec![SourceError::new(error.span, error.message.clone())] + } else { + self.children() + .filter(|node| node.erroneous()) + .flat_map(|node| node.errors()) + .collect() + } + } + + /// Set a synthetic span for the node and all its descendants. + pub fn synthesize(&mut self, span: Span) { + match &mut self.0 { + Repr::Leaf(leaf) => leaf.span = span, + Repr::Inner(inner) => Arc::make_mut(inner).synthesize(span), + Repr::Error(error) => Arc::make_mut(error).span = span, + } + } +} + +impl SyntaxNode { + /// Mark this node as erroneous. + pub(super) fn make_erroneous(&mut self) { + if let Repr::Inner(inner) = &mut self.0 { + Arc::make_mut(inner).erroneous = true; + } + } + + /// Convert the child to another kind. + #[track_caller] + pub(super) fn convert_to_kind(&mut self, kind: SyntaxKind) { + debug_assert!(!kind.is_error()); + match &mut self.0 { + Repr::Leaf(leaf) => leaf.kind = kind, + Repr::Inner(inner) => Arc::make_mut(inner).kind = kind, + Repr::Error(_) => panic!("cannot convert error"), + } + } + + /// Convert the child to an error. + pub(super) fn convert_to_error(&mut self, message: impl Into<EcoString>) { + let text = std::mem::take(self).into_text(); + *self = SyntaxNode::error(message, text); + } + + /// Assign spans to each node. + #[tracing::instrument(skip_all)] + pub(super) fn numberize( + &mut self, + id: FileId, + within: Range<u64>, + ) -> NumberingResult { + if within.start >= within.end { + return Err(Unnumberable); + } + + let mid = Span::new(id, (within.start + within.end) / 2); + match &mut self.0 { + Repr::Leaf(leaf) => leaf.span = mid, + Repr::Inner(inner) => Arc::make_mut(inner).numberize(id, None, within)?, + Repr::Error(error) => Arc::make_mut(error).span = mid, + } + + Ok(()) + } + + /// Whether this is a leaf node. + pub(super) fn is_leaf(&self) -> bool { + matches!(self.0, Repr::Leaf(_)) + } + + /// The number of descendants, including the node itself. + pub(super) fn descendants(&self) -> usize { + match &self.0 { + Repr::Leaf(_) | Repr::Error(_) => 1, + Repr::Inner(inner) => inner.descendants, + } + } + + /// The node's children, mutably. + pub(super) fn children_mut(&mut self) -> &mut [SyntaxNode] { + match &mut self.0 { + Repr::Leaf(_) | Repr::Error(_) => &mut [], + Repr::Inner(inner) => &mut Arc::make_mut(inner).children, + } + } + + /// Replaces a range of children with a replacement. + /// + /// May have mutated the children if it returns `Err(_)`. + pub(super) fn replace_children( + &mut self, + range: Range<usize>, + replacement: Vec<SyntaxNode>, + ) -> NumberingResult { + if let Repr::Inner(inner) = &mut self.0 { + Arc::make_mut(inner).replace_children(range, replacement)?; + } + Ok(()) + } + + /// Update this node after changes were made to one of its children. + pub(super) fn update_parent( + &mut self, + prev_len: usize, + new_len: usize, + prev_descendants: usize, + new_descendants: usize, + ) { + if let Repr::Inner(inner) = &mut self.0 { + Arc::make_mut(inner).update_parent( + prev_len, + new_len, + prev_descendants, + new_descendants, + ); + } + } + + /// The upper bound of assigned numbers in this subtree. + pub(super) fn upper(&self) -> u64 { + match &self.0 { + Repr::Inner(inner) => inner.upper, + Repr::Leaf(leaf) => leaf.span.number() + 1, + Repr::Error(error) => error.span.number() + 1, + } + } +} + +impl Debug for SyntaxNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match &self.0 { + Repr::Inner(node) => node.fmt(f), + Repr::Leaf(node) => node.fmt(f), + Repr::Error(node) => node.fmt(f), + } + } +} + +impl Default for SyntaxNode { + fn default() -> Self { + Self::error("", "") + } +} + +/// A leaf node in the untyped syntax tree. +#[derive(Clone, Eq, PartialEq, Hash)] +struct LeafNode { + /// What kind of node this is (each kind would have its own struct in a + /// strongly typed AST). + kind: SyntaxKind, + /// The source text of the node. + text: EcoString, + /// The node's span. + span: Span, +} + +impl LeafNode { + /// Create a new leaf node. + #[track_caller] + fn new(kind: SyntaxKind, text: impl Into<EcoString>) -> Self { + debug_assert!(!kind.is_error()); + Self { kind, text: text.into(), span: Span::detached() } + } + + /// The byte length of the node in the source text. + fn len(&self) -> usize { + self.text.len() + } +} + +impl Debug for LeafNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{:?}: {:?}", self.kind, self.text) + } +} + +/// An inner node in the untyped syntax tree. +#[derive(Clone, Eq, PartialEq, Hash)] +struct InnerNode { + /// What kind of node this is (each kind would have its own struct in a + /// strongly typed AST). + kind: SyntaxKind, + /// The byte length of the node in the source. + len: usize, + /// The node's span. + span: Span, + /// The number of nodes in the whole subtree, including this node. + descendants: usize, + /// Whether this node or any of its children are erroneous. + erroneous: bool, + /// The upper bound of this node's numbering range. + upper: u64, + /// This node's children, losslessly make up this node. + children: Vec<SyntaxNode>, +} + +impl InnerNode { + /// Create a new inner node with the given kind and children. + #[track_caller] + fn new(kind: SyntaxKind, children: Vec<SyntaxNode>) -> Self { + debug_assert!(!kind.is_error()); + + let mut len = 0; + let mut descendants = 1; + let mut erroneous = false; + + for child in &children { + len += child.len(); + descendants += child.descendants(); + erroneous |= child.erroneous(); + } + + Self { + kind, + len, + span: Span::detached(), + descendants, + erroneous, + upper: 0, + children, + } + } + + /// Set a synthetic span for the node and all its descendants. + fn synthesize(&mut self, span: Span) { + self.span = span; + self.upper = span.number(); + for child in &mut self.children { + child.synthesize(span); + } + } + + /// Assign span numbers `within` an interval to this node's subtree or just + /// a `range` of its children. + fn numberize( + &mut self, + id: FileId, + range: Option<Range<usize>>, + within: Range<u64>, + ) -> NumberingResult { + // Determine how many nodes we will number. + let descendants = match &range { + Some(range) if range.is_empty() => return Ok(()), + Some(range) => self.children[range.clone()] + .iter() + .map(SyntaxNode::descendants) + .sum::<usize>(), + None => self.descendants, + }; + + // Determine the distance between two neighbouring assigned numbers. If + // possible, we try to fit all numbers into the left half of `within` + // so that there is space for future insertions. + let space = within.end - within.start; + let mut stride = space / (2 * descendants as u64); + if stride == 0 { + stride = space / self.descendants as u64; + if stride == 0 { + return Err(Unnumberable); + } + } + + // Number the node itself. + let mut start = within.start; + if range.is_none() { + let end = start + stride; + self.span = Span::new(id, (start + end) / 2); + self.upper = within.end; + start = end; + } + + // Number the children. + let len = self.children.len(); + for child in &mut self.children[range.unwrap_or(0..len)] { + let end = start + child.descendants() as u64 * stride; + child.numberize(id, start..end)?; + start = end; + } + + Ok(()) + } + + /// Replaces a range of children with a replacement. + /// + /// May have mutated the children if it returns `Err(_)`. + fn replace_children( + &mut self, + mut range: Range<usize>, + replacement: Vec<SyntaxNode>, + ) -> NumberingResult { + let superseded = &self.children[range.clone()]; + + // Compute the new byte length. + self.len = self.len + replacement.iter().map(SyntaxNode::len).sum::<usize>() + - superseded.iter().map(SyntaxNode::len).sum::<usize>(); + + // Compute the new number of descendants. + self.descendants = self.descendants + + replacement.iter().map(SyntaxNode::descendants).sum::<usize>() + - superseded.iter().map(SyntaxNode::descendants).sum::<usize>(); + + // Determine whether we're still erroneous after the replacement. That's + // the case if + // - any of the new nodes is erroneous, + // - or if we were erroneous before due to a non-superseded node. + self.erroneous = replacement.iter().any(SyntaxNode::erroneous) + || (self.erroneous + && (self.children[..range.start].iter().any(SyntaxNode::erroneous)) + || self.children[range.end..].iter().any(SyntaxNode::erroneous)); + + // Perform the replacement. + let replacement_count = replacement.len(); + self.children.splice(range.clone(), replacement); + range.end = range.start + replacement_count; + + // Renumber the new children. Retries until it works, taking + // exponentially more children into account. + let mut left = 0; + let mut right = 0; + let max_left = range.start; + let max_right = self.children.len() - range.end; + loop { + let renumber = range.start - left..range.end + right; + + // The minimum assignable number is either + // - the upper bound of the node right before the to-be-renumbered + // children, + // - or this inner node's span number plus one if renumbering starts + // at the first child. + let start_number = renumber + .start + .checked_sub(1) + .and_then(|i| self.children.get(i)) + .map_or(self.span.number() + 1, |child| child.upper()); + + // The upper bound for renumbering is either + // - the span number of the first child after the to-be-renumbered + // children, + // - or this node's upper bound if renumbering ends behind the last + // child. + let end_number = self + .children + .get(renumber.end) + .map_or(self.upper, |next| next.span().number()); + + // Try to renumber. + let within = start_number..end_number; + let id = self.span.id(); + if self.numberize(id, Some(renumber), within).is_ok() { + return Ok(()); + } + + // If it didn't even work with all children, we give up. + if left == max_left && right == max_right { + return Err(Unnumberable); + } + + // Exponential expansion to both sides. + left = (left + 1).next_power_of_two().min(max_left); + right = (right + 1).next_power_of_two().min(max_right); + } + } + + /// Update this node after changes were made to one of its children. + fn update_parent( + &mut self, + prev_len: usize, + new_len: usize, + prev_descendants: usize, + new_descendants: usize, + ) { + self.len = self.len + new_len - prev_len; + self.descendants = self.descendants + new_descendants - prev_descendants; + self.erroneous = self.children.iter().any(SyntaxNode::erroneous); + } +} + +impl Debug for InnerNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{:?}: {}", self.kind, self.len)?; + if !self.children.is_empty() { + f.write_str(" ")?; + f.debug_list().entries(&self.children).finish()?; + } + Ok(()) + } +} + +/// An error node in the untyped syntax tree. +#[derive(Clone, Eq, PartialEq, Hash)] +struct ErrorNode { + /// The error message. + message: EcoString, + /// The source text of the node. + text: EcoString, + /// The node's span. + span: Span, +} + +impl ErrorNode { + /// Create new error node. + fn new(message: impl Into<EcoString>, text: impl Into<EcoString>) -> Self { + Self { + message: message.into(), + text: text.into(), + span: Span::detached(), + } + } + + /// The byte length of the node in the source text. + fn len(&self) -> usize { + self.text.len() + } +} + +impl Debug for ErrorNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Error: {:?} ({})", self.text, self.message) + } +} + +/// A syntax node in a context. +/// +/// Knows its exact offset in the file and provides access to its +/// children, parent and siblings. +/// +/// **Note that all sibling and leaf accessors skip over trivia!** +#[derive(Clone)] +pub struct LinkedNode<'a> { + node: &'a SyntaxNode, + parent: Option<Rc<Self>>, + index: usize, + offset: usize, +} + +impl<'a> LinkedNode<'a> { + /// Start a new traversal at a root node. + pub fn new(root: &'a SyntaxNode) -> Self { + Self { node: root, parent: None, index: 0, offset: 0 } + } + + /// Get the contained syntax node. + pub fn get(&self) -> &'a SyntaxNode { + self.node + } + + /// The index of this node in its parent's children list. + pub fn index(&self) -> usize { + self.index + } + + /// The absolute byte offset of this node in the source file. + pub fn offset(&self) -> usize { + self.offset + } + + /// The byte range of this node in the source file. + pub fn range(&self) -> Range<usize> { + self.offset..self.offset + self.node.len() + } + + /// An iterator over this node's children. + pub fn children(&self) -> LinkedChildren<'a> { + LinkedChildren { + parent: Rc::new(self.clone()), + iter: self.node.children().enumerate(), + front: self.offset, + back: self.offset + self.len(), + } + } + + /// Find a descendant with the given span. + pub fn find(&self, span: Span) -> Option<LinkedNode<'a>> { + if self.span() == span { + return Some(self.clone()); + } + + if let Repr::Inner(inner) = &self.0 { + // The parent of a subtree has a smaller span number than all of its + // descendants. Therefore, we can bail out early if the target span's + // number is smaller than our number. + if span.number() < inner.span.number() { + return None; + } + + let mut children = self.children().peekable(); + while let Some(child) = children.next() { + // Every node in this child's subtree has a smaller span number than + // the next sibling. Therefore we only need to recurse if the next + // sibling's span number is larger than the target span's number. + if children + .peek() + .map_or(true, |next| next.span().number() > span.number()) + { + if let Some(found) = child.find(span) { + return Some(found); + } + } + } + } + + None + } +} + +/// Access to parents and siblings. +impl<'a> LinkedNode<'a> { + /// Get this node's parent. + pub fn parent(&self) -> Option<&Self> { + self.parent.as_deref() + } + + /// Get the first previous non-trivia sibling node. + pub fn prev_sibling(&self) -> Option<Self> { + let parent = self.parent()?; + let index = self.index.checked_sub(1)?; + let node = parent.node.children().nth(index)?; + let offset = self.offset - node.len(); + let prev = Self { node, parent: self.parent.clone(), index, offset }; + if prev.kind().is_trivia() { + prev.prev_sibling() + } else { + Some(prev) + } + } + + /// Get the next non-trivia sibling node. + pub fn next_sibling(&self) -> Option<Self> { + let parent = self.parent()?; + let index = self.index.checked_add(1)?; + let node = parent.node.children().nth(index)?; + let offset = self.offset + self.node.len(); + let next = Self { node, parent: self.parent.clone(), index, offset }; + if next.kind().is_trivia() { + next.next_sibling() + } else { + Some(next) + } + } + + /// Get the kind of this node's parent. + pub fn parent_kind(&self) -> Option<SyntaxKind> { + Some(self.parent()?.node.kind()) + } + + /// Get the kind of this node's first previous non-trivia sibling. + pub fn prev_sibling_kind(&self) -> Option<SyntaxKind> { + Some(self.prev_sibling()?.node.kind()) + } + + /// Get the kind of this node's next non-trivia sibling. + pub fn next_sibling_kind(&self) -> Option<SyntaxKind> { + Some(self.next_sibling()?.node.kind()) + } +} + +/// Access to leafs. +impl<'a> LinkedNode<'a> { + /// Get the rightmost non-trivia leaf before this node. + pub fn prev_leaf(&self) -> Option<Self> { + let mut node = self.clone(); + while let Some(prev) = node.prev_sibling() { + if let Some(leaf) = prev.rightmost_leaf() { + return Some(leaf); + } + node = prev; + } + self.parent()?.prev_leaf() + } + + /// Find the leftmost contained non-trivia leaf. + pub fn leftmost_leaf(&self) -> Option<Self> { + if self.is_leaf() && !self.kind().is_trivia() && !self.kind().is_error() { + return Some(self.clone()); + } + + for child in self.children() { + if let Some(leaf) = child.leftmost_leaf() { + return Some(leaf); + } + } + + None + } + + /// Get the leaf at the specified byte offset. + pub fn leaf_at(&self, cursor: usize) -> Option<Self> { + if self.node.children().len() == 0 && cursor <= self.offset + self.len() { + return Some(self.clone()); + } + + let mut offset = self.offset; + let count = self.node.children().len(); + for (i, child) in self.children().enumerate() { + let len = child.len(); + if (offset < cursor && cursor <= offset + len) + || (offset == cursor && i + 1 == count) + { + return child.leaf_at(cursor); + } + offset += len; + } + + None + } + + /// Find the rightmost contained non-trivia leaf. + pub fn rightmost_leaf(&self) -> Option<Self> { + if self.is_leaf() && !self.kind().is_trivia() { + return Some(self.clone()); + } + + for child in self.children().rev() { + if let Some(leaf) = child.rightmost_leaf() { + return Some(leaf); + } + } + + None + } + + /// Get the leftmost non-trivia leaf after this node. + pub fn next_leaf(&self) -> Option<Self> { + let mut node = self.clone(); + while let Some(next) = node.next_sibling() { + if let Some(leaf) = next.leftmost_leaf() { + return Some(leaf); + } + node = next; + } + self.parent()?.next_leaf() + } +} + +impl Deref for LinkedNode<'_> { + type Target = SyntaxNode; + + fn deref(&self) -> &Self::Target { + self.get() + } +} + +impl Debug for LinkedNode<'_> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.node.fmt(f) + } +} + +/// An iterator over the children of a linked node. +pub struct LinkedChildren<'a> { + parent: Rc<LinkedNode<'a>>, + iter: std::iter::Enumerate<std::slice::Iter<'a, SyntaxNode>>, + front: usize, + back: usize, +} + +impl<'a> Iterator for LinkedChildren<'a> { + type Item = LinkedNode<'a>; + + fn next(&mut self) -> Option<Self::Item> { + self.iter.next().map(|(index, node)| { + let offset = self.front; + self.front += node.len(); + LinkedNode { + node, + parent: Some(self.parent.clone()), + index, + offset, + } + }) + } + + fn size_hint(&self) -> (usize, Option<usize>) { + self.iter.size_hint() + } +} + +impl DoubleEndedIterator for LinkedChildren<'_> { + fn next_back(&mut self) -> Option<Self::Item> { + self.iter.next_back().map(|(index, node)| { + self.back -= node.len(); + LinkedNode { + node, + parent: Some(self.parent.clone()), + index, + offset: self.back, + } + }) + } +} + +impl ExactSizeIterator for LinkedChildren<'_> {} + +/// Result of numbering a node within an interval. +pub(super) type NumberingResult = Result<(), Unnumberable>; + +/// Indicates that a node cannot be numbered within a given interval. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(super) struct Unnumberable; + +impl Display for Unnumberable { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("cannot number within this interval") + } +} + +impl std::error::Error for Unnumberable {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::syntax::Source; + + #[test] + fn test_linked_node() { + let source = Source::detached("#set text(12pt, red)"); + + // Find "text". + let node = LinkedNode::new(source.root()).leaf_at(7).unwrap(); + assert_eq!(node.offset(), 5); + assert_eq!(node.text(), "text"); + + // Go back to "#set". Skips the space. + let prev = node.prev_sibling().unwrap(); + assert_eq!(prev.offset(), 1); + assert_eq!(prev.text(), "set"); + } + + #[test] + fn test_linked_node_non_trivia_leaf() { + let source = Source::detached("#set fun(12pt, red)"); + let leaf = LinkedNode::new(source.root()).leaf_at(6).unwrap(); + let prev = leaf.prev_leaf().unwrap(); + assert_eq!(leaf.text(), "fun"); + assert_eq!(prev.text(), "set"); + + let source = Source::detached("#let x = 10"); + let leaf = LinkedNode::new(source.root()).leaf_at(9).unwrap(); + let prev = leaf.prev_leaf().unwrap(); + let next = leaf.next_leaf().unwrap(); + assert_eq!(prev.text(), "="); + assert_eq!(leaf.text(), " "); + assert_eq!(next.text(), "10"); + } +} diff --git a/crates/typst/src/syntax/parser.rs b/crates/typst/src/syntax/parser.rs new file mode 100644 index 00000000..54670df5 --- /dev/null +++ b/crates/typst/src/syntax/parser.rs @@ -0,0 +1,1643 @@ +use std::collections::HashSet; +use std::ops::Range; + +use ecow::{eco_format, EcoString}; +use unicode_math_class::MathClass; + +use super::{ast, is_newline, LexMode, Lexer, SyntaxKind, SyntaxNode}; + +/// Parse a source file. +pub fn parse(text: &str) -> SyntaxNode { + let mut p = Parser::new(text, 0, LexMode::Markup); + markup(&mut p, true, 0, |_| false); + p.finish().into_iter().next().unwrap() +} + +/// Parse code directly. +/// +/// This is only used for syntax highlighting. +pub fn parse_code(text: &str) -> SyntaxNode { + let mut p = Parser::new(text, 0, LexMode::Code); + let m = p.marker(); + p.skip(); + code_exprs(&mut p, |_| false); + p.wrap_skipless(m, SyntaxKind::Code); + p.finish().into_iter().next().unwrap() +} + +fn markup( + p: &mut Parser, + mut at_start: bool, + min_indent: usize, + mut stop: impl FnMut(&Parser) -> bool, +) { + let m = p.marker(); + let mut nesting: usize = 0; + while !p.eof() { + match p.current() { + SyntaxKind::LeftBracket => nesting += 1, + SyntaxKind::RightBracket if nesting > 0 => nesting -= 1, + _ if stop(p) => break, + _ => {} + } + + if p.newline() { + at_start = true; + if min_indent > 0 && p.column(p.current_end()) < min_indent { + break; + } + p.eat(); + continue; + } + + let prev = p.prev_end(); + markup_expr(p, &mut at_start); + if !p.progress(prev) { + p.unexpected(); + } + } + p.wrap(m, SyntaxKind::Markup); +} + +pub(super) fn reparse_markup( + text: &str, + range: Range<usize>, + at_start: &mut bool, + nesting: &mut usize, + mut stop: impl FnMut(SyntaxKind) -> bool, +) -> Option<Vec<SyntaxNode>> { + let mut p = Parser::new(text, range.start, LexMode::Markup); + while !p.eof() && p.current_start() < range.end { + match p.current() { + SyntaxKind::LeftBracket => *nesting += 1, + SyntaxKind::RightBracket if *nesting > 0 => *nesting -= 1, + _ if stop(p.current()) => break, + _ => {} + } + + if p.newline() { + *at_start = true; + p.eat(); + continue; + } + + let prev = p.prev_end(); + markup_expr(&mut p, at_start); + if !p.progress(prev) { + p.unexpected(); + } + } + (p.balanced && p.current_start() == range.end).then(|| p.finish()) +} + +fn markup_expr(p: &mut Parser, at_start: &mut bool) { + match p.current() { + SyntaxKind::Space + | SyntaxKind::Parbreak + | SyntaxKind::LineComment + | SyntaxKind::BlockComment => { + p.eat(); + return; + } + + SyntaxKind::Text + | SyntaxKind::Linebreak + | SyntaxKind::Escape + | SyntaxKind::Shorthand + | SyntaxKind::SmartQuote + | SyntaxKind::Raw + | SyntaxKind::Link + | SyntaxKind::Label => p.eat(), + + SyntaxKind::Hashtag => embedded_code_expr(p), + SyntaxKind::Star => strong(p), + SyntaxKind::Underscore => emph(p), + SyntaxKind::HeadingMarker if *at_start => heading(p), + SyntaxKind::ListMarker if *at_start => list_item(p), + SyntaxKind::EnumMarker if *at_start => enum_item(p), + SyntaxKind::TermMarker if *at_start => term_item(p), + SyntaxKind::RefMarker => reference(p), + SyntaxKind::Dollar => equation(p), + + SyntaxKind::LeftBracket + | SyntaxKind::RightBracket + | SyntaxKind::HeadingMarker + | SyntaxKind::ListMarker + | SyntaxKind::EnumMarker + | SyntaxKind::TermMarker + | SyntaxKind::Colon => p.convert(SyntaxKind::Text), + + _ => {} + } + + *at_start = false; +} + +fn strong(p: &mut Parser) { + let m = p.marker(); + p.assert(SyntaxKind::Star); + markup(p, false, 0, |p| { + p.at(SyntaxKind::Star) + || p.at(SyntaxKind::Parbreak) + || p.at(SyntaxKind::RightBracket) + }); + p.expect_closing_delimiter(m, SyntaxKind::Star); + p.wrap(m, SyntaxKind::Strong); +} + +fn emph(p: &mut Parser) { + let m = p.marker(); + p.assert(SyntaxKind::Underscore); + markup(p, false, 0, |p| { + p.at(SyntaxKind::Underscore) + || p.at(SyntaxKind::Parbreak) + || p.at(SyntaxKind::RightBracket) + }); + p.expect_closing_delimiter(m, SyntaxKind::Underscore); + p.wrap(m, SyntaxKind::Emph); +} + +fn heading(p: &mut Parser) { + let m = p.marker(); + p.assert(SyntaxKind::HeadingMarker); + whitespace_line(p); + markup(p, false, usize::MAX, |p| { + p.at(SyntaxKind::Label) + || p.at(SyntaxKind::RightBracket) + || (p.at(SyntaxKind::Space) && p.lexer.clone().next() == SyntaxKind::Label) + }); + p.wrap(m, SyntaxKind::Heading); +} + +fn list_item(p: &mut Parser) { + let m = p.marker(); + p.assert(SyntaxKind::ListMarker); + let min_indent = p.column(p.prev_end()); + whitespace_line(p); + markup(p, false, min_indent, |p| p.at(SyntaxKind::RightBracket)); + p.wrap(m, SyntaxKind::ListItem); +} + +fn enum_item(p: &mut Parser) { + let m = p.marker(); + p.assert(SyntaxKind::EnumMarker); + let min_indent = p.column(p.prev_end()); + whitespace_line(p); + markup(p, false, min_indent, |p| p.at(SyntaxKind::RightBracket)); + p.wrap(m, SyntaxKind::EnumItem); +} + +fn term_item(p: &mut Parser) { + let m = p.marker(); + p.assert(SyntaxKind::TermMarker); + let min_indent = p.column(p.prev_end()); + whitespace_line(p); + markup(p, false, usize::MAX, |p| { + p.at(SyntaxKind::Colon) || p.at(SyntaxKind::RightBracket) + }); + p.expect(SyntaxKind::Colon); + whitespace_line(p); + markup(p, false, min_indent, |p| p.at(SyntaxKind::RightBracket)); + p.wrap(m, SyntaxKind::TermItem); +} + +fn reference(p: &mut Parser) { + let m = p.marker(); + p.assert(SyntaxKind::RefMarker); + if p.directly_at(SyntaxKind::LeftBracket) { + content_block(p); + } + p.wrap(m, SyntaxKind::Ref); +} + +fn whitespace_line(p: &mut Parser) { + while !p.newline() && p.current().is_trivia() { + p.eat(); + } +} + +fn equation(p: &mut Parser) { + let m = p.marker(); + p.enter(LexMode::Math); + p.assert(SyntaxKind::Dollar); + math(p, |p| p.at(SyntaxKind::Dollar)); + p.expect_closing_delimiter(m, SyntaxKind::Dollar); + p.exit(); + p.wrap(m, SyntaxKind::Equation); +} + +fn math(p: &mut Parser, mut stop: impl FnMut(&Parser) -> bool) { + let m = p.marker(); + while !p.eof() && !stop(p) { + let prev = p.prev_end(); + math_expr(p); + if !p.progress(prev) { + p.unexpected(); + } + } + p.wrap(m, SyntaxKind::Math); +} + +fn math_expr(p: &mut Parser) { + math_expr_prec(p, 0, SyntaxKind::Eof) +} + +fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { + let m = p.marker(); + let mut continuable = false; + match p.current() { + SyntaxKind::Hashtag => embedded_code_expr(p), + SyntaxKind::MathIdent => { + continuable = true; + p.eat(); + while p.directly_at(SyntaxKind::Text) + && p.current_text() == "." + && matches!( + p.lexer.clone().next(), + SyntaxKind::MathIdent | SyntaxKind::Text + ) + { + p.convert(SyntaxKind::Dot); + p.convert(SyntaxKind::Ident); + p.wrap(m, SyntaxKind::FieldAccess); + } + if min_prec < 3 && p.directly_at(SyntaxKind::Text) && p.current_text() == "(" + { + math_args(p); + p.wrap(m, SyntaxKind::FuncCall); + continuable = false; + } + } + + SyntaxKind::Text | SyntaxKind::Shorthand => { + continuable = matches!( + math_class(p.current_text()), + None | Some(MathClass::Alphabetic) + ); + if !maybe_delimited(p, true) { + p.eat(); + } + } + + SyntaxKind::Linebreak | SyntaxKind::MathAlignPoint => p.eat(), + SyntaxKind::Escape | SyntaxKind::Str => { + continuable = true; + p.eat(); + } + + SyntaxKind::Root => { + if min_prec < 3 { + p.eat(); + let m2 = p.marker(); + math_expr_prec(p, 2, stop); + math_unparen(p, m2); + p.wrap(m, SyntaxKind::MathRoot); + } + } + + _ => p.expected("expression"), + } + + if continuable + && min_prec < 3 + && p.prev_end() == p.current_start() + && maybe_delimited(p, false) + { + p.wrap(m, SyntaxKind::Math); + } + + while !p.eof() && !p.at(stop) { + if p.directly_at(SyntaxKind::Text) && p.current_text() == "!" { + p.eat(); + p.wrap(m, SyntaxKind::Math); + continue; + } + + let Some((kind, stop, assoc, mut prec)) = math_op(p.current()) else { + break; + }; + + if prec < min_prec { + break; + } + + match assoc { + ast::Assoc::Left => prec += 1, + ast::Assoc::Right => {} + } + + if kind == SyntaxKind::MathFrac { + math_unparen(p, m); + } + + p.eat(); + let m2 = p.marker(); + math_expr_prec(p, prec, stop); + math_unparen(p, m2); + + if p.eat_if(SyntaxKind::Underscore) || p.eat_if(SyntaxKind::Hat) { + let m3 = p.marker(); + math_expr_prec(p, prec, SyntaxKind::Eof); + math_unparen(p, m3); + } + + p.wrap(m, kind); + } +} + +fn maybe_delimited(p: &mut Parser, allow_fence: bool) -> bool { + if allow_fence && math_class(p.current_text()) == Some(MathClass::Fence) { + math_delimited(p, MathClass::Fence); + true + } else if math_class(p.current_text()) == Some(MathClass::Opening) { + math_delimited(p, MathClass::Closing); + true + } else { + false + } +} + +fn math_delimited(p: &mut Parser, stop: MathClass) { + let m = p.marker(); + p.eat(); + let m2 = p.marker(); + while !p.eof() && !p.at(SyntaxKind::Dollar) { + let class = math_class(p.current_text()); + if stop == MathClass::Fence && class == Some(MathClass::Closing) { + break; + } + + if class == Some(stop) { + p.wrap(m2, SyntaxKind::Math); + p.eat(); + p.wrap(m, SyntaxKind::MathDelimited); + return; + } + + let prev = p.prev_end(); + math_expr(p); + if !p.progress(prev) { + p.unexpected(); + } + } + + p.wrap(m, SyntaxKind::Math); +} + +fn math_unparen(p: &mut Parser, m: Marker) { + let Some(node) = p.nodes.get_mut(m.0) else { return }; + if node.kind() != SyntaxKind::MathDelimited { + return; + } + + if let [first, .., last] = node.children_mut() { + if first.text() == "(" && last.text() == ")" { + first.convert_to_kind(SyntaxKind::LeftParen); + last.convert_to_kind(SyntaxKind::RightParen); + } + } + + node.convert_to_kind(SyntaxKind::Math); +} + +fn math_class(text: &str) -> Option<MathClass> { + match text { + "[|" => return Some(MathClass::Opening), + "|]" => return Some(MathClass::Closing), + "||" => return Some(MathClass::Fence), + _ => {} + } + + let mut chars = text.chars(); + chars + .next() + .filter(|_| chars.next().is_none()) + .and_then(unicode_math_class::class) +} + +fn math_op(kind: SyntaxKind) -> Option<(SyntaxKind, SyntaxKind, ast::Assoc, usize)> { + match kind { + SyntaxKind::Underscore => { + Some((SyntaxKind::MathAttach, SyntaxKind::Hat, ast::Assoc::Right, 2)) + } + SyntaxKind::Hat => { + Some((SyntaxKind::MathAttach, SyntaxKind::Underscore, ast::Assoc::Right, 2)) + } + SyntaxKind::Slash => { + Some((SyntaxKind::MathFrac, SyntaxKind::Eof, ast::Assoc::Left, 1)) + } + _ => None, + } +} + +fn math_args(p: &mut Parser) { + let m = p.marker(); + p.convert(SyntaxKind::LeftParen); + + let mut namable = true; + let mut named = None; + let mut has_arrays = false; + let mut array = p.marker(); + let mut arg = p.marker(); + + while !p.eof() && !p.at(SyntaxKind::Dollar) { + if namable + && (p.at(SyntaxKind::MathIdent) || p.at(SyntaxKind::Text)) + && p.text[p.current_end()..].starts_with(':') + { + p.convert(SyntaxKind::Ident); + p.convert(SyntaxKind::Colon); + named = Some(arg); + arg = p.marker(); + array = p.marker(); + } + + match p.current_text() { + ")" => break, + ";" => { + maybe_wrap_in_math(p, arg, named); + p.wrap(array, SyntaxKind::Array); + p.convert(SyntaxKind::Semicolon); + array = p.marker(); + arg = p.marker(); + namable = true; + named = None; + has_arrays = true; + continue; + } + "," => { + maybe_wrap_in_math(p, arg, named); + p.convert(SyntaxKind::Comma); + arg = p.marker(); + namable = true; + if named.is_some() { + array = p.marker(); + named = None; + } + continue; + } + _ => {} + } + + let prev = p.prev_end(); + math_expr(p); + if !p.progress(prev) { + p.unexpected(); + } + + namable = false; + } + + if arg != p.marker() { + maybe_wrap_in_math(p, arg, named); + if named.is_some() { + array = p.marker(); + } + } + + if has_arrays && array != p.marker() { + p.wrap(array, SyntaxKind::Array); + } + + if p.at(SyntaxKind::Text) && p.current_text() == ")" { + p.convert(SyntaxKind::RightParen); + } else { + p.expected("closing paren"); + p.balanced = false; + } + + p.wrap(m, SyntaxKind::Args); +} + +fn maybe_wrap_in_math(p: &mut Parser, arg: Marker, named: Option<Marker>) { + let exprs = p.post_process(arg).filter(|node| node.is::<ast::Expr>()).count(); + if exprs != 1 { + p.wrap(arg, SyntaxKind::Math); + } + + if let Some(m) = named { + p.wrap(m, SyntaxKind::Named); + } +} + +fn code(p: &mut Parser, stop: impl FnMut(&Parser) -> bool) { + let m = p.marker(); + code_exprs(p, stop); + p.wrap(m, SyntaxKind::Code); +} + +fn code_exprs(p: &mut Parser, mut stop: impl FnMut(&Parser) -> bool) { + while !p.eof() && !stop(p) { + p.stop_at_newline(true); + let prev = p.prev_end(); + code_expr(p); + if p.progress(prev) && !p.eof() && !stop(p) && !p.eat_if(SyntaxKind::Semicolon) { + p.expected("semicolon or line break"); + } + p.unstop(); + if !p.progress(prev) && !p.eof() { + p.unexpected(); + } + } +} + +fn code_expr(p: &mut Parser) { + code_expr_prec(p, false, 0, false) +} + +fn code_expr_or_pattern(p: &mut Parser) { + code_expr_prec(p, false, 0, true) +} + +fn embedded_code_expr(p: &mut Parser) { + p.stop_at_newline(true); + p.enter(LexMode::Code); + p.assert(SyntaxKind::Hashtag); + p.unskip(); + + let stmt = matches!( + p.current(), + SyntaxKind::Let + | SyntaxKind::Set + | SyntaxKind::Show + | SyntaxKind::Import + | SyntaxKind::Include + ); + + let prev = p.prev_end(); + code_expr_prec(p, true, 0, false); + + // Consume error for things like `#12p` or `#"abc\"`. + if !p.progress(prev) { + p.unexpected(); + } + + let semi = + (stmt || p.directly_at(SyntaxKind::Semicolon)) && p.eat_if(SyntaxKind::Semicolon); + + if stmt && !semi && !p.eof() && !p.at(SyntaxKind::RightBracket) { + p.expected("semicolon or line break"); + } + + p.exit(); + p.unstop(); +} + +fn code_expr_prec( + p: &mut Parser, + atomic: bool, + min_prec: usize, + allow_destructuring: bool, +) { + let m = p.marker(); + if let (false, Some(op)) = (atomic, ast::UnOp::from_kind(p.current())) { + p.eat(); + code_expr_prec(p, atomic, op.precedence(), false); + p.wrap(m, SyntaxKind::Unary); + } else { + code_primary(p, atomic, allow_destructuring); + } + + loop { + if p.directly_at(SyntaxKind::LeftParen) || p.directly_at(SyntaxKind::LeftBracket) + { + args(p); + p.wrap(m, SyntaxKind::FuncCall); + continue; + } + + let at_field_or_method = + p.directly_at(SyntaxKind::Dot) && p.lexer.clone().next() == SyntaxKind::Ident; + + if atomic && !at_field_or_method { + break; + } + + if p.eat_if(SyntaxKind::Dot) { + p.expect(SyntaxKind::Ident); + p.wrap(m, SyntaxKind::FieldAccess); + continue; + } + + let binop = + if ast::BinOp::NotIn.precedence() >= min_prec && p.eat_if(SyntaxKind::Not) { + if p.at(SyntaxKind::In) { + Some(ast::BinOp::NotIn) + } else { + p.expected("keyword `in`"); + break; + } + } else { + ast::BinOp::from_kind(p.current()) + }; + + if let Some(op) = binop { + let mut prec = op.precedence(); + if prec < min_prec { + break; + } + + match op.assoc() { + ast::Assoc::Left => prec += 1, + ast::Assoc::Right => {} + } + + p.eat(); + code_expr_prec(p, false, prec, false); + p.wrap(m, SyntaxKind::Binary); + continue; + } + + break; + } +} + +fn code_primary(p: &mut Parser, atomic: bool, allow_destructuring: bool) { + let m = p.marker(); + match p.current() { + SyntaxKind::Ident => { + p.eat(); + if !atomic && p.at(SyntaxKind::Arrow) { + p.wrap(m, SyntaxKind::Params); + p.assert(SyntaxKind::Arrow); + code_expr(p); + p.wrap(m, SyntaxKind::Closure); + } + } + SyntaxKind::Underscore if !atomic => { + p.eat(); + if p.at(SyntaxKind::Arrow) { + p.wrap(m, SyntaxKind::Params); + p.eat(); + code_expr(p); + p.wrap(m, SyntaxKind::Closure); + } else if let Some(underscore) = p.node_mut(m) { + underscore.convert_to_error("expected expression, found underscore"); + } + } + + SyntaxKind::LeftBrace => code_block(p), + SyntaxKind::LeftBracket => content_block(p), + SyntaxKind::LeftParen => with_paren(p, allow_destructuring), + SyntaxKind::Dollar => equation(p), + SyntaxKind::Let => let_binding(p), + SyntaxKind::Set => set_rule(p), + SyntaxKind::Show => show_rule(p), + SyntaxKind::If => conditional(p), + SyntaxKind::While => while_loop(p), + SyntaxKind::For => for_loop(p), + SyntaxKind::Import => module_import(p), + SyntaxKind::Include => module_include(p), + SyntaxKind::Break => break_stmt(p), + SyntaxKind::Continue => continue_stmt(p), + SyntaxKind::Return => return_stmt(p), + + SyntaxKind::None + | SyntaxKind::Auto + | SyntaxKind::Int + | SyntaxKind::Float + | SyntaxKind::Bool + | SyntaxKind::Numeric + | SyntaxKind::Str + | SyntaxKind::Label + | SyntaxKind::Raw => p.eat(), + + _ => p.expected("expression"), + } +} + +fn block(p: &mut Parser) { + match p.current() { + SyntaxKind::LeftBracket => content_block(p), + SyntaxKind::LeftBrace => code_block(p), + _ => p.expected("block"), + } +} + +pub(super) fn reparse_block(text: &str, range: Range<usize>) -> Option<SyntaxNode> { + let mut p = Parser::new(text, range.start, LexMode::Code); + assert!(p.at(SyntaxKind::LeftBracket) || p.at(SyntaxKind::LeftBrace)); + block(&mut p); + (p.balanced && p.prev_end() == range.end) + .then(|| p.finish().into_iter().next().unwrap()) +} + +fn code_block(p: &mut Parser) { + let m = p.marker(); + p.enter(LexMode::Code); + p.stop_at_newline(false); + p.assert(SyntaxKind::LeftBrace); + code(p, |p| { + p.at(SyntaxKind::RightBrace) + || p.at(SyntaxKind::RightBracket) + || p.at(SyntaxKind::RightParen) + }); + p.expect_closing_delimiter(m, SyntaxKind::RightBrace); + p.exit(); + p.unstop(); + p.wrap(m, SyntaxKind::CodeBlock); +} + +fn content_block(p: &mut Parser) { + let m = p.marker(); + p.enter(LexMode::Markup); + p.assert(SyntaxKind::LeftBracket); + markup(p, true, 0, |p| p.at(SyntaxKind::RightBracket)); + p.expect_closing_delimiter(m, SyntaxKind::RightBracket); + p.exit(); + p.wrap(m, SyntaxKind::ContentBlock); +} + +fn with_paren(p: &mut Parser, allow_destructuring: bool) { + let m = p.marker(); + let mut kind = collection(p, true); + if p.at(SyntaxKind::Arrow) { + validate_params_at(p, m); + p.wrap(m, SyntaxKind::Params); + p.assert(SyntaxKind::Arrow); + code_expr(p); + kind = SyntaxKind::Closure; + } else if p.at(SyntaxKind::Eq) && kind != SyntaxKind::Parenthesized { + // TODO: add warning if p.at(SyntaxKind::Eq) && kind == SyntaxKind::Parenthesized + + validate_pattern_at(p, m, false); + p.wrap(m, SyntaxKind::Destructuring); + p.assert(SyntaxKind::Eq); + code_expr(p); + kind = SyntaxKind::DestructAssignment; + } + + match kind { + SyntaxKind::Array if !allow_destructuring => validate_array_at(p, m), + SyntaxKind::Dict if !allow_destructuring => validate_dict_at(p, m), + SyntaxKind::Parenthesized if !allow_destructuring => { + validate_parenthesized_at(p, m) + } + SyntaxKind::Destructuring if !allow_destructuring => { + invalidate_destructuring(p, m) + } + _ => {} + } + p.wrap(m, kind); +} + +fn invalidate_destructuring(p: &mut Parser, m: Marker) { + let mut collection_kind = Option::None; + for child in p.post_process(m) { + match child.kind() { + SyntaxKind::Named | SyntaxKind::Keyed => match collection_kind { + Some(SyntaxKind::Array) => child.convert_to_error(eco_format!( + "expected expression, found {}", + child.kind().name() + )), + _ => collection_kind = Some(SyntaxKind::Dict), + }, + SyntaxKind::LeftParen | SyntaxKind::RightParen | SyntaxKind::Comma => {} + kind => match collection_kind { + Some(SyntaxKind::Dict) => child.convert_to_error(eco_format!( + "expected named or keyed pair, found {}", + kind.name() + )), + _ => collection_kind = Some(SyntaxKind::Array), + }, + } + } +} + +fn collection(p: &mut Parser, keyed: bool) -> SyntaxKind { + p.stop_at_newline(false); + + let m = p.marker(); + p.assert(SyntaxKind::LeftParen); + + let mut count = 0; + let mut parenthesized = true; + let mut kind = None; + if keyed && p.eat_if(SyntaxKind::Colon) { + kind = Some(SyntaxKind::Dict); + parenthesized = false; + } + + while !p.current().is_terminator() { + let prev = p.prev_end(); + match item(p, keyed) { + SyntaxKind::Spread => parenthesized = false, + SyntaxKind::Named | SyntaxKind::Keyed => { + match kind { + Some(SyntaxKind::Array) => kind = Some(SyntaxKind::Destructuring), + _ => kind = Some(SyntaxKind::Dict), + } + parenthesized = false; + } + SyntaxKind::Int => match kind { + Some(SyntaxKind::Array) | None => kind = Some(SyntaxKind::Array), + Some(_) => kind = Some(SyntaxKind::Destructuring), + }, + _ if kind.is_none() => kind = Some(SyntaxKind::Array), + _ => {} + } + + if !p.progress(prev) { + p.unexpected(); + continue; + } + + count += 1; + + if p.current().is_terminator() { + break; + } + + if p.expect(SyntaxKind::Comma) { + parenthesized = false; + } + } + + p.expect_closing_delimiter(m, SyntaxKind::RightParen); + p.unstop(); + + if parenthesized && count == 1 { + SyntaxKind::Parenthesized + } else { + kind.unwrap_or(SyntaxKind::Array) + } +} + +fn item(p: &mut Parser, keyed: bool) -> SyntaxKind { + let m = p.marker(); + + if p.eat_if(SyntaxKind::Dots) { + if p.at(SyntaxKind::Comma) || p.at(SyntaxKind::RightParen) { + p.wrap(m, SyntaxKind::Spread); + return SyntaxKind::Spread; + } + + code_expr(p); + p.wrap(m, SyntaxKind::Spread); + return SyntaxKind::Spread; + } + + if p.at(SyntaxKind::Underscore) { + // This is a temporary workaround to fix `v.map(_ => {})`. + let mut lexer = p.lexer.clone(); + let next = + std::iter::from_fn(|| Some(lexer.next())).find(|kind| !kind.is_trivia()); + if next != Some(SyntaxKind::Arrow) { + p.eat(); + return SyntaxKind::Underscore; + } + } + + code_expr_or_pattern(p); + + if !p.eat_if(SyntaxKind::Colon) { + return SyntaxKind::Int; + } + + if !p.eat_if(SyntaxKind::Underscore) { + code_expr(p); + } + + let kind = match p.node(m).map(SyntaxNode::kind) { + Some(SyntaxKind::Ident) => SyntaxKind::Named, + Some(SyntaxKind::Str) if keyed => SyntaxKind::Keyed, + _ => { + for child in p.post_process(m) { + if child.kind() == SyntaxKind::Colon { + break; + } + + let mut message = EcoString::from("expected identifier"); + if keyed { + message.push_str(" or string"); + } + message.push_str(", found "); + message.push_str(child.kind().name()); + child.convert_to_error(message); + } + SyntaxKind::Named + } + }; + + p.wrap(m, kind); + kind +} + +fn args(p: &mut Parser) { + if !p.at(SyntaxKind::LeftParen) && !p.at(SyntaxKind::LeftBracket) { + p.expected("argument list"); + } + + let m = p.marker(); + if p.at(SyntaxKind::LeftParen) { + collection(p, false); + validate_args_at(p, m); + } + + while p.directly_at(SyntaxKind::LeftBracket) { + content_block(p); + } + + p.wrap(m, SyntaxKind::Args); +} + +enum PatternKind { + Ident, + Placeholder, + Destructuring, +} + +fn pattern(p: &mut Parser) -> PatternKind { + let m = p.marker(); + if p.at(SyntaxKind::LeftParen) { + let kind = collection(p, false); + validate_pattern_at(p, m, true); + + if kind == SyntaxKind::Parenthesized { + PatternKind::Ident + } else { + p.wrap(m, SyntaxKind::Destructuring); + PatternKind::Destructuring + } + } else if p.eat_if(SyntaxKind::Underscore) { + PatternKind::Placeholder + } else { + p.expect(SyntaxKind::Ident); + PatternKind::Ident + } +} + +fn let_binding(p: &mut Parser) { + let m = p.marker(); + p.assert(SyntaxKind::Let); + + let m2 = p.marker(); + let mut closure = false; + let mut destructuring = false; + match pattern(p) { + PatternKind::Ident => { + closure = p.directly_at(SyntaxKind::LeftParen); + if closure { + let m3 = p.marker(); + collection(p, false); + validate_params_at(p, m3); + p.wrap(m3, SyntaxKind::Params); + } + } + PatternKind::Placeholder => {} + PatternKind::Destructuring => destructuring = true, + } + + let f = if closure || destructuring { Parser::expect } else { Parser::eat_if }; + if f(p, SyntaxKind::Eq) { + code_expr(p); + } + + if closure { + p.wrap(m2, SyntaxKind::Closure); + } + + p.wrap(m, SyntaxKind::LetBinding); +} + +fn set_rule(p: &mut Parser) { + let m = p.marker(); + p.assert(SyntaxKind::Set); + + let m2 = p.marker(); + p.expect(SyntaxKind::Ident); + while p.eat_if(SyntaxKind::Dot) { + p.expect(SyntaxKind::Ident); + p.wrap(m2, SyntaxKind::FieldAccess); + } + + args(p); + if p.eat_if(SyntaxKind::If) { + code_expr(p); + } + p.wrap(m, SyntaxKind::SetRule); +} + +fn show_rule(p: &mut Parser) { + let m = p.marker(); + p.assert(SyntaxKind::Show); + p.unskip(); + let m2 = p.marker(); + p.skip(); + + if !p.at(SyntaxKind::Colon) { + code_expr(p); + } + + if p.eat_if(SyntaxKind::Colon) { + code_expr(p); + } else { + p.expected_at(m2, "colon"); + } + + p.wrap(m, SyntaxKind::ShowRule); +} + +fn conditional(p: &mut Parser) { + let m = p.marker(); + p.assert(SyntaxKind::If); + code_expr(p); + block(p); + if p.eat_if(SyntaxKind::Else) { + if p.at(SyntaxKind::If) { + conditional(p); + } else { + block(p); + } + } + p.wrap(m, SyntaxKind::Conditional); +} + +fn while_loop(p: &mut Parser) { + let m = p.marker(); + p.assert(SyntaxKind::While); + code_expr(p); + block(p); + p.wrap(m, SyntaxKind::WhileLoop); +} + +fn for_loop(p: &mut Parser) { + let m = p.marker(); + p.assert(SyntaxKind::For); + pattern(p); + if p.at(SyntaxKind::Comma) { + p.expected("keyword `in` - did you mean to use a destructuring pattern?"); + if !p.eat_if(SyntaxKind::Ident) { + p.eat_if(SyntaxKind::Underscore); + } + p.eat_if(SyntaxKind::In); + } else { + p.expect(SyntaxKind::In); + } + code_expr(p); + block(p); + p.wrap(m, SyntaxKind::ForLoop); +} + +fn module_import(p: &mut Parser) { + let m = p.marker(); + p.assert(SyntaxKind::Import); + code_expr(p); + if p.eat_if(SyntaxKind::Colon) && !p.eat_if(SyntaxKind::Star) { + import_items(p); + } + p.wrap(m, SyntaxKind::ModuleImport); +} + +fn import_items(p: &mut Parser) { + let m = p.marker(); + while !p.eof() && !p.at(SyntaxKind::Semicolon) { + if !p.eat_if(SyntaxKind::Ident) { + p.unexpected(); + } + if p.current().is_terminator() { + break; + } + p.expect(SyntaxKind::Comma); + } + p.wrap(m, SyntaxKind::ImportItems); +} + +fn module_include(p: &mut Parser) { + let m = p.marker(); + p.assert(SyntaxKind::Include); + code_expr(p); + p.wrap(m, SyntaxKind::ModuleInclude); +} + +fn break_stmt(p: &mut Parser) { + let m = p.marker(); + p.assert(SyntaxKind::Break); + p.wrap(m, SyntaxKind::LoopBreak); +} + +fn continue_stmt(p: &mut Parser) { + let m = p.marker(); + p.assert(SyntaxKind::Continue); + p.wrap(m, SyntaxKind::LoopContinue); +} + +fn return_stmt(p: &mut Parser) { + let m = p.marker(); + p.assert(SyntaxKind::Return); + if !p.current().is_terminator() && !p.at(SyntaxKind::Comma) { + code_expr(p); + } + p.wrap(m, SyntaxKind::FuncReturn); +} + +fn validate_parenthesized_at(p: &mut Parser, m: Marker) { + for child in p.post_process(m) { + let kind = child.kind(); + match kind { + SyntaxKind::Array => validate_array(child.children_mut().iter_mut()), + SyntaxKind::Dict => validate_dict(child.children_mut().iter_mut()), + SyntaxKind::Underscore => { + child.convert_to_error(eco_format!( + "expected expression, found {}", + kind.name() + )); + } + _ => {} + } + } +} + +fn validate_array_at(p: &mut Parser, m: Marker) { + validate_array(p.post_process(m)) +} + +fn validate_array<'a>(children: impl Iterator<Item = &'a mut SyntaxNode>) { + for child in children { + let kind = child.kind(); + match kind { + SyntaxKind::Array => validate_array(child.children_mut().iter_mut()), + SyntaxKind::Dict => validate_dict(child.children_mut().iter_mut()), + SyntaxKind::Named | SyntaxKind::Keyed | SyntaxKind::Underscore => { + child.convert_to_error(eco_format!( + "expected expression, found {}", + kind.name() + )); + } + _ => {} + } + } +} + +fn validate_dict_at(p: &mut Parser, m: Marker) { + validate_dict(p.post_process(m)) +} + +fn validate_dict<'a>(children: impl Iterator<Item = &'a mut SyntaxNode>) { + let mut used = HashSet::new(); + for child in children { + match child.kind() { + SyntaxKind::Named | SyntaxKind::Keyed => { + let Some(first) = child.children_mut().first_mut() else { continue }; + let key = match first.cast::<ast::Str>() { + Some(str) => str.get(), + None => first.text().clone(), + }; + + if !used.insert(key.clone()) { + first.convert_to_error(eco_format!("duplicate key: {key}")); + child.make_erroneous(); + } + } + SyntaxKind::Spread => {} + SyntaxKind::LeftParen + | SyntaxKind::RightParen + | SyntaxKind::Comma + | SyntaxKind::Colon => {} + kind => { + child.convert_to_error(eco_format!( + "expected named or keyed pair, found {}", + kind.name() + )); + } + } + } +} + +fn validate_params_at(p: &mut Parser, m: Marker) { + let mut used_spread = false; + let mut used = HashSet::new(); + for child in p.post_process(m) { + match child.kind() { + SyntaxKind::Ident => { + if !used.insert(child.text().clone()) { + child.convert_to_error(eco_format!( + "duplicate parameter: {}", + child.text() + )); + } + } + SyntaxKind::Named => { + let Some(within) = child.children_mut().first_mut() else { return }; + if !used.insert(within.text().clone()) { + within.convert_to_error(eco_format!( + "duplicate parameter: {}", + within.text() + )); + child.make_erroneous(); + } + } + SyntaxKind::Spread => { + let Some(within) = child.children_mut().last_mut() else { continue }; + if used_spread { + child.convert_to_error("only one argument sink is allowed"); + continue; + } + used_spread = true; + if within.kind() == SyntaxKind::Dots { + continue; + } else if within.kind() != SyntaxKind::Ident { + within.convert_to_error(eco_format!( + "expected identifier, found {}", + within.kind().name(), + )); + child.make_erroneous(); + continue; + } + if !used.insert(within.text().clone()) { + within.convert_to_error(eco_format!( + "duplicate parameter: {}", + within.text() + )); + child.make_erroneous(); + } + } + SyntaxKind::Array | SyntaxKind::Dict | SyntaxKind::Destructuring => { + validate_pattern(child.children_mut().iter_mut(), &mut used, false); + child.convert_to_kind(SyntaxKind::Destructuring); + } + SyntaxKind::LeftParen + | SyntaxKind::RightParen + | SyntaxKind::Comma + | SyntaxKind::Underscore => {} + kind => { + child.convert_to_error(eco_format!( + "expected identifier, named pair or argument sink, found {}", + kind.name() + )); + } + } + } +} + +fn validate_args_at(p: &mut Parser, m: Marker) { + let mut used = HashSet::new(); + for child in p.post_process(m) { + if child.kind() == SyntaxKind::Named { + let Some(within) = child.children_mut().first_mut() else { return }; + if !used.insert(within.text().clone()) { + within.convert_to_error(eco_format!( + "duplicate argument: {}", + within.text() + )); + child.make_erroneous(); + } + } else if child.kind() == SyntaxKind::Underscore { + child.convert_to_error("unexpected underscore"); + } + } +} + +fn validate_pattern_at(p: &mut Parser, m: Marker, forbid_expressions: bool) { + let mut used = HashSet::new(); + validate_pattern(p.post_process(m), &mut used, forbid_expressions); +} + +fn validate_pattern<'a>( + children: impl Iterator<Item = &'a mut SyntaxNode>, + used: &mut HashSet<EcoString>, + forbid_expressions: bool, +) { + let mut used_spread = false; + for child in children { + match child.kind() { + SyntaxKind::Ident => { + if !used.insert(child.text().clone()) { + child.convert_to_error( + "at most one binding per identifier is allowed", + ); + } + } + SyntaxKind::Spread => { + let Some(within) = child.children_mut().last_mut() else { continue }; + if used_spread { + child.convert_to_error("at most one destructuring sink is allowed"); + continue; + } + used_spread = true; + + if within.kind() == SyntaxKind::Dots { + continue; + } else if forbid_expressions && within.kind() != SyntaxKind::Ident { + within.convert_to_error(eco_format!( + "expected identifier, found {}", + within.kind().name(), + )); + child.make_erroneous(); + continue; + } + + if !used.insert(within.text().clone()) { + within.convert_to_error( + "at most one binding per identifier is allowed", + ); + child.make_erroneous(); + } + } + SyntaxKind::Named => { + let Some(within) = child.children_mut().first_mut() else { return }; + if !used.insert(within.text().clone()) { + within.convert_to_error( + "at most one binding per identifier is allowed", + ); + child.make_erroneous(); + } + + if forbid_expressions { + let Some(within) = child.children_mut().last_mut() else { return }; + if within.kind() != SyntaxKind::Ident + && within.kind() != SyntaxKind::Underscore + { + within.convert_to_error(eco_format!( + "expected identifier, found {}", + within.kind().name(), + )); + child.make_erroneous(); + } + } + } + SyntaxKind::LeftParen + | SyntaxKind::RightParen + | SyntaxKind::Comma + | SyntaxKind::Underscore => {} + kind => { + if forbid_expressions { + child.convert_to_error(eco_format!( + "expected identifier or destructuring sink, found {}", + kind.name() + )); + } + } + } + } +} + +/// Manages parsing of a stream of tokens. +struct Parser<'s> { + text: &'s str, + lexer: Lexer<'s>, + prev_end: usize, + current_start: usize, + current: SyntaxKind, + modes: Vec<LexMode>, + nodes: Vec<SyntaxNode>, + stop_at_newline: Vec<bool>, + balanced: bool, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +struct Marker(usize); + +impl<'s> Parser<'s> { + fn new(text: &'s str, offset: usize, mode: LexMode) -> Self { + let mut lexer = Lexer::new(text, mode); + lexer.jump(offset); + let current = lexer.next(); + Self { + lexer, + text, + prev_end: offset, + current_start: offset, + current, + modes: vec![], + nodes: vec![], + stop_at_newline: vec![], + balanced: true, + } + } + + fn finish(self) -> Vec<SyntaxNode> { + self.nodes + } + + fn prev_end(&self) -> usize { + self.prev_end + } + + fn current(&self) -> SyntaxKind { + self.current + } + + fn current_start(&self) -> usize { + self.current_start + } + + fn current_end(&self) -> usize { + self.lexer.cursor() + } + + fn current_text(&self) -> &'s str { + &self.text[self.current_start..self.current_end()] + } + + fn at(&self, kind: SyntaxKind) -> bool { + self.current == kind + } + + #[track_caller] + fn assert(&mut self, kind: SyntaxKind) { + assert_eq!(self.current, kind); + self.eat(); + } + + fn eof(&self) -> bool { + self.at(SyntaxKind::Eof) + } + + fn directly_at(&self, kind: SyntaxKind) -> bool { + self.current == kind && self.prev_end == self.current_start + } + + fn eat_if(&mut self, kind: SyntaxKind) -> bool { + let at = self.at(kind); + if at { + self.eat(); + } + at + } + + fn convert(&mut self, kind: SyntaxKind) { + self.current = kind; + self.eat(); + } + + fn newline(&mut self) -> bool { + self.lexer.newline() + } + + fn column(&self, at: usize) -> usize { + self.text[..at].chars().rev().take_while(|&c| !is_newline(c)).count() + } + + fn marker(&self) -> Marker { + Marker(self.nodes.len()) + } + + fn node(&self, m: Marker) -> Option<&SyntaxNode> { + self.nodes.get(m.0) + } + + fn node_mut(&mut self, m: Marker) -> Option<&mut SyntaxNode> { + self.nodes.get_mut(m.0) + } + + fn post_process(&mut self, m: Marker) -> impl Iterator<Item = &mut SyntaxNode> { + self.nodes[m.0..] + .iter_mut() + .filter(|child| !child.kind().is_error() && !child.kind().is_trivia()) + } + + fn wrap(&mut self, m: Marker, kind: SyntaxKind) { + self.unskip(); + self.wrap_skipless(m, kind); + self.skip(); + } + + fn wrap_skipless(&mut self, m: Marker, kind: SyntaxKind) { + let from = m.0.min(self.nodes.len()); + let children = self.nodes.drain(from..).collect(); + self.nodes.push(SyntaxNode::inner(kind, children)); + } + + fn progress(&self, offset: usize) -> bool { + offset < self.prev_end + } + + fn enter(&mut self, mode: LexMode) { + self.modes.push(self.lexer.mode()); + self.lexer.set_mode(mode); + } + + fn exit(&mut self) { + let mode = self.modes.pop().unwrap(); + if mode != self.lexer.mode() { + self.unskip(); + self.lexer.set_mode(mode); + self.lexer.jump(self.current_start); + self.lex(); + self.skip(); + } + } + + fn stop_at_newline(&mut self, stop: bool) { + self.stop_at_newline.push(stop); + } + + fn unstop(&mut self) { + self.unskip(); + self.stop_at_newline.pop(); + self.lexer.jump(self.prev_end); + self.lex(); + self.skip(); + } + + fn eat(&mut self) { + self.save(); + self.lex(); + self.skip(); + } + + fn skip(&mut self) { + if self.lexer.mode() != LexMode::Markup { + while self.current.is_trivia() { + self.save(); + self.lex(); + } + } + } + + fn unskip(&mut self) { + if self.lexer.mode() != LexMode::Markup && self.prev_end != self.current_start { + while self.nodes.last().map_or(false, |last| last.kind().is_trivia()) { + self.nodes.pop(); + } + + self.lexer.jump(self.prev_end); + self.lex(); + } + } + + fn save(&mut self) { + let text = self.current_text(); + if self.at(SyntaxKind::Error) { + let message = self.lexer.take_error().unwrap(); + self.nodes.push(SyntaxNode::error(message, text)); + } else { + self.nodes.push(SyntaxNode::leaf(self.current, text)); + } + + if self.lexer.mode() == LexMode::Markup || !self.current.is_trivia() { + self.prev_end = self.current_end(); + } + } + + fn lex(&mut self) { + self.current_start = self.lexer.cursor(); + self.current = self.lexer.next(); + if self.lexer.mode() == LexMode::Code + && self.lexer.newline() + && self.stop_at_newline.last().copied().unwrap_or(false) + && !matches!(self.lexer.clone().next(), SyntaxKind::Else | SyntaxKind::Dot) + { + self.current = SyntaxKind::Eof; + } + } + + fn expect(&mut self, kind: SyntaxKind) -> bool { + let at = self.at(kind); + if at { + self.eat(); + } else { + self.balanced &= !kind.is_grouping(); + self.expected(kind.name()); + } + at + } + + fn expect_closing_delimiter(&mut self, open: Marker, kind: SyntaxKind) { + if !self.eat_if(kind) { + self.nodes[open.0].convert_to_error("unclosed delimiter"); + } + } + + fn expected(&mut self, thing: &str) { + self.unskip(); + if self + .nodes + .last() + .map_or(true, |child| child.kind() != SyntaxKind::Error) + { + let message = eco_format!("expected {}", thing); + self.nodes.push(SyntaxNode::error(message, "")); + } + self.skip(); + } + + fn expected_at(&mut self, m: Marker, thing: &str) { + let message = eco_format!("expected {}", thing); + let error = SyntaxNode::error(message, ""); + self.nodes.insert(m.0, error); + } + + fn unexpected(&mut self) { + self.unskip(); + while self + .nodes + .last() + .map_or(false, |child| child.kind() == SyntaxKind::Error && child.is_empty()) + { + self.nodes.pop(); + } + self.skip(); + + let kind = self.current; + let offset = self.nodes.len(); + self.eat(); + self.balanced &= !kind.is_grouping(); + + if !kind.is_error() { + self.nodes[offset] + .convert_to_error(eco_format!("unexpected {}", kind.name())); + } + } +} diff --git a/crates/typst/src/syntax/reparser.rs b/crates/typst/src/syntax/reparser.rs new file mode 100644 index 00000000..a4186fa7 --- /dev/null +++ b/crates/typst/src/syntax/reparser.rs @@ -0,0 +1,322 @@ +use std::ops::Range; + +use super::{ + is_newline, parse, reparse_block, reparse_markup, Span, SyntaxKind, SyntaxNode, +}; + +/// Refresh the given syntax node with as little parsing as possible. +/// +/// Takes the new text, the range in the old text that was replaced and the +/// length of the replacement and returns the range in the new text that was +/// ultimately reparsed. +/// +/// The high-level API for this function is +/// [`Source::edit`](super::Source::edit). +pub fn reparse( + root: &mut SyntaxNode, + text: &str, + replaced: Range<usize>, + replacement_len: usize, +) -> Range<usize> { + try_reparse(text, replaced, replacement_len, None, root, 0).unwrap_or_else(|| { + let id = root.span().id(); + *root = parse(text); + root.numberize(id, Span::FULL).unwrap(); + 0..text.len() + }) +} + +/// Try to reparse inside the given node. +fn try_reparse( + text: &str, + replaced: Range<usize>, + replacement_len: usize, + parent_kind: Option<SyntaxKind>, + node: &mut SyntaxNode, + offset: usize, +) -> Option<Range<usize>> { + // The range of children which overlap with the edit. + #[allow(clippy::reversed_empty_ranges)] + let mut overlap = usize::MAX..0; + let mut cursor = offset; + let node_kind = node.kind(); + + for (i, child) in node.children_mut().iter_mut().enumerate() { + let prev_range = cursor..cursor + child.len(); + let prev_len = child.len(); + let prev_desc = child.descendants(); + + // Does the child surround the edit? + // If so, try to reparse within it or itself. + if !child.is_leaf() && includes(&prev_range, &replaced) { + let new_len = prev_len + replacement_len - replaced.len(); + let new_range = cursor..cursor + new_len; + + // Try to reparse within the child. + if let Some(range) = try_reparse( + text, + replaced.clone(), + replacement_len, + Some(node_kind), + child, + cursor, + ) { + assert_eq!(child.len(), new_len); + let new_desc = child.descendants(); + node.update_parent(prev_len, new_len, prev_desc, new_desc); + return Some(range); + } + + // If the child is a block, try to reparse the block. + if child.kind().is_block() { + if let Some(newborn) = reparse_block(text, new_range.clone()) { + return node + .replace_children(i..i + 1, vec![newborn]) + .is_ok() + .then_some(new_range); + } + } + } + + // Does the child overlap with the edit? + if overlaps(&prev_range, &replaced) { + overlap.start = overlap.start.min(i); + overlap.end = i + 1; + } + + // Is the child beyond the edit? + if replaced.end < cursor { + break; + } + + cursor += child.len(); + } + + // Try to reparse a range of markup expressions within markup. This is only + // possible if the markup is top-level or contained in a block, not if it is + // contained in things like headings or lists because too much can go wrong + // with indent and line breaks. + if overlap.is_empty() + || node.kind() != SyntaxKind::Markup + || !matches!(parent_kind, None | Some(SyntaxKind::ContentBlock)) + { + return None; + } + + let children = node.children_mut(); + + // Reparse a segment. Retries until it works, taking exponentially more + // children into account. + let mut expansion = 1; + loop { + // Add slack in both directions. + let mut start = overlap.start.saturating_sub(expansion.max(2)); + let mut end = (overlap.end + expansion).min(children.len()); + + // Expand to the left. + while start > 0 && expand(&children[start]) { + start -= 1; + } + + // Expand to the right. + while end < children.len() && expand(&children[end]) { + end += 1; + } + + // Also take hashtag. + if start > 0 && children[start - 1].kind() == SyntaxKind::Hashtag { + start -= 1; + } + + // Synthesize what `at_start` and `nesting` would be at the start of the + // reparse. + let mut prefix_len = 0; + let mut nesting = 0; + let mut at_start = true; + for child in &children[..start] { + prefix_len += child.len(); + next_at_start(child, &mut at_start); + next_nesting(child, &mut nesting); + } + + // Determine what `at_start` will have to be at the end of the reparse. + let mut prev_len = 0; + let mut prev_at_start_after = at_start; + let mut prev_nesting_after = nesting; + for child in &children[start..end] { + prev_len += child.len(); + next_at_start(child, &mut prev_at_start_after); + next_nesting(child, &mut prev_nesting_after); + } + + // Determine the range in the new text that we want to reparse. + let shifted = offset + prefix_len; + let new_len = prev_len + replacement_len - replaced.len(); + let new_range = shifted..shifted + new_len; + let at_end = end == children.len(); + + // Stop parsing early if this kind is encountered. + let stop_kind = match parent_kind { + Some(_) => SyntaxKind::RightBracket, + None => SyntaxKind::Eof, + }; + + // Reparse! + let reparsed = reparse_markup( + text, + new_range.clone(), + &mut at_start, + &mut nesting, + |kind| kind == stop_kind, + ); + + if let Some(newborns) = reparsed { + // If more children follow, at_start must match its previous value. + // Similarly, if we children follow or we not top-level the nesting + // must match its previous value. + if (at_end || at_start == prev_at_start_after) + && ((at_end && parent_kind.is_none()) || nesting == prev_nesting_after) + { + return node + .replace_children(start..end, newborns) + .is_ok() + .then_some(new_range); + } + } + + // If it didn't even work with all children, we give up. + if start == 0 && at_end { + break; + } + + // Exponential expansion to both sides. + expansion *= 2; + } + + None +} + +/// Whether the inner range is fully contained in the outer one (no touching). +fn includes(outer: &Range<usize>, inner: &Range<usize>) -> bool { + outer.start < inner.start && outer.end > inner.end +} + +/// Whether the first and second range overlap or touch. +fn overlaps(first: &Range<usize>, second: &Range<usize>) -> bool { + (first.start <= second.start && second.start <= first.end) + || (second.start <= first.start && first.start <= second.end) +} + +/// Whether the selection should be expanded beyond a node of this kind. +fn expand(node: &SyntaxNode) -> bool { + let kind = node.kind(); + kind.is_trivia() + || kind.is_error() + || kind == SyntaxKind::Semicolon + || node.text() == "/" + || node.text() == ":" +} + +/// Whether `at_start` would still be true after this node given the +/// previous value of the property. +fn next_at_start(node: &SyntaxNode, at_start: &mut bool) { + let kind = node.kind(); + if kind.is_trivia() { + *at_start |= kind == SyntaxKind::Parbreak + || (kind == SyntaxKind::Space && node.text().chars().any(is_newline)); + } else { + *at_start = false; + } +} + +/// Update `nesting` based on the node. +fn next_nesting(node: &SyntaxNode, nesting: &mut usize) { + if node.kind() == SyntaxKind::Text { + match node.text().as_str() { + "[" => *nesting += 1, + "]" if *nesting > 0 => *nesting -= 1, + _ => {} + } + } +} + +#[cfg(test)] +mod tests { + use std::ops::Range; + + use super::super::{parse, Source, Span}; + + #[track_caller] + fn test(prev: &str, range: Range<usize>, with: &str, incremental: bool) { + let mut source = Source::detached(prev); + let prev = source.root().clone(); + let range = source.edit(range, with); + let mut found = source.root().clone(); + let mut expected = parse(source.text()); + found.synthesize(Span::detached()); + expected.synthesize(Span::detached()); + if found != expected { + eprintln!("source: {:?}", source.text()); + eprintln!("previous: {prev:#?}"); + eprintln!("expected: {expected:#?}"); + eprintln!("found: {found:#?}"); + panic!("test failed"); + } + if incremental { + assert_ne!(source.len_bytes(), range.len(), "should have been incremental"); + } else { + assert_eq!( + source.len_bytes(), + range.len(), + "shouldn't have been incremental" + ); + } + } + + #[test] + fn test_reparse_markup() { + test("abc~def~gh~", 5..6, "+", true); + test("~~~~~~~", 3..4, "A", true); + test("abc~~", 1..2, "", true); + test("#var. hello", 5..6, " ", false); + test("#var;hello", 9..10, "a", false); + test("https:/world", 7..7, "/", false); + test("hello world", 7..12, "walkers", false); + test("some content", 0..12, "", false); + test("", 0..0, "do it", false); + test("a d e", 1..3, " b c d", false); + test("~*~*~", 2..2, "*", false); + test("::1\n2. a\n3", 7..7, "4", true); + test("* #{1+2} *", 6..7, "3", true); + test("#{(0, 1, 2)}", 6..7, "11pt", true); + test("\n= A heading", 4..4, "n evocative", false); + test("#call() abc~d", 7..7, "[]", true); + test("a your thing a", 6..7, "a", false); + test("#grid(columns: (auto, 1fr, 40%))", 16..20, "4pt", false); + test("abc\n= a heading\njoke", 3..4, "\nmore\n\n", true); + test("#show f: a => b..", 16..16, "c", false); + test("#for", 4..4, "//", false); + test("a\n#let \nb", 7..7, "i", true); + test(r"#{{let x = z}; a = 1} b", 7..7, "//", false); + test(r#"a ```typst hello```"#, 16..17, "", false); + } + + #[test] + fn test_reparse_block() { + test("Hello #{ x + 1 }!", 9..10, "abc", true); + test("A#{}!", 3..3, "\"", false); + test("#{ [= x] }!", 5..5, "=", true); + test("#[[]]", 3..3, "\\", true); + test("#[[ab]]", 4..5, "\\", true); + test("#{}}", 2..2, "{", false); + test("A: #[BC]", 6..6, "{", true); + test("A: #[BC]", 6..6, "#{", true); + test("A: #[BC]", 6..6, "#{}", true); + test("#{\"ab\"}A", 5..5, "c", true); + test("#{\"ab\"}A", 5..6, "c", false); + test("a#[]b", 3..3, "#{", true); + test("a#{call(); abc}b", 8..8, "[]", true); + test("a #while x {\n g(x) \n} b", 12..12, "//", true); + test("a#[]b", 3..3, "[hey]", true); + } +} diff --git a/crates/typst/src/syntax/source.rs b/crates/typst/src/syntax/source.rs new file mode 100644 index 00000000..2f3e4144 --- /dev/null +++ b/crates/typst/src/syntax/source.rs @@ -0,0 +1,421 @@ +//! Source file management. + +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::ops::Range; +use std::sync::Arc; + +use comemo::Prehashed; + +use super::ast::Markup; +use super::reparser::reparse; +use super::{is_newline, parse, LinkedNode, Span, SyntaxNode}; +use crate::diag::SourceResult; +use crate::file::FileId; +use crate::util::StrExt; + +/// A source file. +/// +/// All line and column indices start at zero, just like byte indices. Only for +/// user-facing display, you should add 1 to them. +/// +/// Values of this type are cheap to clone and hash. +#[derive(Clone)] +pub struct Source(Arc<Repr>); + +/// The internal representation. +#[derive(Clone)] +struct Repr { + id: FileId, + text: Prehashed<String>, + root: Prehashed<SyntaxNode>, + lines: Vec<Line>, +} + +impl Source { + /// Create a new source file. + #[tracing::instrument(skip_all)] + pub fn new(id: FileId, text: String) -> Self { + let mut root = parse(&text); + root.numberize(id, Span::FULL).unwrap(); + Self(Arc::new(Repr { + id, + lines: lines(&text), + text: Prehashed::new(text), + root: Prehashed::new(root), + })) + } + + /// Create a source file without a real id and path, usually for testing. + pub fn detached(text: impl Into<String>) -> Self { + Self::new(FileId::detached(), text.into()) + } + + /// Create a source file with the same synthetic span for all nodes. + pub fn synthesized(text: String, span: Span) -> Self { + let mut root = parse(&text); + root.synthesize(span); + Self(Arc::new(Repr { + id: FileId::detached(), + lines: lines(&text), + text: Prehashed::new(text), + root: Prehashed::new(root), + })) + } + + /// The root node of the file's untyped syntax tree. + pub fn root(&self) -> &SyntaxNode { + &self.0.root + } + + /// The root node of the file's typed abstract syntax tree. + pub fn ast(&self) -> SourceResult<Markup> { + let errors = self.root().errors(); + if errors.is_empty() { + Ok(self.root().cast().expect("root node must be markup")) + } else { + Err(Box::new(errors)) + } + } + + /// The id of the source file. + pub fn id(&self) -> FileId { + self.0.id + } + + /// The whole source as a string slice. + pub fn text(&self) -> &str { + &self.0.text + } + + /// Slice out the part of the source code enclosed by the range. + pub fn get(&self, range: Range<usize>) -> Option<&str> { + self.text().get(range) + } + + /// Fully replace the source text. + pub fn replace(&mut self, text: String) { + let inner = Arc::make_mut(&mut self.0); + inner.text = Prehashed::new(text); + inner.lines = lines(&inner.text); + let mut root = parse(&inner.text); + root.numberize(inner.id, Span::FULL).unwrap(); + inner.root = Prehashed::new(root); + } + + /// Edit the source file by replacing the given range. + /// + /// Returns the range in the new source that was ultimately reparsed. + /// + /// The method panics if the `replace` range is out of bounds. + #[track_caller] + pub fn edit(&mut self, replace: Range<usize>, with: &str) -> Range<usize> { + let start_byte = replace.start; + let start_utf16 = self.byte_to_utf16(start_byte).unwrap(); + let line = self.byte_to_line(start_byte).unwrap(); + + let inner = Arc::make_mut(&mut self.0); + + // Update the text itself. + inner.text.update(|text| text.replace_range(replace.clone(), with)); + + // Remove invalidated line starts. + inner.lines.truncate(line + 1); + + // Handle adjoining of \r and \n. + if inner.text[..start_byte].ends_with('\r') && with.starts_with('\n') { + inner.lines.pop(); + } + + // Recalculate the line starts after the edit. + inner.lines.extend(lines_from( + start_byte, + start_utf16, + &inner.text[start_byte..], + )); + + // Incrementally reparse the replaced range. + inner + .root + .update(|root| reparse(root, &inner.text, replace, with.len())) + } + + /// Get the length of the file in UTF-8 encoded bytes. + pub fn len_bytes(&self) -> usize { + self.text().len() + } + + /// Get the length of the file in UTF-16 code units. + pub fn len_utf16(&self) -> usize { + let last = self.0.lines.last().unwrap(); + last.utf16_idx + self.0.text[last.byte_idx..].len_utf16() + } + + /// Get the length of the file in lines. + pub fn len_lines(&self) -> usize { + self.0.lines.len() + } + + /// Find the node with the given span. + /// + /// Returns `None` if the span does not point into this source file. + pub fn find(&self, span: Span) -> Option<LinkedNode<'_>> { + LinkedNode::new(self.root()).find(span) + } + + /// Return the index of the UTF-16 code unit at the byte index. + pub fn byte_to_utf16(&self, byte_idx: usize) -> Option<usize> { + let line_idx = self.byte_to_line(byte_idx)?; + let line = self.0.lines.get(line_idx)?; + let head = self.0.text.get(line.byte_idx..byte_idx)?; + Some(line.utf16_idx + head.len_utf16()) + } + + /// Return the index of the line that contains the given byte index. + pub fn byte_to_line(&self, byte_idx: usize) -> Option<usize> { + (byte_idx <= self.0.text.len()).then(|| { + match self.0.lines.binary_search_by_key(&byte_idx, |line| line.byte_idx) { + Ok(i) => i, + Err(i) => i - 1, + } + }) + } + + /// Return the index of the column at the byte index. + /// + /// The column is defined as the number of characters in the line before the + /// byte index. + pub fn byte_to_column(&self, byte_idx: usize) -> Option<usize> { + let line = self.byte_to_line(byte_idx)?; + let start = self.line_to_byte(line)?; + let head = self.get(start..byte_idx)?; + Some(head.chars().count()) + } + + /// Return the byte index at the UTF-16 code unit. + pub fn utf16_to_byte(&self, utf16_idx: usize) -> Option<usize> { + let line = self.0.lines.get( + match self.0.lines.binary_search_by_key(&utf16_idx, |line| line.utf16_idx) { + Ok(i) => i, + Err(i) => i - 1, + }, + )?; + + let mut k = line.utf16_idx; + for (i, c) in self.0.text[line.byte_idx..].char_indices() { + if k >= utf16_idx { + return Some(line.byte_idx + i); + } + k += c.len_utf16(); + } + + (k == utf16_idx).then_some(self.0.text.len()) + } + + /// Return the byte position at which the given line starts. + pub fn line_to_byte(&self, line_idx: usize) -> Option<usize> { + self.0.lines.get(line_idx).map(|line| line.byte_idx) + } + + /// Return the range which encloses the given line. + pub fn line_to_range(&self, line_idx: usize) -> Option<Range<usize>> { + let start = self.line_to_byte(line_idx)?; + let end = self.line_to_byte(line_idx + 1).unwrap_or(self.0.text.len()); + Some(start..end) + } + + /// Return the byte index of the given (line, column) pair. + /// + /// The column defines the number of characters to go beyond the start of + /// the line. + pub fn line_column_to_byte( + &self, + line_idx: usize, + column_idx: usize, + ) -> Option<usize> { + let range = self.line_to_range(line_idx)?; + let line = self.get(range.clone())?; + let mut chars = line.chars(); + for _ in 0..column_idx { + chars.next(); + } + Some(range.start + (line.len() - chars.as_str().len())) + } +} + +impl Debug for Source { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Source({})", self.id().path().display()) + } +} + +impl Hash for Source { + fn hash<H: Hasher>(&self, state: &mut H) { + self.0.id.hash(state); + self.0.text.hash(state); + self.0.root.hash(state); + } +} + +impl AsRef<str> for Source { + fn as_ref(&self) -> &str { + self.text() + } +} + +/// Metadata about a line. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +struct Line { + /// The UTF-8 byte offset where the line starts. + byte_idx: usize, + /// The UTF-16 codepoint offset where the line starts. + utf16_idx: usize, +} + +/// Create a line vector. +fn lines(text: &str) -> Vec<Line> { + std::iter::once(Line { byte_idx: 0, utf16_idx: 0 }) + .chain(lines_from(0, 0, text)) + .collect() +} + +/// Compute a line iterator from an offset. +fn lines_from( + byte_offset: usize, + utf16_offset: usize, + text: &str, +) -> impl Iterator<Item = Line> + '_ { + let mut s = unscanny::Scanner::new(text); + let mut utf16_idx = utf16_offset; + + std::iter::from_fn(move || { + s.eat_until(|c: char| { + utf16_idx += c.len_utf16(); + is_newline(c) + }); + + if s.done() { + return None; + } + + if s.eat() == Some('\r') && s.eat_if('\n') { + utf16_idx += 1; + } + + Some(Line { byte_idx: byte_offset + s.cursor(), utf16_idx }) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + const TEST: &str = "ä\tcde\nf💛g\r\nhi\rjkl"; + + #[test] + fn test_source_file_new() { + let source = Source::detached(TEST); + assert_eq!( + source.0.lines, + [ + Line { byte_idx: 0, utf16_idx: 0 }, + Line { byte_idx: 7, utf16_idx: 6 }, + Line { byte_idx: 15, utf16_idx: 12 }, + Line { byte_idx: 18, utf16_idx: 15 }, + ] + ); + } + + #[test] + fn test_source_file_pos_to_line() { + let source = Source::detached(TEST); + assert_eq!(source.byte_to_line(0), Some(0)); + assert_eq!(source.byte_to_line(2), Some(0)); + assert_eq!(source.byte_to_line(6), Some(0)); + assert_eq!(source.byte_to_line(7), Some(1)); + assert_eq!(source.byte_to_line(8), Some(1)); + assert_eq!(source.byte_to_line(12), Some(1)); + assert_eq!(source.byte_to_line(21), Some(3)); + assert_eq!(source.byte_to_line(22), None); + } + + #[test] + fn test_source_file_pos_to_column() { + let source = Source::detached(TEST); + assert_eq!(source.byte_to_column(0), Some(0)); + assert_eq!(source.byte_to_column(2), Some(1)); + assert_eq!(source.byte_to_column(6), Some(5)); + assert_eq!(source.byte_to_column(7), Some(0)); + assert_eq!(source.byte_to_column(8), Some(1)); + assert_eq!(source.byte_to_column(12), Some(2)); + } + + #[test] + fn test_source_file_utf16() { + #[track_caller] + fn roundtrip(source: &Source, byte_idx: usize, utf16_idx: usize) { + let middle = source.byte_to_utf16(byte_idx).unwrap(); + let result = source.utf16_to_byte(middle).unwrap(); + assert_eq!(middle, utf16_idx); + assert_eq!(result, byte_idx); + } + + let source = Source::detached(TEST); + roundtrip(&source, 0, 0); + roundtrip(&source, 2, 1); + roundtrip(&source, 3, 2); + roundtrip(&source, 8, 7); + roundtrip(&source, 12, 9); + roundtrip(&source, 21, 18); + assert_eq!(source.byte_to_utf16(22), None); + assert_eq!(source.utf16_to_byte(19), None); + } + + #[test] + fn test_source_file_roundtrip() { + #[track_caller] + fn roundtrip(source: &Source, byte_idx: usize) { + let line = source.byte_to_line(byte_idx).unwrap(); + let column = source.byte_to_column(byte_idx).unwrap(); + let result = source.line_column_to_byte(line, column).unwrap(); + assert_eq!(result, byte_idx); + } + + let source = Source::detached(TEST); + roundtrip(&source, 0); + roundtrip(&source, 7); + roundtrip(&source, 12); + roundtrip(&source, 21); + } + + #[test] + fn test_source_file_edit() { + // This tests only the non-parser parts. The reparsing itself is + // tested separately. + #[track_caller] + fn test(prev: &str, range: Range<usize>, with: &str, after: &str) { + let mut source = Source::detached(prev); + let result = Source::detached(after); + source.edit(range, with); + assert_eq!(source.text(), result.text()); + assert_eq!(source.0.lines, result.0.lines); + } + + // Test inserting at the beginning. + test("abc\n", 0..0, "hi\n", "hi\nabc\n"); + test("\nabc", 0..0, "hi\r", "hi\r\nabc"); + + // Test editing in the middle. + test(TEST, 4..16, "❌", "ä\tc❌i\rjkl"); + + // Test appending. + test("abc\ndef", 7..7, "hi", "abc\ndefhi"); + test("abc\ndef\n", 8..8, "hi", "abc\ndef\nhi"); + + // Test appending with adjoining \r and \n. + test("abc\ndef\r", 8..8, "\nghi", "abc\ndef\r\nghi"); + + // Test removing everything. + test(TEST, 0..21, "", ""); + } +} diff --git a/crates/typst/src/syntax/span.rs b/crates/typst/src/syntax/span.rs new file mode 100644 index 00000000..5c220252 --- /dev/null +++ b/crates/typst/src/syntax/span.rs @@ -0,0 +1,148 @@ +use std::fmt::{self, Debug, Formatter}; +use std::num::NonZeroU64; +use std::ops::Range; + +use super::Source; +use crate::file::FileId; +use crate::World; + +/// A unique identifier for a syntax node. +/// +/// This is used throughout the compiler to track which source section an error +/// or element stems from. Can be [mapped back](Self::range) to a byte range for +/// user facing display. +/// +/// During editing, the span values stay mostly stable, even for nodes behind an +/// insertion. This is not true for simple ranges as they would shift. Spans can +/// be used as inputs to memoized functions without hurting cache performance +/// when text is inserted somewhere in the document other than the end. +/// +/// Span ids are ordered in the syntax tree to enable quickly finding the node +/// with some id: +/// - The id of a parent is always smaller than the ids of any of its children. +/// - The id of a node is always greater than any id in the subtrees of any left +/// sibling and smaller than any id in the subtrees of any right sibling. +/// +/// This type takes up 8 bytes and is null-optimized (i.e. `Option<Span>` also +/// takes 8 bytes). +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Span(NonZeroU64); + +impl Span { + /// The full range of numbers available for span numbering. + pub const FULL: Range<u64> = 2..(1 << Self::BITS); + const DETACHED: u64 = 1; + + // Data layout: + // | 16 bits source id | 48 bits number | + const BITS: usize = 48; + + /// Create a new span from a source id and a unique number. + /// + /// Panics if the `number` is not contained in `FULL`. + #[track_caller] + pub const fn new(id: FileId, number: u64) -> Self { + assert!( + Self::FULL.start <= number && number < Self::FULL.end, + "span number outside valid range" + ); + + Self::pack(id, number) + } + + /// A span that does not point into any source file. + pub const fn detached() -> Self { + Self::pack(FileId::detached(), Self::DETACHED) + } + + /// Pack the components into a span. + #[track_caller] + const fn pack(id: FileId, number: u64) -> Span { + let bits = ((id.as_u16() as u64) << Self::BITS) | number; + match NonZeroU64::new(bits) { + Some(v) => Self(v), + None => panic!("span encoding is zero"), + } + } + + /// The id of the source file the span points into. + pub const fn id(self) -> FileId { + FileId::from_u16((self.0.get() >> Self::BITS) as u16) + } + + /// The unique number of the span within its source file. + pub const fn number(self) -> u64 { + self.0.get() & ((1 << Self::BITS) - 1) + } + + /// Whether the span is detached. + pub const fn is_detached(self) -> bool { + self.id().is_detached() + } + + /// Get the byte range for this span. + #[track_caller] + pub fn range(self, world: &dyn World) -> Range<usize> { + let source = world + .source(self.id()) + .expect("span does not point into any source file"); + self.range_in(&source) + } + + /// Get the byte range for this span in the given source file. + #[track_caller] + pub fn range_in(self, source: &Source) -> Range<usize> { + source + .find(self) + .expect("span does not point into this source file") + .range() + } +} + +/// A value with a span locating it in the source code. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct Spanned<T> { + /// The spanned value. + pub v: T, + /// The value's location in source code. + pub span: Span, +} + +impl<T> Spanned<T> { + /// Create a new instance from a value and its span. + pub fn new(v: T, span: Span) -> Self { + Self { v, span } + } + + /// Convert from `&Spanned<T>` to `Spanned<&T>` + pub fn as_ref(&self) -> Spanned<&T> { + Spanned { v: &self.v, span: self.span } + } + + /// Map the value using a function. + pub fn map<F, U>(self, f: F) -> Spanned<U> + where + F: FnOnce(T) -> U, + { + Spanned { v: f(self.v), span: self.span } + } +} + +impl<T: Debug> Debug for Spanned<T> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.v.fmt(f) + } +} + +#[cfg(test)] +mod tests { + use super::{FileId, Span}; + + #[test] + fn test_span_encoding() { + let id = FileId::from_u16(5); + let span = Span::new(id, 10); + assert_eq!(span.id(), id); + assert_eq!(span.number(), 10); + } +} diff --git a/crates/typst/src/util/bytes.rs b/crates/typst/src/util/bytes.rs new file mode 100644 index 00000000..9165467b --- /dev/null +++ b/crates/typst/src/util/bytes.rs @@ -0,0 +1,59 @@ +use std::borrow::Cow; +use std::fmt::{self, Debug, Formatter}; +use std::ops::Deref; +use std::sync::Arc; + +use comemo::Prehashed; + +/// A shared byte buffer that is cheap to clone and hash. +#[derive(Clone, Hash, Eq, PartialEq)] +pub struct Bytes(Arc<Prehashed<Cow<'static, [u8]>>>); + +impl Bytes { + /// Create a buffer from a static byte slice. + pub fn from_static(slice: &'static [u8]) -> Self { + Self(Arc::new(Prehashed::new(Cow::Borrowed(slice)))) + } + + /// Return a view into the buffer. + pub fn as_slice(&self) -> &[u8] { + self + } + + /// Return a copy of the buffer as a vector. + pub fn to_vec(&self) -> Vec<u8> { + self.0.to_vec() + } +} + +impl From<&[u8]> for Bytes { + fn from(slice: &[u8]) -> Self { + Self(Arc::new(Prehashed::new(slice.to_vec().into()))) + } +} + +impl From<Vec<u8>> for Bytes { + fn from(vec: Vec<u8>) -> Self { + Self(Arc::new(Prehashed::new(vec.into()))) + } +} + +impl Deref for Bytes { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef<[u8]> for Bytes { + fn as_ref(&self) -> &[u8] { + self + } +} + +impl Debug for Bytes { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "bytes({})", self.len()) + } +} diff --git a/crates/typst/src/util/fat.rs b/crates/typst/src/util/fat.rs new file mode 100644 index 00000000..d3c9bb20 --- /dev/null +++ b/crates/typst/src/util/fat.rs @@ -0,0 +1,55 @@ +//! Fat pointer handling. +//! +//! This assumes the memory representation of fat pointers. Although it is not +//! guaranteed by Rust, it's improbable that it will change. Still, when the +//! pointer metadata APIs are stable, we should definitely move to them: +//! <https://github.com/rust-lang/rust/issues/81513> + +use std::alloc::Layout; +use std::mem; + +/// Create a fat pointer from a data address and a vtable address. +/// +/// # Safety +/// Must only be called when `T` is a `dyn Trait`. The data address must point +/// to a value whose type implements the trait of `T` and the `vtable` must have +/// been extracted with [`vtable`]. +#[track_caller] +pub unsafe fn from_raw_parts<T: ?Sized>(data: *const (), vtable: *const ()) -> *const T { + let fat = FatPointer { data, vtable }; + debug_assert_eq!(Layout::new::<*const T>(), Layout::new::<FatPointer>()); + mem::transmute_copy::<FatPointer, *const T>(&fat) +} + +/// Create a mutable fat pointer from a data address and a vtable address. +/// +/// # Safety +/// Must only be called when `T` is a `dyn Trait`. The data address must point +/// to a value whose type implements the trait of `T` and the `vtable` must have +/// been extracted with [`vtable`]. +#[track_caller] +pub unsafe fn from_raw_parts_mut<T: ?Sized>(data: *mut (), vtable: *const ()) -> *mut T { + let fat = FatPointer { data, vtable }; + debug_assert_eq!(Layout::new::<*mut T>(), Layout::new::<FatPointer>()); + mem::transmute_copy::<FatPointer, *mut T>(&fat) +} + +/// Extract the address to a trait object's vtable. +/// +/// # Safety +/// Must only be called when `T` is a `dyn Trait`. +#[track_caller] +pub unsafe fn vtable<T: ?Sized>(ptr: *const T) -> *const () { + debug_assert_eq!(Layout::new::<*const T>(), Layout::new::<FatPointer>()); + mem::transmute_copy::<*const T, FatPointer>(&ptr).vtable +} + +/// The memory representation of a trait object pointer. +/// +/// Although this is not guaranteed by Rust, it's improbable that it will +/// change. +#[repr(C)] +struct FatPointer { + data: *const (), + vtable: *const (), +} diff --git a/crates/typst/src/util/mod.rs b/crates/typst/src/util/mod.rs new file mode 100644 index 00000000..05914b04 --- /dev/null +++ b/crates/typst/src/util/mod.rs @@ -0,0 +1,268 @@ +//! Utilities. + +pub mod fat; + +mod bytes; + +pub use bytes::Bytes; + +use std::fmt::{self, Debug, Formatter}; +use std::hash::Hash; +use std::num::NonZeroUsize; +use std::path::{Component, Path, PathBuf}; +use std::sync::Arc; + +use siphasher::sip128::{Hasher128, SipHasher13}; + +/// Turn a closure into a struct implementing [`Debug`]. +pub fn debug<F>(f: F) -> impl Debug +where + F: Fn(&mut Formatter) -> fmt::Result, +{ + struct Wrapper<F>(F); + + impl<F> Debug for Wrapper<F> + where + F: Fn(&mut Formatter) -> fmt::Result, + { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.0(f) + } + } + + Wrapper(f) +} + +/// Calculate a 128-bit siphash of a value. +pub fn hash128<T: Hash + ?Sized>(value: &T) -> u128 { + let mut state = SipHasher13::new(); + value.hash(&mut state); + state.finish128().as_u128() +} + +/// An extra constant for [`NonZeroUsize`]. +pub trait NonZeroExt { + /// The number `1`. + const ONE: Self; +} + +impl NonZeroExt for NonZeroUsize { + const ONE: Self = match Self::new(1) { + Some(v) => v, + None => unreachable!(), + }; +} + +/// Extra methods for [`str`]. +pub trait StrExt { + /// The number of code units this string would use if it was encoded in + /// UTF16. This runs in linear time. + fn len_utf16(&self) -> usize; +} + +impl StrExt for str { + fn len_utf16(&self) -> usize { + self.chars().map(char::len_utf16).sum() + } +} + +/// Extra methods for [`Arc`]. +pub trait ArcExt<T> { + /// Takes the inner value if there is exactly one strong reference and + /// clones it otherwise. + fn take(self) -> T; +} + +impl<T: Clone> ArcExt<T> for Arc<T> { + fn take(self) -> T { + match Arc::try_unwrap(self) { + Ok(v) => v, + Err(rc) => (*rc).clone(), + } + } +} + +/// Extra methods for [`[T]`](slice). +pub trait SliceExt<T> { + /// Split a slice into consecutive runs with the same key and yield for + /// each such run the key and the slice of elements with that key. + fn group_by_key<K, F>(&self, f: F) -> GroupByKey<'_, T, F> + where + F: FnMut(&T) -> K, + K: PartialEq; +} + +impl<T> SliceExt<T> for [T] { + fn group_by_key<K, F>(&self, f: F) -> GroupByKey<'_, T, F> { + GroupByKey { slice: self, f } + } +} + +/// This struct is created by [`SliceExt::group_by_key`]. +pub struct GroupByKey<'a, T, F> { + slice: &'a [T], + f: F, +} + +impl<'a, T, K, F> Iterator for GroupByKey<'a, T, F> +where + F: FnMut(&T) -> K, + K: PartialEq, +{ + type Item = (K, &'a [T]); + + fn next(&mut self) -> Option<Self::Item> { + let mut iter = self.slice.iter(); + let key = (self.f)(iter.next()?); + let count = 1 + iter.take_while(|t| (self.f)(t) == key).count(); + let (head, tail) = self.slice.split_at(count); + self.slice = tail; + Some((key, head)) + } +} + +/// Extra methods for [`Path`]. +pub trait PathExt { + /// Lexically normalize a path. + fn normalize(&self) -> PathBuf; + + /// Treat `self` as a virtual root relative to which the `path` is resolved. + /// + /// Returns `None` if the path lexically escapes the root. The path + /// might still escape through symlinks. + fn join_rooted(&self, path: &Path) -> Option<PathBuf>; +} + +impl PathExt for Path { + fn normalize(&self) -> PathBuf { + let mut out = PathBuf::new(); + for component in self.components() { + match component { + Component::CurDir => {} + Component::ParentDir => match out.components().next_back() { + Some(Component::Normal(_)) => { + out.pop(); + } + _ => out.push(component), + }, + Component::Prefix(_) | Component::RootDir | Component::Normal(_) => { + out.push(component) + } + } + } + if out.as_os_str().is_empty() { + out.push(Component::CurDir); + } + out + } + + fn join_rooted(&self, path: &Path) -> Option<PathBuf> { + let mut parts: Vec<_> = self.components().collect(); + let root = parts.len(); + for component in path.components() { + match component { + Component::Prefix(_) => return None, + Component::RootDir => parts.truncate(root), + Component::CurDir => {} + Component::ParentDir => { + if parts.len() <= root { + return None; + } + parts.pop(); + } + Component::Normal(_) => parts.push(component), + } + } + if parts.len() < root { + return None; + } + Some(parts.into_iter().collect()) + } +} + +/// Format pieces separated with commas and a final "and" or "or". +pub fn separated_list(pieces: &[impl AsRef<str>], last: &str) -> String { + let mut buf = String::new(); + for (i, part) in pieces.iter().enumerate() { + match i { + 0 => {} + 1 if pieces.len() == 2 => { + buf.push(' '); + buf.push_str(last); + buf.push(' '); + } + i if i + 1 == pieces.len() => { + buf.push_str(", "); + buf.push_str(last); + buf.push(' '); + } + _ => buf.push_str(", "), + } + buf.push_str(part.as_ref()); + } + buf +} + +/// Format a comma-separated list. +/// +/// Tries to format horizontally, but falls back to vertical formatting if the +/// pieces are too long. +pub fn pretty_comma_list(pieces: &[impl AsRef<str>], trailing_comma: bool) -> String { + const MAX_WIDTH: usize = 50; + + let mut buf = String::new(); + let len = pieces.iter().map(|s| s.as_ref().len()).sum::<usize>() + + 2 * pieces.len().saturating_sub(1); + + if len <= MAX_WIDTH { + for (i, piece) in pieces.iter().enumerate() { + if i > 0 { + buf.push_str(", "); + } + buf.push_str(piece.as_ref()); + } + if trailing_comma { + buf.push(','); + } + } else { + for piece in pieces { + buf.push_str(piece.as_ref().trim()); + buf.push_str(",\n"); + } + } + + buf +} + +/// Format an array-like construct. +/// +/// Tries to format horizontally, but falls back to vertical formatting if the +/// pieces are too long. +pub fn pretty_array_like(parts: &[impl AsRef<str>], trailing_comma: bool) -> String { + let list = pretty_comma_list(parts, trailing_comma); + let mut buf = String::new(); + buf.push('('); + if list.contains('\n') { + buf.push('\n'); + for (i, line) in list.lines().enumerate() { + if i > 0 { + buf.push('\n'); + } + buf.push_str(" "); + buf.push_str(line); + } + buf.push('\n'); + } else { + buf.push_str(&list); + } + buf.push(')'); + buf +} + +/// Check if the [`Option`]-wrapped L is same to R. +pub fn option_eq<L, R>(left: Option<L>, other: R) -> bool +where + L: PartialEq<R>, +{ + left.map(|v| v == other).unwrap_or(false) +} |
