From 37a7afddfaffd44cb9bc013c9506599267e08983 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 3 Nov 2022 11:44:53 +0100 Subject: Split crates --- Cargo.lock | 61 +- Cargo.toml | 88 +-- benches/oneshot.rs | 18 +- cli/Cargo.toml | 26 + cli/src/main.rs | 681 +++++++++++++++++++ library/Cargo.toml | 26 + library/src/ext.rs | 181 ++++++ library/src/graphics/hide.rs | 27 + library/src/graphics/image.rs | 121 ++++ library/src/graphics/line.rs | 68 ++ library/src/graphics/mod.rs | 11 + library/src/graphics/shape.rs | 201 ++++++ library/src/layout/align.rs | 62 ++ library/src/layout/columns.rs | 117 ++++ library/src/layout/container.rs | 80 +++ library/src/layout/flow.rs | 267 ++++++++ library/src/layout/grid.rs | 600 +++++++++++++++++ library/src/layout/mod.rs | 835 ++++++++++++++++++++++++ library/src/layout/pad.rs | 83 +++ library/src/layout/page.rs | 421 ++++++++++++ library/src/layout/place.rs | 56 ++ library/src/layout/spacing.rs | 100 +++ library/src/layout/stack.rs | 322 +++++++++ library/src/layout/transform.rs | 117 ++++ library/src/lib.rs | 191 ++++++ library/src/math/mod.rs | 188 ++++++ library/src/math/tex.rs | 164 +++++ library/src/prelude.rs | 27 + library/src/structure/doc.rs | 29 + library/src/structure/heading.rs | 176 +++++ library/src/structure/list.rs | 371 +++++++++++ library/src/structure/mod.rs | 13 + library/src/structure/reference.rs | 29 + library/src/structure/table.rs | 160 +++++ library/src/text/deco.rs | 248 +++++++ library/src/text/link.rs | 102 +++ library/src/text/mod.rs | 555 ++++++++++++++++ library/src/text/par.rs | 1259 +++++++++++++++++++++++++++++++++++ library/src/text/quotes.rs | 149 +++++ library/src/text/raw.rs | 206 ++++++ library/src/text/shaping.rs | 655 +++++++++++++++++++ library/src/text/shift.rs | 188 ++++++ library/src/utility/color.rs | 66 ++ library/src/utility/data.rs | 132 ++++ library/src/utility/math.rs | 144 ++++ library/src/utility/mod.rs | 54 ++ library/src/utility/string.rs | 141 ++++ macros/src/lib.rs | 20 +- src/diag.rs | 18 +- src/export/pdf/font.rs | 2 +- src/export/pdf/outline.rs | 2 +- src/export/pdf/page.rs | 1 + src/export/render.rs | 24 +- src/font/book.rs | 2 +- src/font/variant.rs | 2 +- src/frame.rs | 69 +- src/geom/align.rs | 37 ++ src/geom/angle.rs | 2 +- src/geom/mod.rs | 2 + src/geom/paint.rs | 18 - src/geom/stroke.rs | 61 ++ src/lib.rs | 57 +- src/library/ext.rs | 179 ----- src/library/graphics/hide.rs | 27 - src/library/graphics/image.rs | 120 ---- src/library/graphics/line.rs | 80 --- src/library/graphics/mod.rs | 11 - src/library/graphics/shape.rs | 201 ------ src/library/layout/align.rs | 62 -- src/library/layout/columns.rs | 117 ---- src/library/layout/container.rs | 80 --- src/library/layout/flow.rs | 267 -------- src/library/layout/grid.rs | 593 ----------------- src/library/layout/mod.rs | 814 ----------------------- src/library/layout/pad.rs | 83 --- src/library/layout/page.rs | 421 ------------ src/library/layout/place.rs | 56 -- src/library/layout/spacing.rs | 100 --- src/library/layout/stack.rs | 321 --------- src/library/layout/transform.rs | 116 ---- src/library/math/frac.rs | 17 - src/library/math/mod.rs | 295 --------- src/library/math/script.rs | 31 - src/library/mod.rs | 183 ------ src/library/prelude.rs | 27 - src/library/raw.rs | 149 ----- src/library/structure/doc.rs | 29 - src/library/structure/heading.rs | 176 ----- src/library/structure/list.rs | 371 ----------- src/library/structure/mod.rs | 13 - src/library/structure/reference.rs | 29 - src/library/structure/table.rs | 160 ----- src/library/text/deco.rs | 249 ------- src/library/text/link.rs | 114 ---- src/library/text/mod.rs | 593 ----------------- src/library/text/par.rs | 1266 ------------------------------------ src/library/text/quotes.rs | 148 ----- src/library/text/raw.rs | 205 ------ src/library/text/shaping.rs | 654 ------------------- src/library/text/shift.rs | 187 ------ src/library/utility/color.rs | 66 -- src/library/utility/data.rs | 131 ---- src/library/utility/math.rs | 144 ---- src/library/utility/mod.rs | 54 -- src/library/utility/string.rs | 140 ---- src/main.rs | 674 ------------------- src/model/args.rs | 2 +- src/model/array.rs | 10 +- src/model/cast.rs | 142 +++- src/model/content.rs | 10 +- src/model/dict.rs | 8 +- src/model/eval.rs | 183 +++--- src/model/func.rs | 4 +- src/model/items.rs | 123 ++++ src/model/methods.rs | 2 +- src/model/mod.rs | 8 +- src/model/ops.rs | 21 +- src/model/str.rs | 18 +- src/model/styles.rs | 118 ++-- src/model/value.rs | 8 +- src/model/vm.rs | 23 +- src/syntax/ast.rs | 26 +- src/syntax/highlight.rs | 2 +- src/syntax/incremental.rs | 26 +- src/syntax/node.rs | 70 +- src/syntax/parser.rs | 2 +- src/syntax/span.rs | 4 +- src/syntax/tokens.rs | 2 +- src/util/eco.rs | 13 +- src/util/mod.rs | 12 +- tests/typ/math/basic.typ | 6 - tests/typeset.rs | 26 +- 132 files changed, 10470 insertions(+), 10285 deletions(-) create mode 100644 cli/Cargo.toml create mode 100644 cli/src/main.rs create mode 100644 library/Cargo.toml create mode 100644 library/src/ext.rs create mode 100644 library/src/graphics/hide.rs create mode 100644 library/src/graphics/image.rs create mode 100644 library/src/graphics/line.rs create mode 100644 library/src/graphics/mod.rs create mode 100644 library/src/graphics/shape.rs create mode 100644 library/src/layout/align.rs create mode 100644 library/src/layout/columns.rs create mode 100644 library/src/layout/container.rs create mode 100644 library/src/layout/flow.rs create mode 100644 library/src/layout/grid.rs create mode 100644 library/src/layout/mod.rs create mode 100644 library/src/layout/pad.rs create mode 100644 library/src/layout/page.rs create mode 100644 library/src/layout/place.rs create mode 100644 library/src/layout/spacing.rs create mode 100644 library/src/layout/stack.rs create mode 100644 library/src/layout/transform.rs create mode 100644 library/src/lib.rs create mode 100644 library/src/math/mod.rs create mode 100644 library/src/math/tex.rs create mode 100644 library/src/prelude.rs create mode 100644 library/src/structure/doc.rs create mode 100644 library/src/structure/heading.rs create mode 100644 library/src/structure/list.rs create mode 100644 library/src/structure/mod.rs create mode 100644 library/src/structure/reference.rs create mode 100644 library/src/structure/table.rs create mode 100644 library/src/text/deco.rs create mode 100644 library/src/text/link.rs create mode 100644 library/src/text/mod.rs create mode 100644 library/src/text/par.rs create mode 100644 library/src/text/quotes.rs create mode 100644 library/src/text/raw.rs create mode 100644 library/src/text/shaping.rs create mode 100644 library/src/text/shift.rs create mode 100644 library/src/utility/color.rs create mode 100644 library/src/utility/data.rs create mode 100644 library/src/utility/math.rs create mode 100644 library/src/utility/mod.rs create mode 100644 library/src/utility/string.rs create mode 100644 src/geom/stroke.rs delete mode 100644 src/library/ext.rs delete mode 100644 src/library/graphics/hide.rs delete mode 100644 src/library/graphics/image.rs delete mode 100644 src/library/graphics/line.rs delete mode 100644 src/library/graphics/mod.rs delete mode 100644 src/library/graphics/shape.rs delete mode 100644 src/library/layout/align.rs delete mode 100644 src/library/layout/columns.rs delete mode 100644 src/library/layout/container.rs delete mode 100644 src/library/layout/flow.rs delete mode 100644 src/library/layout/grid.rs delete mode 100644 src/library/layout/mod.rs delete mode 100644 src/library/layout/pad.rs delete mode 100644 src/library/layout/page.rs delete mode 100644 src/library/layout/place.rs delete mode 100644 src/library/layout/spacing.rs delete mode 100644 src/library/layout/stack.rs delete mode 100644 src/library/layout/transform.rs delete mode 100644 src/library/math/frac.rs delete mode 100644 src/library/math/mod.rs delete mode 100644 src/library/math/script.rs delete mode 100644 src/library/mod.rs delete mode 100644 src/library/prelude.rs delete mode 100644 src/library/raw.rs delete mode 100644 src/library/structure/doc.rs delete mode 100644 src/library/structure/heading.rs delete mode 100644 src/library/structure/list.rs delete mode 100644 src/library/structure/mod.rs delete mode 100644 src/library/structure/reference.rs delete mode 100644 src/library/structure/table.rs delete mode 100644 src/library/text/deco.rs delete mode 100644 src/library/text/link.rs delete mode 100644 src/library/text/mod.rs delete mode 100644 src/library/text/par.rs delete mode 100644 src/library/text/quotes.rs delete mode 100644 src/library/text/raw.rs delete mode 100644 src/library/text/shaping.rs delete mode 100644 src/library/text/shift.rs delete mode 100644 src/library/utility/color.rs delete mode 100644 src/library/utility/data.rs delete mode 100644 src/library/utility/math.rs delete mode 100644 src/library/utility/mod.rs delete mode 100644 src/library/utility/string.rs delete mode 100644 src/main.rs create mode 100644 src/model/items.rs diff --git a/Cargo.lock b/Cargo.lock index e1eabd01..aabc95e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1110,49 +1110,78 @@ version = "0.1.0" dependencies = [ "bitflags", "bytemuck", - "chrono", - "codespan-reporting", "comemo", - "csv", - "dirs", "elsa", "flate2", - "hypher", "iai", "image", - "kurbo", - "lipsum", - "memmap2", "miniz_oxide", - "notify", "once_cell", "pdf-writer", - "pico-args", "pixglyph", "regex", "resvg", "rex", "roxmltree", "rustybuzz", - "same-file", "serde", - "serde_json", "siphasher", "subsetter", "svg2pdf", "syntect", "tiny-skia", "ttf-parser 0.17.1", - "typed-arena", + "typst-library", "typst-macros", - "unicode-bidi", - "unicode-math", - "unicode-script", "unicode-segmentation", "unicode-xid", "unscanny", "usvg", "walkdir", +] + +[[package]] +name = "typst-cli" +version = "0.1.0" +dependencies = [ + "chrono", + "codespan-reporting", + "comemo", + "dirs", + "elsa", + "memmap2", + "notify", + "once_cell", + "pico-args", + "same-file", + "siphasher", + "typst", + "typst-library", + "walkdir", +] + +[[package]] +name = "typst-library" +version = "0.1.0" +dependencies = [ + "comemo", + "csv", + "hypher", + "kurbo", + "lipsum", + "once_cell", + "rex", + "roxmltree", + "rustybuzz", + "serde_json", + "syntect", + "ttf-parser 0.17.1", + "typed-arena", + "typst", + "unicode-bidi", + "unicode-math", + "unicode-script", + "unscanny", "xi-unicode", ] diff --git a/Cargo.toml b/Cargo.toml index f08f9056..0e964877 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,97 +5,47 @@ authors = ["The Typst Project Developers"] edition = "2021" [workspace] -members = ["macros"] +members = ["cli", "library", "macros"] [dependencies] -# Workspace -typst-macros = { path = "./macros" } - -# Utilities +typst-macros = { path = "macros" } bitflags = "1" bytemuck = "1" comemo = "0.1" +flate2 = "1" +image = { version = "0.24", default-features = false, features = ["png", "jpeg", "gif"] } +miniz_oxide = "0.5" once_cell = "1" +pdf-writer = "0.6" +pixglyph = { git = "https://github.com/typst/pixglyph" } regex = "1" +resvg = { version = "0.22", default-features = false } +rex = { git = "https://github.com/laurmaedje/ReX" } +roxmltree = "0.14" +rustybuzz = "0.5" serde = { version = "1", features = ["derive"] } siphasher = "0.3" -typed-arena = "2" -unscanny = "0.1" - -# Text and font handling -hypher = "0.1" -kurbo = "0.8" -rustybuzz = "0.5" +subsetter = "0.1" +svg2pdf = "0.4" +syntect = { version = "5", default-features = false, features = ["default-syntaxes", "regex-fancy"] } +tiny-skia = "0.6.2" ttf-parser = "0.17" -unicode-bidi = "0.3.5" -unicode-script = "0.5" unicode-segmentation = "1" unicode-xid = "0.2" -xi-unicode = "0.3" - -# Raster and vector graphics handling -image = { version = "0.24", default-features = false, features = ["png", "jpeg", "gif"] } +unscanny = "0.1" usvg = { version = "0.22", default-features = false } -# External implementation of user-facing features -csv = "1" -lipsum = { git = "https://github.com/reknih/lipsum" } -rex = { git = "https://github.com/laurmaedje/ReX" } -serde_json = "1" -syntect = { version = "5", default-features = false, features = ["default-syntaxes", "regex-fancy"] } -unicode-math = { git = "https://github.com/s3bk/unicode-math/" } - -# PDF export -miniz_oxide = "0.5" -pdf-writer = "0.6" -subsetter = "0.1" -svg2pdf = "0.4" - -# Rendering -flate2 = "1" -pixglyph = { git = "https://github.com/typst/pixglyph" } -resvg = { version = "0.22", default-features = false } -roxmltree = "0.14" -tiny-skia = "0.6.2" - -# Command line interface -chrono = { version = "0.4", default-features = false, features = ["clock", "std"], optional = true } -codespan-reporting = { version = "0.11", optional = true } -dirs = { version = "4", optional = true } -elsa = { version = "1.7", optional = true } -memmap2 = { version = "0.5", optional = true } -notify = { version = "5", optional = true } -pico-args = { version = "0.4", optional = true } -same-file = { version = "1", optional = true } -walkdir = { version = "2", optional = true } - [dev-dependencies] +typst-library = { path = "library" } iai = { git = "https://github.com/reknih/iai" } elsa = "1.7" walkdir = "2" -[features] -cli = [ - "chrono", - "codespan-reporting", - "dirs", - "elsa", - "memmap2", - "notify", - "pico-args", - "same-file", - "walkdir", -] - [profile.dev] -debug = 0 # Faster compilation +debug = 0 [profile.dev.package."*"] -opt-level = 2 # Faster test execution - -[[bin]] -name = "typst" -required-features = ["cli"] +opt-level = 2 [[test]] name = "typeset" diff --git a/benches/oneshot.rs b/benches/oneshot.rs index 2437b723..965d0c78 100644 --- a/benches/oneshot.rs +++ b/benches/oneshot.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use comemo::{Prehashed, Track, Tracked}; use iai::{black_box, main, Iai}; @@ -20,7 +20,7 @@ main!( bench_parse, bench_edit, bench_eval, - bench_layout, + bench_typeset, bench_highlight, bench_render, ); @@ -80,12 +80,10 @@ fn bench_eval(iai: &mut Iai) { iai.run(|| typst::model::eval(world.track(), route.track(), id).unwrap()); } -fn bench_layout(iai: &mut Iai) { +fn bench_typeset(iai: &mut Iai) { let world = BenchWorld::new(); let id = world.source.id(); - let route = typst::model::Route::default(); - let module = typst::model::eval(world.track(), route.track(), id).unwrap(); - iai.run(|| typst::library::layout::Layout::layout(&module.content, world.track())); + iai.run(|| typst::typeset(&world, id)); } fn bench_render(iai: &mut Iai) { @@ -104,7 +102,13 @@ struct BenchWorld { impl BenchWorld { fn new() -> Self { - let config = Config::default(); + let config = Config { + root: PathBuf::new(), + scope: typst_library::scope(), + styles: typst_library::styles(), + items: typst_library::items(), + }; + let font = Font::new(FONT.into(), 0).unwrap(); let book = FontBook::from_fonts([&font]); let id = SourceId::from_u16(0); diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 00000000..5e3f227a --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "typst-cli" +version = "0.1.0" +authors = ["The Typst Project Developers"] +edition = "2021" + +[[bin]] +name = "typst" +path = "src/main.rs" +doc = false + +[dependencies] +typst = { path = ".." } +typst-library = { path = "../library" } +chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } +codespan-reporting = "0.11" +comemo = "0.1" +dirs = "4" +elsa = "1.7" +memmap2 = "0.5" +notify = "5" +once_cell = "1" +pico-args = "0.4" +same-file = "1" +siphasher = "0.3" +walkdir = "2" diff --git a/cli/src/main.rs b/cli/src/main.rs new file mode 100644 index 00000000..62cad8ab --- /dev/null +++ b/cli/src/main.rs @@ -0,0 +1,681 @@ +use std::cell::{RefCell, RefMut}; +use std::collections::HashMap; +use std::fs::{self, File}; +use std::hash::Hash; +use std::io::{self, Read, Write}; +use std::path::{Path, PathBuf}; +use std::process; + +use codespan_reporting::diagnostic::{Diagnostic, Label}; +use codespan_reporting::term::{self, termcolor}; +use comemo::Prehashed; +use elsa::FrozenVec; +use memmap2::Mmap; +use notify::{RecommendedWatcher, RecursiveMode, Watcher}; +use once_cell::unsync::OnceCell; +use pico_args::Arguments; +use same_file::{is_same_file, Handle}; +use siphasher::sip128::{Hasher128, SipHasher}; +use termcolor::{ColorChoice, StandardStream, WriteColor}; +use walkdir::WalkDir; + +use typst::diag::{FileError, FileResult, SourceError, StrResult}; +use typst::font::{Font, FontBook, FontInfo, FontVariant}; +use typst::syntax::{Source, SourceId}; +use typst::util::{Buffer, PathExt}; +use typst::{Config, World}; + +type CodespanResult = Result; +type CodespanError = codespan_reporting::files::Error; + +/// What to do. +enum Command { + Typeset(TypesetCommand), + Fonts(FontsCommand), +} + +/// Typeset a .typ file into a PDF file. +struct TypesetCommand { + input: PathBuf, + output: PathBuf, + root: Option, + watch: bool, +} + +const HELP: &'static str = "\ +typst creates PDF files from .typ files + +USAGE: + typst [OPTIONS] [output.pdf] + typst [SUBCOMMAND] ... + +ARGS: + Path to input Typst file + [output.pdf] Path to output PDF file + +OPTIONS: + -h, --help Print this help + -w, --watch Watch the inputs and recompile on changes + --root Configure the root for absolute paths + +SUBCOMMANDS: + --fonts List all discovered system fonts +"; + +/// List discovered system fonts. +struct FontsCommand { + variants: bool, +} + +const HELP_FONTS: &'static str = "\ +typst --fonts lists all discovered system fonts + +USAGE: + typst --fonts [OPTIONS] + +OPTIONS: + -h, --help Print this help + --variants Also list style variants of each font family +"; + +/// Entry point. +fn main() { + let command = parse_args(); + let ok = command.is_ok(); + if let Err(msg) = command.and_then(dispatch) { + print_error(&msg).unwrap(); + if !ok { + println!("\nfor more information, try --help"); + } + process::exit(1); + } +} + +/// Parse command line arguments. +fn parse_args() -> StrResult { + let mut args = Arguments::from_env(); + let help = args.contains(["-h", "--help"]); + + let command = if args.contains("--fonts") { + if help { + print_help(HELP_FONTS); + } + + Command::Fonts(FontsCommand { variants: args.contains("--variants") }) + } else { + if help { + print_help(HELP); + } + + let root = args.opt_value_from_str("--root").map_err(|_| "missing root path")?; + let watch = args.contains(["-w", "--watch"]); + let (input, output) = parse_input_output(&mut args, "pdf")?; + Command::Typeset(TypesetCommand { input, output, watch, root }) + }; + + // Don't allow excess arguments. + let rest = args.finish(); + if !rest.is_empty() { + Err(format!( + "unexpected argument{}", + if rest.len() > 1 { "s" } else { "" } + ))?; + } + + Ok(command) +} + +/// Parse two freestanding path arguments, with the output path being optional. +/// If it is omitted, it is determined from the input path's file stem plus the +/// given extension. +fn parse_input_output(args: &mut Arguments, ext: &str) -> StrResult<(PathBuf, PathBuf)> { + let input: PathBuf = args.free_from_str().map_err(|_| "missing input file")?; + let output = match args.opt_free_from_str().ok().flatten() { + Some(output) => output, + None => { + let name = input.file_name().ok_or("source path does not point to a file")?; + Path::new(name).with_extension(ext) + } + }; + + // Ensure that the source file is not overwritten. + if is_same_file(&input, &output).unwrap_or(false) { + Err("source and destination files are the same")?; + } + + Ok((input, output)) +} + +/// Print a help string and quit. +fn print_help(help: &'static str) { + print!("{help}"); + std::process::exit(0); +} + +/// Print an application-level error (independent from a source file). +fn print_error(msg: &str) -> io::Result<()> { + let mut w = StandardStream::stderr(ColorChoice::Always); + let styles = term::Styles::default(); + + w.set_color(&styles.header_error)?; + write!(w, "error")?; + + w.reset()?; + writeln!(w, ": {msg}.") +} + +/// Dispatch a command. +fn dispatch(command: Command) -> StrResult<()> { + match command { + Command::Typeset(command) => typeset(command), + Command::Fonts(command) => fonts(command), + } +} + +/// Execute a typesetting command. +fn typeset(command: TypesetCommand) -> StrResult<()> { + let root = if let Some(root) = &command.root { + root.clone() + } else if let Some(dir) = command.input.parent() { + dir.into() + } else { + PathBuf::new() + }; + + let config = Config { + root, + scope: typst_library::scope(), + styles: typst_library::styles(), + items: typst_library::items(), + }; + + // Create the world that serves sources, fonts and files. + let mut world = SystemWorld::new(config); + + // Typeset. + typeset_once(&mut world, &command)?; + + if !command.watch { + return Ok(()); + } + + // Setup file watching. + let (tx, rx) = std::sync::mpsc::channel(); + let mut watcher = RecommendedWatcher::new(tx, notify::Config::default()) + .map_err(|_| "failed to watch directory")?; + + // Watch this directory recursively. + watcher + .watch(Path::new("."), RecursiveMode::Recursive) + .map_err(|_| "failed to watch directory")?; + + // Handle events. + let timeout = std::time::Duration::from_millis(100); + 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")?; + if event + .paths + .iter() + .all(|path| is_same_file(path, &command.output).unwrap_or(false)) + { + continue; + } + + recompile |= world.relevant(&event); + } + + if recompile { + typeset_once(&mut world, &command)?; + } + } +} + +/// Typeset a single time. +fn typeset_once(world: &mut SystemWorld, command: &TypesetCommand) -> StrResult<()> { + status(command, Status::Compiling).unwrap(); + + world.reset(); + let main = world.resolve(&command.input).map_err(|err| err.to_string())?; + match typst::typeset(world, main) { + // Export the PDF. + Ok(frames) => { + let buffer = typst::export::pdf(&frames); + fs::write(&command.output, buffer).map_err(|_| "failed to write PDF file")?; + status(command, Status::Success).unwrap(); + } + + // Print diagnostics. + Err(errors) => { + status(command, Status::Error).unwrap(); + print_diagnostics(&world, *errors) + .map_err(|_| "failed to print diagnostics")?; + } + } + + Ok(()) +} + +/// Clear the terminal and render the status message. +fn status(command: &TypesetCommand, status: Status) -> io::Result<()> { + if !command.watch { + return Ok(()); + } + + let esc = 27 as char; + let input = command.input.display(); + let output = command.output.display(); + let time = chrono::offset::Local::now(); + let timestamp = time.format("%H:%M:%S"); + let message = status.message(); + let color = status.color(); + + let mut w = StandardStream::stderr(ColorChoice::Always); + write!(w, "{esc}c{esc}[1;1H")?; + + w.set_color(&color)?; + write!(w, "watching")?; + w.reset()?; + writeln!(w, " {input}")?; + + w.set_color(&color)?; + write!(w, "writing to")?; + w.reset()?; + writeln!(w, " {output}")?; + + writeln!(w)?; + writeln!(w, "[{timestamp}] {message}")?; + writeln!(w)?; + + w.flush() +} + +/// The status in which the watcher can be. +enum Status { + Compiling, + Success, + Error, +} + +impl Status { + fn message(&self) -> &str { + match self { + Self::Compiling => "compiling ...", + Self::Success => "compiled successfully", + Self::Error => "compiled with errors", + } + } + + fn color(&self) -> termcolor::ColorSpec { + let styles = term::Styles::default(); + match self { + Self::Error => styles.header_error, + _ => styles.header_note, + } + } +} + +/// Print diagnostic messages to the terminal. +fn print_diagnostics( + world: &SystemWorld, + errors: Vec, +) -> Result<(), codespan_reporting::files::Error> { + let mut w = StandardStream::stderr(ColorChoice::Always); + let config = term::Config { tab_width: 2, ..Default::default() }; + + for error in errors { + // The main diagnostic. + let range = error.range(world); + let diag = Diagnostic::error() + .with_message(error.message) + .with_labels(vec![Label::primary(error.span.source(), range)]); + + 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.source(), + world.source(point.span.source()).range(point.span), + ), + ]); + + term::emit(&mut w, &config, world, &help)?; + } + } + + Ok(()) +} + +/// Execute a font listing command. +fn fonts(command: FontsCommand) -> StrResult<()> { + let mut searcher = FontSearcher::new(); + searcher.search_system(); + 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(()) +} + +/// A world that provides access to the operating system. +struct SystemWorld { + config: Prehashed, + book: Prehashed, + fonts: Vec, + hashes: RefCell>>, + paths: RefCell>, + sources: FrozenVec>, +} + +/// Holds details about the location of a font and lazily the font itself. +struct FontSlot { + path: PathBuf, + index: u32, + font: OnceCell>, +} + +/// Holds canonical data for all paths pointing to the same entity. +#[derive(Default)] +struct PathSlot { + source: OnceCell>, + buffer: OnceCell>, +} + +impl SystemWorld { + fn new(config: Config) -> Self { + let mut searcher = FontSearcher::new(); + searcher.search_system(); + + Self { + config: Prehashed::new(config), + book: Prehashed::new(searcher.book), + fonts: searcher.fonts, + hashes: RefCell::default(), + paths: RefCell::default(), + sources: FrozenVec::new(), + } + } +} + +impl World for SystemWorld { + fn config(&self) -> &Prehashed { + &self.config + } + + fn book(&self) -> &Prehashed { + &self.book + } + + fn font(&self, id: usize) -> Option { + let slot = &self.fonts[id]; + slot.font + .get_or_init(|| { + let data = self.file(&slot.path).ok()?; + Font::new(data, slot.index) + }) + .clone() + } + + fn file(&self, path: &Path) -> FileResult { + self.slot(path)? + .buffer + .get_or_init(|| read(path).map(Buffer::from)) + .clone() + } + + fn resolve(&self, path: &Path) -> FileResult { + self.slot(path)? + .source + .get_or_init(|| { + let buf = read(path)?; + let text = String::from_utf8(buf)?; + Ok(self.insert(path, text)) + }) + .clone() + } + + fn source(&self, id: SourceId) -> &Source { + &self.sources[id.into_u16() as usize] + } +} + +impl SystemWorld { + fn slot(&self, path: &Path) -> FileResult> { + let mut hashes = self.hashes.borrow_mut(); + let hash = match hashes.get(path).cloned() { + Some(hash) => hash, + None => { + let hash = PathHash::new(path); + if let Ok(canon) = path.canonicalize() { + hashes.insert(canon.normalize(), hash.clone()); + } + hashes.insert(path.into(), hash.clone()); + hash + } + }?; + + Ok(std::cell::RefMut::map(self.paths.borrow_mut(), |paths| { + paths.entry(hash).or_default() + })) + } + + fn insert(&self, path: &Path, text: String) -> SourceId { + let id = SourceId::from_u16(self.sources.len() as u16); + let source = Source::new(id, path, text); + self.sources.push(Box::new(source)); + id + } + + fn relevant(&mut self, event: ¬ify::Event) -> bool { + match &event.kind { + notify::EventKind::Any => {} + notify::EventKind::Access(_) => return false, + notify::EventKind::Create(_) => return true, + notify::EventKind::Modify(kind) => match kind { + notify::event::ModifyKind::Any => {} + notify::event::ModifyKind::Data(_) => {} + notify::event::ModifyKind::Metadata(_) => return false, + notify::event::ModifyKind::Name(_) => return true, + notify::event::ModifyKind::Other => return false, + }, + notify::EventKind::Remove(_) => {} + notify::EventKind::Other => return false, + } + + event.paths.iter().any(|path| self.dependant(path)) + } + + fn dependant(&self, path: &Path) -> bool { + self.hashes.borrow().contains_key(&path.normalize()) + || PathHash::new(path) + .map_or(false, |hash| self.paths.borrow().contains_key(&hash)) + } + + fn reset(&mut self) { + self.sources.as_mut().clear(); + self.hashes.borrow_mut().clear(); + self.paths.borrow_mut().clear(); + } +} + +/// 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 { + let f = |e| FileError::from_io(e, path); + let handle = Handle::from_path(path).map_err(f)?; + let mut state = SipHasher::new(); + handle.hash(&mut state); + Ok(Self(state.finish128().as_u128())) + } +} + +/// Read a file. +fn read(path: &Path) -> FileResult> { + let f = |e| FileError::from_io(e, path); + let mut file = File::open(path).map_err(f)?; + if file.metadata().map_err(f)?.is_file() { + let mut data = vec![]; + file.read_to_end(&mut data).map_err(f)?; + Ok(data) + } else { + Err(FileError::IsDirectory) + } +} + +impl<'a> codespan_reporting::files::Files<'a> for SystemWorld { + type FileId = SourceId; + type Name = std::path::Display<'a>; + type Source = &'a str; + + fn name(&'a self, id: SourceId) -> CodespanResult { + Ok(World::source(self, id).path().display()) + } + + fn source(&'a self, id: SourceId) -> CodespanResult { + Ok(World::source(self, id).text()) + } + + fn line_index(&'a self, id: SourceId, given: usize) -> CodespanResult { + let source = World::source(self, id); + source + .byte_to_line(given) + .ok_or_else(|| CodespanError::IndexTooLarge { + given, + max: source.len_bytes(), + }) + } + + fn line_range( + &'a self, + id: SourceId, + given: usize, + ) -> CodespanResult> { + let source = World::source(self, id); + source + .line_to_range(given) + .ok_or_else(|| CodespanError::LineTooLarge { given, max: source.len_lines() }) + } + + fn column_number( + &'a self, + id: SourceId, + _: usize, + given: usize, + ) -> CodespanResult { + let source = World::source(self, 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 } + } + }) + } +} + +/// Searches for fonts. +struct FontSearcher { + book: FontBook, + fonts: Vec, +} + +impl FontSearcher { + /// Create a new, empty system searcher. + fn new() -> Self { + Self { book: FontBook::new(), fonts: vec![] } + } + + /// Search for fonts in the linux system font directories. + #[cfg(all(unix, not(target_os = "macos")))] + fn search_system(&mut self) { + self.search_dir("/usr/share/fonts"); + self.search_dir("/usr/local/share/fonts"); + + if let Some(dir) = dirs::font_dir() { + self.search_dir(dir); + } + } + + /// Search for fonts in the macOS system font directories. + #[cfg(target_os = "macos")] + fn search_system(&mut self) { + self.search_dir("/Library/Fonts"); + self.search_dir("/Network/Library/Fonts"); + self.search_dir("/System/Library/Fonts"); + + if let Some(dir) = dirs::font_dir() { + self.search_dir(dir); + } + } + + /// Search for fonts in the Windows system font directories. + #[cfg(windows)] + fn search_system(&mut self) { + let windir = + std::env::var("WINDIR").unwrap_or_else(|_| "C:\\Windows".to_string()); + + self.search_dir(Path::new(&windir).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")); + } + } + + /// Search for all fonts in a directory. + /// recursively. + fn search_dir(&mut self, path: impl AsRef) { + 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: impl AsRef) { + let path = path.as_ref(); + if let Ok(file) = File::open(path) { + if let Ok(mmap) = unsafe { Mmap::map(&file) } { + for (i, info) in FontInfo::from_data(&mmap).enumerate() { + self.book.push(info); + self.fonts.push(FontSlot { + path: path.into(), + index: i as u32, + font: OnceCell::new(), + }); + } + } + } + } +} diff --git a/library/Cargo.toml b/library/Cargo.toml new file mode 100644 index 00000000..18f4779c --- /dev/null +++ b/library/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "typst-library" +version = "0.1.0" +authors = ["The Typst Project Developers"] +edition = "2021" + +[dependencies] +typst = { path = ".." } +comemo = "0.1" +csv = "1" +hypher = "0.1" +kurbo = "0.8" +lipsum = { git = "https://github.com/reknih/lipsum" } +once_cell = "1" +rex = { git = "https://github.com/laurmaedje/ReX" } +roxmltree = "0.14" +rustybuzz = "0.5" +serde_json = "1" +syntect = { version = "5", default-features = false, features = ["default-syntaxes", "regex-fancy"] } +ttf-parser = "0.17" +typed-arena = "2" +unicode-bidi = "0.3.5" +unicode-math = { git = "https://github.com/s3bk/unicode-math/" } +unicode-script = "0.5" +unscanny = "0.1" +xi-unicode = "0.3" diff --git a/library/src/ext.rs b/library/src/ext.rs new file mode 100644 index 00000000..72ef484b --- /dev/null +++ b/library/src/ext.rs @@ -0,0 +1,181 @@ +use super::*; +use crate::prelude::*; + +/// 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; + + /// Add weak vertical spacing above and below the content. + fn spaced(self, above: Option, below: Option) -> Self; + + /// Force a size for this content. + fn boxed(self, sizing: Axes>>) -> Self; + + /// Set alignments for this content. + fn aligned(self, aligns: Axes>) -> Self; + + /// Pad this content at the sides. + fn padded(self, padding: Sides>) -> Self; + + /// Transform this content's contents without affecting layout. + fn moved(self, delta: Axes>) -> Self; + + /// Fill the frames resulting from a content. + fn filled(self, fill: Paint) -> Self; + + /// Stroke the frames resulting from a content. + fn stroked(self, stroke: Stroke) -> Self; +} + +impl ContentExt for Content { + fn strong(self) -> Self { + text::StrongNode(self).pack() + } + + fn emph(self) -> Self { + text::EmphNode(self).pack() + } + + fn underlined(self) -> Self { + text::DecoNode::<{ text::UNDERLINE }>(self).pack() + } + + fn spaced(self, above: Option, below: Option) -> Self { + if above.is_none() && below.is_none() { + return self; + } + + let mut seq = vec![]; + if let Some(above) = above { + seq.push( + layout::VNode { + amount: above.into(), + weak: true, + generated: true, + } + .pack(), + ); + } + + seq.push(self); + if let Some(below) = below { + seq.push( + layout::VNode { + amount: below.into(), + weak: true, + generated: true, + } + .pack(), + ); + } + + Content::sequence(seq) + } + + fn boxed(self, sizing: Axes>>) -> Self { + layout::BoxNode { sizing, child: self }.pack() + } + + fn aligned(self, aligns: Axes>) -> Self { + layout::AlignNode { aligns, child: self }.pack() + } + + fn padded(self, padding: Sides>) -> Self { + layout::PadNode { padding, child: self }.pack() + } + + fn moved(self, delta: Axes>) -> Self { + layout::MoveNode { delta, child: self }.pack() + } + + fn filled(self, fill: Paint) -> Self { + FillNode { fill, child: self }.pack() + } + + fn stroked(self, stroke: Stroke) -> Self { + StrokeNode { stroke, child: self }.pack() + } +} + +/// Additional methods for the style chain. +pub trait StyleMapExt { + /// Set a font family composed of a preferred family and existing families + /// from a style chain. + fn set_family(&mut self, preferred: text::FontFamily, existing: StyleChain); +} + +impl StyleMapExt for StyleMap { + fn set_family(&mut self, preferred: text::FontFamily, existing: StyleChain) { + self.set( + text::TextNode::FAMILY, + FallbackList( + std::iter::once(preferred) + .chain(existing.get(text::TextNode::FAMILY).0.iter().cloned()) + .collect(), + ), + ); + } +} + +/// Fill the frames resulting from content. +#[derive(Debug, Hash)] +struct FillNode { + /// How to fill the frames resulting from the `child`. + fill: Paint, + /// The content whose frames should be filled. + child: Content, +} + +#[node(LayoutBlock)] +impl FillNode {} + +impl LayoutBlock for FillNode { + fn layout_block( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + let mut frames = self.child.layout_block(world, regions, styles)?; + for frame in &mut frames { + let shape = Geometry::Rect(frame.size()).filled(self.fill); + frame.prepend(Point::zero(), Element::Shape(shape)); + } + Ok(frames) + } +} + +/// Stroke the frames resulting from content. +#[derive(Debug, Hash)] +struct StrokeNode { + /// How to stroke the frames resulting from the `child`. + stroke: Stroke, + /// The content whose frames should be stroked. + child: Content, +} + +#[node(LayoutBlock)] +impl StrokeNode {} + +impl LayoutBlock for StrokeNode { + fn layout_block( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + let mut frames = self.child.layout_block(world, regions, styles)?; + for frame in &mut frames { + let shape = Geometry::Rect(frame.size()).stroked(self.stroke); + frame.prepend(Point::zero(), Element::Shape(shape)); + } + Ok(frames) + } +} diff --git a/library/src/graphics/hide.rs b/library/src/graphics/hide.rs new file mode 100644 index 00000000..3affd809 --- /dev/null +++ b/library/src/graphics/hide.rs @@ -0,0 +1,27 @@ +use crate::prelude::*; + +/// Hide content without affecting layout. +#[derive(Debug, Hash)] +pub struct HideNode(pub Content); + +#[node(LayoutInline)] +impl HideNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("body")?).pack()) + } +} + +impl LayoutInline for HideNode { + fn layout_inline( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + let mut frames = self.0.layout_inline(world, regions, styles)?; + for frame in &mut frames { + frame.clear(); + } + Ok(frames) + } +} diff --git a/library/src/graphics/image.rs b/library/src/graphics/image.rs new file mode 100644 index 00000000..6bf02265 --- /dev/null +++ b/library/src/graphics/image.rs @@ -0,0 +1,121 @@ +use std::ffi::OsStr; + +use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat}; + +use crate::prelude::*; +use crate::text::TextNode; + +/// Show a raster or vector graphic. +#[derive(Debug, Hash)] +pub struct ImageNode(pub Image); + +#[node(LayoutInline)] +impl ImageNode { + /// How the image should adjust itself to a given area. + pub const FIT: ImageFit = ImageFit::Cover; + + fn construct(vm: &mut Vm, args: &mut Args) -> SourceResult { + let Spanned { v: path, span } = + args.expect::>("path to image file")?; + + let full = vm.locate(&path).at(span)?; + let buffer = vm.world.file(&full).at(span)?; + let ext = full.extension().and_then(OsStr::to_str).unwrap_or_default(); + let format = match ext.to_lowercase().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!(span, "unknown image format"), + }; + + let image = Image::new(buffer, format).at(span)?; + let width = args.named("width")?; + let height = args.named("height")?; + + Ok(ImageNode(image).pack().boxed(Axes::new(width, height))) + } +} + +impl LayoutInline for ImageNode { + fn layout_inline( + &self, + _: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + let pxw = self.0.width() as f64; + let pxh = self.0.height() as f64; + let px_ratio = pxw / pxh; + + // Find out whether the image is wider or taller than the target size. + let &Regions { first, expand, .. } = regions; + let region_ratio = first.x / first.y; + 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 { + first + } else if expand.x || (!expand.y && wide && first.x.is_finite()) { + Size::new(first.x, first.y.min(first.x.safe_div(px_ratio))) + } else if first.y.is_finite() { + Size::new(first.x.min(first.y * px_ratio), first.y) + } else { + Size::new(Abs::pt(pxw), Abs::pt(pxh)) + }; + + // Compute the actual size of the fitted image. + let fit = styles.get(Self::FIT); + 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(), Element::Image(self.0.clone(), fitted)); + 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 link if it exists. + if let Some(url) = styles.get(TextNode::LINK) { + frame.link(url.clone()); + } + + Ok(vec![frame]) + } +} + +/// How an image should adjust itself to a given area. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum ImageFit { + /// The image should completely cover the area. + Cover, + /// The image should be fully contained in the area. + Contain, + /// The image should be stretched so that it exactly fills the area. + Stretch, +} + +castable! { + ImageFit, + Expected: "string", + Value::Str(string) => match string.as_str() { + "cover" => Self::Cover, + "contain" => Self::Contain, + "stretch" => Self::Stretch, + _ => Err(r#"expected "cover", "contain" or "stretch""#)?, + }, +} diff --git a/library/src/graphics/line.rs b/library/src/graphics/line.rs new file mode 100644 index 00000000..df427171 --- /dev/null +++ b/library/src/graphics/line.rs @@ -0,0 +1,68 @@ +use crate::prelude::*; + +/// Display a line without affecting the layout. +#[derive(Debug, Hash)] +pub struct LineNode { + /// Where the line starts. + origin: Axes>, + /// The offset from the `origin` where the line ends. + delta: Axes>, +} + +#[node(LayoutInline)] +impl LineNode { + /// How to stroke the line. + #[property(resolve, fold)] + pub const STROKE: PartialStroke = PartialStroke::default(); + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let origin = args.named("origin")?.unwrap_or_default(); + + let delta = match args.named::>>("to")? { + Some(to) => to.zip(origin).map(|(to, from)| to - from), + None => { + let length = + args.named::>("length")?.unwrap_or(Abs::cm(1.0).into()); + + let angle = args.named::("angle")?.unwrap_or_default(); + let x = angle.cos() * length; + let y = angle.sin() * length; + + Axes::new(x, y) + } + }; + + Ok(Self { origin, delta }.pack()) + } +} + +impl LayoutInline for LineNode { + fn layout_inline( + &self, + _: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + let stroke = styles.get(Self::STROKE).unwrap_or_default(); + + let origin = self + .origin + .resolve(styles) + .zip(regions.base) + .map(|(l, b)| l.relative_to(b)); + + let delta = self + .delta + .resolve(styles) + .zip(regions.base) + .map(|(l, b)| l.relative_to(b)); + + let target = regions.expand.select(regions.first, Size::zero()); + let mut frame = Frame::new(target); + + let shape = Geometry::Line(delta.to_point()).stroked(stroke); + frame.push(origin.to_point(), Element::Shape(shape)); + + Ok(vec![frame]) + } +} diff --git a/library/src/graphics/mod.rs b/library/src/graphics/mod.rs new file mode 100644 index 00000000..34182121 --- /dev/null +++ b/library/src/graphics/mod.rs @@ -0,0 +1,11 @@ +//! Graphical elements and effects. + +mod hide; +mod image; +mod line; +mod shape; + +pub use self::image::*; +pub use hide::*; +pub use line::*; +pub use shape::*; diff --git a/library/src/graphics/shape.rs b/library/src/graphics/shape.rs new file mode 100644 index 00000000..d980b64a --- /dev/null +++ b/library/src/graphics/shape.rs @@ -0,0 +1,201 @@ +use std::f64::consts::SQRT_2; + +use crate::prelude::*; +use crate::text::TextNode; + +/// A sizable and fillable shape with optional content. +#[derive(Debug, Hash)] +pub struct ShapeNode(pub Option); + +/// A square with optional content. +pub type SquareNode = ShapeNode; + +/// A rectangle with optional content. +pub type RectNode = ShapeNode; + +/// A circle with optional content. +pub type CircleNode = ShapeNode; + +/// A ellipse with optional content. +pub type EllipseNode = ShapeNode; + +#[node(LayoutInline)] +impl ShapeNode { + /// How to fill the shape. + pub const FILL: Option = None; + /// How to stroke the shape. + #[property(skip, resolve, fold)] + pub const STROKE: Smart>> = Smart::Auto; + + /// How much to pad the shape's content. + #[property(resolve, fold)] + pub const INSET: Sides>> = Sides::splat(Rel::zero()); + /// How much to extend the shape's dimensions beyond the allocated space. + #[property(resolve, fold)] + pub const OUTSET: Sides>> = Sides::splat(Rel::zero()); + + /// How much to round the shape's corners. + #[property(skip, resolve, fold)] + pub const RADIUS: Corners>> = Corners::splat(Rel::zero()); + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let size = match S { + SQUARE => args.named::("size")?.map(Rel::from), + CIRCLE => args.named::("radius")?.map(|r| 2.0 * Rel::from(r)), + _ => None, + }; + + let width = match size { + None => args.named("width")?, + size => size, + }; + + let height = match size { + None => args.named("height")?, + size => size, + }; + + Ok(Self(args.eat()?).pack().boxed(Axes::new(width, height))) + } + + fn set(...) { + if is_round(S) { + styles.set_opt( + Self::STROKE, + args.named::>>("stroke")? + .map(|some| some.map(Sides::splat)), + ); + } else { + styles.set_opt(Self::STROKE, args.named("stroke")?); + styles.set_opt(Self::RADIUS, args.named("radius")?); + } + } +} + +impl LayoutInline for ShapeNode { + fn layout_inline( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + let mut frames; + if let Some(child) = &self.0 { + let mut inset = styles.get(Self::INSET); + if is_round(S) { + 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 mut pod = Regions::one(regions.first, regions.base, regions.expand); + frames = child.layout_inline(world, &pod, styles)?; + + for frame in frames.iter_mut() { + frame.apply_role(Role::GenericBlock); + } + + // Relayout with full expansion into square region to make sure + // the result is really a square or circle. + if is_quadratic(S) { + let length = if regions.expand.x || regions.expand.y { + let target = regions.expand.select(regions.first, Size::zero()); + target.x.max(target.y) + } else { + let size = frames[0].size(); + let desired = size.x.max(size.y); + desired.min(regions.first.x).min(regions.first.y) + }; + + pod.first = Size::splat(length); + pod.expand = Axes::splat(true); + frames = child.layout_inline(world, &pod, styles)?; + } + } else { + // The default size that a shape takes on if it has no child and + // enough space. + let mut size = Size::new(Abs::pt(45.0), Abs::pt(30.0)).min(regions.first); + + if is_quadratic(S) { + let length = if regions.expand.x || regions.expand.y { + let target = regions.expand.select(regions.first, Size::zero()); + target.x.max(target.y) + } else { + size.x.min(size.y) + }; + size = Size::splat(length); + } else { + size = regions.expand.select(regions.first, size); + } + + frames = vec![Frame::new(size)]; + } + + let frame = &mut frames[0]; + + // Add fill and/or stroke. + let fill = styles.get(Self::FILL); + let stroke = match styles.get(Self::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)) + } + }; + + let outset = styles.get(Self::OUTSET).relative_to(frame.size()); + let size = frame.size() + outset.sum_by_axis(); + + let radius = styles + .get(Self::RADIUS) + .map(|side| side.relative_to(size.x.min(size.y) / 2.0)); + + let pos = Point::new(-outset.left, -outset.top); + + if fill.is_some() || stroke.iter().any(Option::is_some) { + if is_round(S) { + let shape = ellipse(size, fill, stroke.left); + frame.prepend(pos, Element::Shape(shape)); + } else { + frame.prepend_multiple( + rounded_rect(size, radius, fill, stroke) + .into_iter() + .map(|x| (pos, Element::Shape(x))), + ) + } + } + + // Apply link if it exists. + if let Some(url) = styles.get(TextNode::LINK) { + frame.link(url.clone()); + } + + Ok(frames) + } +} + +/// A category of shape. +pub type ShapeKind = usize; + +/// A rectangle with equal side lengths. +const SQUARE: ShapeKind = 0; + +/// A quadrilateral with four right angles. +const RECT: ShapeKind = 1; + +/// An ellipse with coinciding foci. +const CIRCLE: ShapeKind = 2; + +/// A curve around two focal points. +const ELLIPSE: ShapeKind = 3; + +/// Whether a shape kind is curvy. +fn is_round(kind: ShapeKind) -> bool { + matches!(kind, CIRCLE | ELLIPSE) +} + +/// Whether a shape kind has equal side length. +fn is_quadratic(kind: ShapeKind) -> bool { + matches!(kind, SQUARE | CIRCLE) +} diff --git a/library/src/layout/align.rs b/library/src/layout/align.rs new file mode 100644 index 00000000..5e1b3555 --- /dev/null +++ b/library/src/layout/align.rs @@ -0,0 +1,62 @@ +use crate::prelude::*; +use crate::text::{HorizontalAlign, ParNode}; + +/// Align content along the layouting axes. +#[derive(Debug, Hash)] +pub struct AlignNode { + /// How to align the content horizontally and vertically. + pub aligns: Axes>, + /// The content to be aligned. + pub child: Content, +} + +#[node(LayoutBlock)] +impl AlignNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let aligns: Axes> = args.find()?.unwrap_or_default(); + let body: Content = args.expect("body")?; + + if let Axes { x: Some(x), y: None } = aligns { + if !body.has::() { + return Ok(body.styled(ParNode::ALIGN, HorizontalAlign(x))); + } + } + + Ok(body.aligned(aligns)) + } +} + +impl LayoutBlock for AlignNode { + fn layout_block( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + // The child only needs to expand along an axis if there's no alignment. + let mut pod = regions.clone(); + pod.expand &= self.aligns.as_ref().map(Option::is_none); + + // Align paragraphs inside the child. + let mut passed = StyleMap::new(); + if let Some(align) = self.aligns.x { + passed.set(ParNode::ALIGN, HorizontalAlign(align)); + } + + // Layout the child. + let mut frames = self.child.layout_block(world, &pod, passed.chain(&styles))?; + for (region, frame) in regions.iter().zip(&mut frames) { + // Align in the target size. The target size depends on whether we + // should expand. + let target = regions.expand.select(region, frame.size()); + let aligns = self + .aligns + .map(|align| align.resolve(styles)) + .unwrap_or(Axes::new(Align::Left, Align::Top)); + + frame.resize(target, aligns); + } + + Ok(frames) + } +} diff --git a/library/src/layout/columns.rs b/library/src/layout/columns.rs new file mode 100644 index 00000000..8eae922b --- /dev/null +++ b/library/src/layout/columns.rs @@ -0,0 +1,117 @@ +use crate::prelude::*; +use crate::text::TextNode; + +/// Separate a region into multiple equally sized columns. +#[derive(Debug, Hash)] +pub struct ColumnsNode { + /// How many columns there should be. + pub columns: NonZeroUsize, + /// The child to be layouted into the columns. Most likely, this should be a + /// flow or stack node. + pub child: Content, +} + +#[node(LayoutBlock)] +impl ColumnsNode { + /// The size of the gutter space between each column. + #[property(resolve)] + pub const GUTTER: Rel = Ratio::new(0.04).into(); + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(Self { + columns: args.expect("column count")?, + child: args.expect("body")?, + } + .pack()) + } +} + +impl LayoutBlock for ColumnsNode { + fn layout_block( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + // Separating the infinite space into infinite columns does not make + // much sense. + if !regions.first.x.is_finite() { + return self.child.layout_block(world, regions, styles); + } + + // Determine the width of the gutter and each column. + let columns = self.columns.get(); + let gutter = styles.get(Self::GUTTER).relative_to(regions.base.x); + let width = (regions.first.x - gutter * (columns - 1) as f64) / columns as f64; + + // Create the pod regions. + let pod = Regions { + first: Size::new(width, regions.first.y), + base: Size::new(width, regions.base.y), + backlog: std::iter::once(®ions.first.y) + .chain(regions.backlog.as_slice()) + .flat_map(|&height| std::iter::repeat(height).take(columns)) + .skip(1) + .collect(), + last: regions.last, + expand: Axes::new(true, regions.expand.y), + }; + + // Layout the children. + let mut frames = self.child.layout_block(world, &pod, styles)?.into_iter(); + let mut finished = vec![]; + + let dir = styles.get(TextNode::DIR); + 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.first.x, height)); + let mut cursor = Abs::zero(); + + for _ in 0 .. columns { + let frame = match frames.next() { + Some(frame) => frame, + None => break, + }; + + if !regions.expand.y { + output.size_mut().y.set_max(frame.height()); + } + + let width = frame.width(); + let x = if dir.is_positive() { + cursor + } else { + regions.first.x - cursor - width + }; + + output.push_frame(Point::with_x(x), frame); + cursor += width + gutter; + } + + finished.push(output); + } + + Ok(finished) + } +} + +/// A column break. +#[derive(Debug, Clone, Hash)] +pub struct ColbreakNode { + pub weak: bool, +} + +#[node] +impl ColbreakNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let weak = args.named("weak")?.unwrap_or(false); + Ok(Self { weak }.pack()) + } +} diff --git a/library/src/layout/container.rs b/library/src/layout/container.rs new file mode 100644 index 00000000..d65b78b6 --- /dev/null +++ b/library/src/layout/container.rs @@ -0,0 +1,80 @@ +use crate::prelude::*; + +/// An inline-level container that sizes content. +#[derive(Debug, Clone, Hash)] +pub struct BoxNode { + /// How to size the content horizontally and vertically. + pub sizing: Axes>>, + /// The content to be sized. + pub child: Content, +} + +#[node(LayoutInline)] +impl BoxNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let width = args.named("width")?; + let height = args.named("height")?; + let body = args.eat::()?.unwrap_or_default(); + Ok(body.boxed(Axes::new(width, height))) + } +} + +impl LayoutInline for BoxNode { + fn layout_inline( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + // The "pod" is the region into which the child will be layouted. + let pod = { + // Resolve the sizing to a concrete size. + let size = self + .sizing + .resolve(styles) + .zip(regions.base) + .map(|(s, b)| s.map(|v| v.relative_to(b))) + .unwrap_or(regions.first); + + // Select the appropriate base and expansion for the child depending + // on whether it is automatically or relatively sized. + let is_auto = self.sizing.as_ref().map(Option::is_none); + let base = is_auto.select(regions.base, size); + let expand = regions.expand | !is_auto; + + Regions::one(size, base, expand) + }; + + // Layout the child. + let mut frames = self.child.layout_inline(world, &pod, styles)?; + + // Ensure frame size matches regions size if expansion is on. + let frame = &mut frames[0]; + let target = regions.expand.select(regions.first, frame.size()); + frame.resize(target, Align::LEFT_TOP); + + Ok(frames) + } +} + +/// A block-level container that places content into a separate flow. +#[derive(Debug, Clone, Hash)] +pub struct BlockNode(pub Content); + +#[node(LayoutBlock)] +impl BlockNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.eat()?.unwrap_or_default()).pack()) + } +} + +impl LayoutBlock for BlockNode { + fn layout_block( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + self.0.layout_block(world, regions, styles) + } +} diff --git a/library/src/layout/flow.rs b/library/src/layout/flow.rs new file mode 100644 index 00000000..a5992796 --- /dev/null +++ b/library/src/layout/flow.rs @@ -0,0 +1,267 @@ +use std::cmp::Ordering; + +use super::{AlignNode, PlaceNode, Spacing}; +use crate::prelude::*; +use crate::text::ParNode; + +/// Arrange spacing, paragraphs and block-level nodes into a flow. +/// +/// This node is reponsible for layouting both the top-level content flow and +/// the contents of boxes. +#[derive(Hash)] +pub struct FlowNode(pub StyleVec); + +/// A child of a flow node. +#[derive(Hash, PartialEq)] +pub enum FlowChild { + /// Vertical spacing between other children. + Spacing(Spacing), + /// Arbitrary block-level content. + Block(Content), + /// A column / region break. + Colbreak, +} + +#[node(LayoutBlock)] +impl FlowNode {} + +impl LayoutBlock for FlowNode { + fn layout_block( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + let mut layouter = FlowLayouter::new(regions); + + for (child, map) in self.0.iter() { + let styles = map.chain(&styles); + match child { + FlowChild::Spacing(kind) => { + layouter.layout_spacing(*kind, styles); + } + FlowChild::Block(block) => { + layouter.layout_block(world, block, styles)?; + } + FlowChild::Colbreak => { + layouter.finish_region(); + } + } + } + + Ok(layouter.finish()) + } +} + +impl Debug for FlowNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Flow ")?; + self.0.fmt(f) + } +} + +impl Debug for FlowChild { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Spacing(kind) => write!(f, "{:?}", kind), + Self::Block(block) => block.fmt(f), + Self::Colbreak => f.pad("Colbreak"), + } + } +} + +impl PartialOrd for FlowChild { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (Self::Spacing(a), Self::Spacing(b)) => a.partial_cmp(b), + _ => None, + } + } +} + +/// Performs flow layout. +pub struct FlowLayouter { + /// The regions to layout children into. + regions: Regions, + /// Whether the flow should expand to fill the region. + expand: Axes, + /// The full size of `regions.size` that was available before we started + /// subtracting. + full: Size, + /// The size used by the frames for the current region. + used: Size, + /// The sum of fractions in the current region. + fr: Fr, + /// Spacing and layouted blocks. + items: Vec, + /// Finished frames for previous regions. + finished: Vec, +} + +/// A prepared item in a flow layout. +enum FlowItem { + /// Absolute spacing between other items. + Absolute(Abs), + /// Fractional spacing between other items. + Fractional(Fr), + /// A frame for a layouted block and how to align it. + Frame(Frame, Axes), + /// An absolutely placed frame. + Placed(Frame), +} + +impl FlowLayouter { + /// Create a new flow layouter. + pub fn new(regions: &Regions) -> Self { + let expand = regions.expand; + let full = regions.first; + + // Disable vertical expansion for children. + let mut regions = regions.clone(); + regions.expand.y = false; + + Self { + regions, + expand, + full, + used: Size::zero(), + fr: Fr::zero(), + items: vec![], + finished: vec![], + } + } + + /// Layout spacing. + pub fn layout_spacing(&mut self, spacing: Spacing, styles: StyleChain) { + match spacing { + Spacing::Relative(v) => { + // Resolve the spacing and limit it to the remaining space. + let resolved = v.resolve(styles).relative_to(self.full.y); + let limited = resolved.min(self.regions.first.y); + self.regions.first.y -= limited; + self.used.y += limited; + self.items.push(FlowItem::Absolute(resolved)); + } + Spacing::Fractional(v) => { + self.items.push(FlowItem::Fractional(v)); + self.fr += v; + } + } + } + + /// Layout a block. + pub fn layout_block( + &mut self, + world: Tracked, + block: &Content, + styles: StyleChain, + ) -> SourceResult<()> { + // Don't even try layouting into a full region. + if self.regions.is_full() { + self.finish_region(); + } + + // Placed nodes that are out of flow produce placed items which aren't + // aligned later. + if let Some(placed) = block.downcast::() { + if placed.out_of_flow() { + let frame = block.layout_block(world, &self.regions, styles)?.remove(0); + self.items.push(FlowItem::Placed(frame)); + return Ok(()); + } + } + + // How to align the block. + let aligns = Axes::new( + // For non-expanding paragraphs it is crucial that we align the + // whole paragraph as it is itself aligned. + styles.get(ParNode::ALIGN), + // Vertical align node alignment is respected by the flow. + block + .downcast::() + .and_then(|aligned| aligned.aligns.y) + .map(|align| align.resolve(styles)) + .unwrap_or(Align::Top), + ); + + let frames = block.layout_block(world, &self.regions, styles)?; + let len = frames.len(); + for (i, mut frame) in frames.into_iter().enumerate() { + // Set the generic block role. + frame.apply_role(Role::GenericBlock); + + // Grow our size, shrink the region and save the frame for later. + let size = frame.size(); + self.used.y += size.y; + self.used.x.set_max(size.x); + self.regions.first.y -= size.y; + self.items.push(FlowItem::Frame(frame, aligns)); + + if i + 1 < len { + self.finish_region(); + } + } + + Ok(()) + } + + /// Finish the frame for one region. + pub fn finish_region(&mut self) { + // Determine the size of the flow in this region dependening on whether + // the region expands. + let mut size = self.expand.select(self.full, self.used); + + // Account for fractional spacing in the size calculation. + let remaining = self.full.y - self.used.y; + if self.fr.get() > 0.0 && self.full.y.is_finite() { + self.used.y = self.full.y; + size.y = self.full.y; + } + + let mut output = Frame::new(size); + let mut offset = Abs::zero(); + let mut ruler = Align::Top; + + // Place all frames. + for item in self.items.drain(..) { + match item { + FlowItem::Absolute(v) => { + offset += v; + } + FlowItem::Fractional(v) => { + offset += v.share(self.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 - self.used.y); + let pos = Point::new(x, y); + offset += frame.height(); + output.push_frame(pos, frame); + } + FlowItem::Placed(frame) => { + output.push_frame(Point::zero(), frame); + } + } + } + + // Advance to the next region. + self.regions.next(); + self.full = self.regions.first; + self.used = Size::zero(); + self.fr = Fr::zero(); + self.finished.push(output); + } + + /// Finish layouting and return the resulting frames. + pub fn finish(mut self) -> Vec { + if self.expand.y { + while !self.regions.backlog.is_empty() { + self.finish_region(); + } + } + + self.finish_region(); + self.finished + } +} diff --git a/library/src/layout/grid.rs b/library/src/layout/grid.rs new file mode 100644 index 00000000..f6610d78 --- /dev/null +++ b/library/src/layout/grid.rs @@ -0,0 +1,600 @@ +use crate::prelude::*; + +/// Arrange content in a grid. +#[derive(Debug, Hash)] +pub struct GridNode { + /// Defines sizing for content rows and columns. + pub tracks: Axes>, + /// Defines sizing of gutter rows and columns between content. + pub gutter: Axes>, + /// The content to be arranged in a grid. + pub cells: Vec, +} + +#[node(LayoutBlock)] +impl GridNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let TrackSizings(columns) = args.named("columns")?.unwrap_or_default(); + let TrackSizings(rows) = args.named("rows")?.unwrap_or_default(); + let TrackSizings(base_gutter) = args.named("gutter")?.unwrap_or_default(); + let column_gutter = args.named("column-gutter")?.map(|TrackSizings(v)| v); + let row_gutter = args.named("row-gutter")?.map(|TrackSizings(v)| v); + Ok(Self { + tracks: Axes::new(columns, rows), + gutter: Axes::new( + column_gutter.unwrap_or_else(|| base_gutter.clone()), + row_gutter.unwrap_or(base_gutter), + ), + cells: args.all()?, + } + .pack()) + } +} + +impl LayoutBlock for GridNode { + fn layout_block( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + // Prepare grid layout by unifying content and gutter tracks. + let layouter = GridLayouter::new( + world, + self.tracks.as_deref(), + self.gutter.as_deref(), + &self.cells, + regions, + styles, + ); + + // Measure the columns and layout the grid row-by-row. + layouter.layout() + } +} + +/// Defines how to size a grid cell along an axis. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum TrackSizing { + /// A track that fits its cell's contents. + Auto, + /// A track size specified in absolute terms and relative to the parent's + /// size. + Relative(Rel), + /// A track size specified as a fraction of the remaining free space in the + /// parent. + Fractional(Fr), +} + +/// Track sizing definitions. +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct TrackSizings(pub Vec); + +castable! { + TrackSizings, + Expected: "integer, auto, relative length, fraction, or array of the latter three", + Value::Auto => Self(vec![TrackSizing::Auto]), + Value::Length(v) => Self(vec![TrackSizing::Relative(v.into())]), + Value::Ratio(v) => Self(vec![TrackSizing::Relative(v.into())]), + Value::Relative(v) => Self(vec![TrackSizing::Relative(v)]), + Value::Fraction(v) => Self(vec![TrackSizing::Fractional(v)]), + Value::Int(v) => Self(vec![ + TrackSizing::Auto; + Value::Int(v).cast::()?.get() + ]), + Value::Array(values) => Self(values + .into_iter() + .filter_map(|v| v.cast().ok()) + .collect()), +} + +castable! { + TrackSizing, + Expected: "auto, relative length, or fraction", + Value::Auto => Self::Auto, + Value::Length(v) => Self::Relative(v.into()), + Value::Ratio(v) => Self::Relative(v.into()), + Value::Relative(v) => Self::Relative(v), + Value::Fraction(v) => Self::Fractional(v), +} + +/// Performs grid layout. +pub struct GridLayouter<'a> { + /// The core context. + world: Tracked<'a, dyn World>, + /// The grid cells. + cells: &'a [Content], + /// The column tracks including gutter tracks. + cols: Vec, + /// The row tracks including gutter tracks. + rows: Vec, + /// The regions to layout children into. + regions: Regions, + /// The inherited styles. + styles: StyleChain<'a>, + /// Resolved column sizes. + rcols: Vec, + /// Rows in the current region. + lrows: Vec, + /// The full height of the current region. + full: Abs, + /// The used-up size of the current region. The horizontal size is + /// determined once after columns are resolved and not touched again. + used: Size, + /// The sum of fractions in the current region. + fr: Fr, + /// Frames for finished regions. + finished: Vec, +} + +/// 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. + Frame(Frame), + /// 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( + world: Tracked<'a, dyn World>, + tracks: Axes<&[TrackSizing]>, + gutter: Axes<&[TrackSizing]>, + cells: &'a [Content], + regions: &Regions, + 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 auto = TrackSizing::Auto; + let zero = TrackSizing::Relative(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)); + 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)); + rows.push(get_or(gutter.y, y, zero)); + } + + // Remove superfluous gutter tracks. + cols.pop(); + rows.pop(); + + let full = regions.first.y; + let rcols = vec![Abs::zero(); cols.len()]; + let lrows = vec![]; + + // We use the regions for auto row measurement. Since at that moment, + // columns are already sized, we can enable horizontal expansion. + let mut regions = regions.clone(); + regions.expand = Axes::new(true, false); + + Self { + world, + cells, + cols, + rows, + regions, + styles, + rcols, + lrows, + full, + used: Size::zero(), + fr: Fr::zero(), + finished: vec![], + } + } + + /// Determines the columns sizes and then layouts the grid row-by-row. + pub fn layout(mut self) -> SourceResult> { + self.measure_columns()?; + + 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 y % 2 == 0 && self.regions.is_full() { + self.finish_region()?; + } + + match self.rows[y] { + TrackSizing::Auto => self.layout_auto_row(y)?, + TrackSizing::Relative(v) => self.layout_relative_row(v, y)?, + TrackSizing::Fractional(v) => { + self.lrows.push(Row::Fr(v, y)); + self.fr += v; + } + } + } + + self.finish_region()?; + Ok(self.finished) + } + + /// Determine all column sizes. + fn measure_columns(&mut self) -> 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 { + TrackSizing::Auto => {} + TrackSizing::Relative(v) => { + let resolved = + v.resolve(self.styles).relative_to(self.regions.base.x); + *rcol = resolved; + rel += resolved; + } + TrackSizing::Fractional(v) => fr += v, + } + } + + // Size that is not used by fixed-size columns. + let available = self.regions.first.x - rel; + if available >= Abs::zero() { + // Determine size of auto columns. + let (auto, count) = self.measure_auto_columns(available)?; + + // If there is remaining space, distribute it to fractional columns, + // otherwise shrink auto columns. + let remaining = available - auto; + if remaining >= Abs::zero() { + if !fr.is_zero() { + self.grow_fractional_columns(remaining, fr); + } + } else { + self.shrink_auto_columns(available, count); + } + } + + // Sum up the resolved column sizes once here. + self.used.x = self.rcols.iter().sum(); + + Ok(()) + } + + /// Measure the size that is available to auto columns. + fn measure_auto_columns(&mut self, 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 != TrackSizing::Auto { + continue; + } + + let mut resolved = Abs::zero(); + for y in 0 .. self.rows.len() { + if let Some(cell) = self.cell(x, y) { + let size = Size::new(available, self.regions.base.y); + let mut pod = + Regions::one(size, self.regions.base, Axes::splat(false)); + + // For relative rows, we can already resolve the correct + // base, for auto it's already correct and for fr we could + // only guess anyway. + if let TrackSizing::Relative(v) = self.rows[y] { + pod.base.y = + v.resolve(self.styles).relative_to(self.regions.base.y); + } + + let frame = + cell.layout_block(self.world, &pod, self.styles)?.remove(0); + 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) { + for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { + if let TrackSizing::Fractional(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) { + // The fair share each auto column may have. + let fair = available / count as f64; + + // The number of overlarge auto columns and the space that will be + // equally redistributed to them. + let mut overlarge: usize = 0; + let mut redistribute = available; + + // Find out the number of and space used by overlarge auto columns. + for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { + if col == TrackSizing::Auto { + if *rcol > fair { + overlarge += 1; + } else { + redistribute -= *rcol; + } + } + } + + // Redistribute the space equally. + let share = redistribute / overlarge as f64; + for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { + if col == TrackSizing::Auto && *rcol > fair { + *rcol = share; + } + } + } + + /// Layout a row with automatic height. Such a row may break across multiple + /// regions. + fn layout_auto_row(&mut self, y: usize) -> SourceResult<()> { + let mut resolved: Vec = vec![]; + + // Determine the size for each region of the row. + for (x, &rcol) in self.rcols.iter().enumerate() { + if let Some(cell) = self.cell(x, y) { + let mut pod = self.regions.clone(); + pod.first.x = rcol; + pod.base.x = rcol; + + // All widths should be `rcol` except the base for auto columns. + if self.cols[x] == TrackSizing::Auto { + pod.base.x = self.regions.base.x; + } + + let mut sizes = cell + .layout_block(self.world, &pod, self.styles)? + .into_iter() + .map(|frame| frame.height()); + + // For each region, we want to know the maximum height any + // column requires. + 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); + } + } + + // 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(first, y)?; + self.push_row(frame); + return Ok(()); + } + + // Expand all but the last region if the space is not + // eaten up by any fr rows. + if self.fr.is_zero() { + let len = resolved.len(); + for (region, target) in self.regions.iter().zip(&mut resolved[.. len - 1]) { + target.set_max(region.y); + } + } + + // Layout into multiple regions. + let frames = self.layout_multi_row(&resolved, y)?; + let len = frames.len(); + for (i, frame) in frames.into_iter().enumerate() { + self.push_row(frame); + if i + 1 < len { + self.finish_region()?; + } + } + + Ok(()) + } + + /// 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, v: Rel, y: usize) -> SourceResult<()> { + let resolved = v.resolve(self.styles).relative_to(self.regions.base.y); + let frame = self.layout_single_row(resolved, y)?; + + // Skip to fitting region. + let height = frame.height(); + while !self.regions.first.y.fits(height) && !self.regions.in_last() { + self.finish_region()?; + + // Don't skip multiple regions for gutter and don't push a row. + if y % 2 == 1 { + return Ok(()); + } + } + + self.push_row(frame); + + Ok(()) + } + + /// Layout a row with fixed height and return its frame. + fn layout_single_row(&mut self, height: Abs, y: usize) -> SourceResult { + let mut output = Frame::new(Size::new(self.used.x, 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); + + // Set the base to the region's base for auto rows and to the + // size for relative and fractional rows. + let base = Axes::new(self.cols[x], self.rows[y]) + .map(|s| s == TrackSizing::Auto) + .select(self.regions.base, size); + + let pod = Regions::one(size, base, Axes::splat(true)); + let frame = cell.layout_block(self.world, &pod, self.styles)?.remove(0); + match frame.role() { + Some(Role::ListLabel | Role::ListItemBody) => { + output.apply_role(Role::ListItem) + } + Some(Role::TableCell) => output.apply_role(Role::TableRow), + _ => {} + } + + output.push_frame(pos, frame); + } + + pos.x += rcol; + } + + Ok(output) + } + + /// Layout a row spanning multiple regions. + fn layout_multi_row( + &mut self, + heights: &[Abs], + y: usize, + ) -> SourceResult> { + // Prepare frames. + let mut outputs: Vec<_> = heights + .iter() + .map(|&h| Frame::new(Size::new(self.used.x, h))) + .collect(); + + // Prepare regions. + let size = Size::new(self.used.x, heights[0]); + let mut pod = Regions::one(size, self.regions.base, Axes::splat(true)); + pod.backlog = heights[1 ..].to_vec(); + + // 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.first.x = rcol; + pod.base.x = rcol; + + // All widths should be `rcol` except the base for auto columns. + if self.cols[x] == TrackSizing::Auto { + pod.base.x = self.regions.base.x; + } + + // Push the layouted frames into the individual output frames. + let frames = cell.layout_block(self.world, &pod, self.styles)?; + for (output, frame) in outputs.iter_mut().zip(frames) { + match frame.role() { + Some(Role::ListLabel | Role::ListItemBody) => { + output.apply_role(Role::ListItem) + } + Some(Role::TableCell) => output.apply_role(Role::TableRow), + _ => {} + } + output.push_frame(pos, frame); + } + } + + pos.x += rcol; + } + + Ok(outputs) + } + + /// Push a row frame into the current region. + fn push_row(&mut self, frame: Frame) { + self.regions.first.y -= frame.height(); + self.used.y += frame.height(); + self.lrows.push(Row::Frame(frame)); + } + + /// Finish rows for one region. + fn finish_region(&mut self) -> SourceResult<()> { + // Determine the size of the grid in this region, expanding fully if + // there are fr rows. + let mut size = self.used; + if self.fr.get() > 0.0 && self.full.is_finite() { + size.y = self.full; + } + + // The frame for the region. + let mut output = Frame::new(size); + let mut pos = Point::zero(); + + // Place finished rows and layout fractional rows. + for row in std::mem::take(&mut self.lrows) { + let frame = match row { + Row::Frame(frame) => frame, + Row::Fr(v, y) => { + let remaining = self.full - self.used.y; + let height = v.share(self.fr, remaining); + self.layout_single_row(height, y)? + } + }; + + let height = frame.height(); + output.push_frame(pos, frame); + pos.y += height; + } + + self.finished.push(output); + self.regions.next(); + self.full = self.regions.first.y; + self.used.y = Abs::zero(); + self.fr = Fr::zero(); + + 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, x: usize, y: usize) -> Option<&'a Content> { + assert!(x < self.cols.len()); + assert!(y < self.rows.len()); + + // 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 + } + } +} diff --git a/library/src/layout/mod.rs b/library/src/layout/mod.rs new file mode 100644 index 00000000..ddfaa351 --- /dev/null +++ b/library/src/layout/mod.rs @@ -0,0 +1,835 @@ +//! Composable layouts. + +mod align; +mod columns; +mod container; +mod flow; +mod grid; +mod pad; +mod page; +mod place; +mod spacing; +mod stack; +mod transform; + +pub use align::*; +pub use columns::*; +pub use container::*; +pub use flow::*; +pub use grid::*; +pub use pad::*; +pub use page::*; +pub use place::*; +pub use spacing::*; +pub use stack::*; +pub use transform::*; + +use std::mem; + +use comemo::Tracked; +use typed_arena::Arena; +use typst::diag::SourceResult; +use typst::frame::Frame; +use typst::geom::*; +use typst::model::{ + capability, Barrier, Content, Node, SequenceNode, Show, StyleChain, StyleEntry, + StyleMap, StyleVec, StyleVecBuilder, StyledNode, Target, +}; +use typst::World; + +use crate::structure::{ + DescNode, DocNode, EnumNode, ListItem, ListNode, DESC, ENUM, LIST, +}; +use crate::text::{ + LinebreakNode, ParChild, ParNode, ParbreakNode, SmartQuoteNode, SpaceNode, TextNode, +}; + +/// The root-level layout. +#[capability] +pub trait Layout: 'static + Sync + Send { + /// Layout into one frame per page. + fn layout(&self, world: Tracked) -> SourceResult>; +} + +impl Layout for Content { + #[comemo::memoize] + fn layout(&self, world: Tracked) -> SourceResult> { + let styles = StyleChain::with_root(&world.config().styles); + let scratch = Scratch::default(); + + let mut builder = Builder::new(world, &scratch, true); + builder.accept(self, styles)?; + + let (doc, shared) = builder.into_doc(styles)?; + doc.layout(world, shared) + } +} + +/// Block-level layout. +#[capability] +pub trait LayoutBlock: 'static + Sync + Send { + /// Layout into one frame per region. + fn layout_block( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult>; +} + +impl LayoutBlock for Content { + #[comemo::memoize] + fn layout_block( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + if let Some(node) = self.to::() { + let barrier = StyleEntry::Barrier(Barrier::new(self.id())); + let styles = barrier.chain(&styles); + return node.layout_block(world, regions, styles); + } + + let scratch = Scratch::default(); + let mut builder = Builder::new(world, &scratch, false); + builder.accept(self, styles)?; + let (flow, shared) = builder.into_flow(styles)?; + flow.layout_block(world, regions, shared) + } +} + +/// Inline-level layout. +#[capability] +pub trait LayoutInline: 'static + Sync + Send { + /// Layout into a single frame. + fn layout_inline( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult>; +} + +impl LayoutInline for Content { + #[comemo::memoize] + fn layout_inline( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + if let Some(node) = self.to::() { + let barrier = StyleEntry::Barrier(Barrier::new(self.id())); + let styles = barrier.chain(&styles); + return node.layout_inline(world, regions, styles); + } + + if let Some(node) = self.to::() { + let barrier = StyleEntry::Barrier(Barrier::new(self.id())); + let styles = barrier.chain(&styles); + return node.layout_block(world, regions, styles); + } + + let scratch = Scratch::default(); + let mut builder = Builder::new(world, &scratch, false); + builder.accept(self, styles)?; + let (flow, shared) = builder.into_flow(styles)?; + flow.layout_block(world, regions, shared) + } +} + +/// A sequence of regions to layout into. +#[derive(Debug, Clone, Hash)] +pub struct Regions { + /// The (remaining) size of the first region. + pub first: Size, + /// The base size for relative sizing. + pub base: Size, + /// The height of followup regions. The width is the same for all regions. + pub backlog: Vec, + /// 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, + /// Whether nodes should expand to fill the regions instead of shrinking to + /// fit the content. + pub expand: Axes, +} + +impl Regions { + /// Create a new region sequence with exactly one region. + pub fn one(size: Size, base: Size, expand: Axes) -> Self { + Self { + first: size, + base, + backlog: vec![], + last: None, + expand, + } + } + + /// Create a new sequence of same-size regions that repeats indefinitely. + pub fn repeat(size: Size, base: Size, expand: Axes) -> Self { + Self { + first: size, + base, + backlog: vec![], + last: Some(size.y), + expand, + } + } + + /// 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(&self, mut f: F) -> Self + where + F: FnMut(Size) -> Size, + { + let x = self.first.x; + Self { + first: f(self.first), + base: f(self.base), + backlog: self.backlog.iter().map(|&y| f(Size::new(x, y)).y).collect(), + last: self.last.map(|y| f(Size::new(x, y)).y), + expand: self.expand, + } + } + + /// Whether the first region is full and a region break is called for. + pub fn is_full(&self) -> bool { + Abs::zero().fits(self.first.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.first.y == height) + } + + /// Advance to the next region if there is any. + pub fn next(&mut self) { + if let Some(height) = (!self.backlog.is_empty()) + .then(|| self.backlog.remove(0)) + .or(self.last) + { + self.first.y = height; + self.base.y = 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 iterater may be infinite. + pub fn iter(&self) -> impl Iterator + '_ { + let first = std::iter::once(self.first); + let backlog = self.backlog.iter(); + let last = self.last.iter().cycle(); + first.chain(backlog.chain(last).map(|&h| Size::new(self.first.x, h))) + } +} + +/// Builds a document or a flow node from content. +struct Builder<'a> { + /// The core context. + world: Tracked<'a, dyn World>, + /// Scratch arenas for building. + scratch: &'a Scratch<'a>, + /// The current document building state. + doc: Option>, + /// 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>, + /// An arena where intermediate content resulting from show rules is stored. + templates: Arena, +} + +/// Determines whether a style could interrupt some composable structure. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub enum Interruption { + /// The style forces a list break. + List, + /// The style forces a paragraph break. + Par, + /// The style forces a page break. + Page, +} + +impl<'a> Builder<'a> { + pub fn new( + world: Tracked<'a, dyn World>, + scratch: &'a Scratch<'a>, + top: bool, + ) -> Self { + Self { + world, + scratch, + doc: top.then(|| DocBuilder::default()), + flow: FlowBuilder::default(), + par: ParBuilder::default(), + list: ListBuilder::default(), + } + } + + pub fn into_doc( + mut self, + styles: StyleChain<'a>, + ) -> SourceResult<(DocNode, StyleChain<'a>)> { + self.interrupt(Interruption::Page, styles, true)?; + let (pages, shared) = self.doc.unwrap().pages.finish(); + Ok((DocNode(pages), shared)) + } + + pub fn into_flow( + mut self, + styles: StyleChain<'a>, + ) -> SourceResult<(FlowNode, StyleChain<'a>)> { + self.interrupt(Interruption::Par, styles, false)?; + let (children, shared) = self.flow.0.finish(); + Ok((FlowNode(children), shared)) + } + + pub fn accept( + &mut self, + content: &'a Content, + styles: StyleChain<'a>, + ) -> SourceResult<()> { + if let Some(text) = content.downcast::() { + if let Some(realized) = styles.apply(self.world, Target::Text(&text.0))? { + let stored = self.scratch.templates.alloc(realized); + return self.accept(stored, styles); + } + } else if let Some(styled) = content.downcast::() { + return self.styled(styled, styles); + } else if let Some(seq) = content.downcast::() { + return self.sequence(seq, styles); + } else if content.has::() && self.show(content, styles)? { + return Ok(()); + } + + if self.list.accept(content, styles) { + return Ok(()); + } + + self.interrupt(Interruption::List, styles, false)?; + + if content.is::() { + self.list.accept(content, styles); + return Ok(()); + } + + if self.par.accept(content, styles) { + return Ok(()); + } + + self.interrupt(Interruption::Par, styles, false)?; + + if self.flow.accept(content, styles) { + return Ok(()); + } + + let keep = content + .downcast::() + .map_or(false, |pagebreak| !pagebreak.weak); + self.interrupt(Interruption::Page, styles, keep)?; + + if let Some(doc) = &mut self.doc { + doc.accept(content, styles); + } + + // We might want to issue a warning or error for content that wasn't + // handled (e.g. a pagebreak in a flow building process). However, we + // don't have the spans here at the moment. + Ok(()) + } + + fn show( + &mut self, + content: &'a Content, + styles: StyleChain<'a>, + ) -> SourceResult { + if let Some(mut realized) = styles.apply(self.world, Target::Node(content))? { + let mut map = StyleMap::new(); + let barrier = Barrier::new(content.id()); + map.push(StyleEntry::Barrier(barrier)); + map.push(StyleEntry::Barrier(barrier)); + realized = realized.styled_with_map(map); + let stored = self.scratch.templates.alloc(realized); + self.accept(stored, styles)?; + Ok(true) + } else { + Ok(false) + } + } + + fn styled( + &mut self, + styled: &'a StyledNode, + styles: StyleChain<'a>, + ) -> SourceResult<()> { + let stored = self.scratch.styles.alloc(styles); + let styles = styled.map.chain(stored); + + let intr = if styled.map.interrupts::() { + Some(Interruption::Page) + } else if styled.map.interrupts::() { + Some(Interruption::Par) + } else if styled.map.interrupts::() + || styled.map.interrupts::() + || styled.map.interrupts::() + { + Some(Interruption::List) + } else { + None + }; + + if let Some(intr) = intr { + self.interrupt(intr, styles, false)?; + } + + self.accept(&styled.sub, styles)?; + + if let Some(intr) = intr { + self.interrupt(intr, styles, true)?; + } + + Ok(()) + } + + fn interrupt( + &mut self, + intr: Interruption, + styles: StyleChain<'a>, + keep: bool, + ) -> SourceResult<()> { + if intr >= Interruption::List && !self.list.is_empty() { + mem::take(&mut self.list).finish(self)?; + } + + if intr >= Interruption::Par && !self.par.is_empty() { + mem::take(&mut self.par).finish(self); + } + + if intr >= Interruption::Page { + if let Some(doc) = &mut self.doc { + if !self.flow.is_empty() || (doc.keep_next && keep) { + mem::take(&mut self.flow).finish(doc, styles); + } + doc.keep_next = !keep; + } + } + + Ok(()) + } + + fn sequence( + &mut self, + seq: &'a SequenceNode, + styles: StyleChain<'a>, + ) -> SourceResult<()> { + for content in &seq.0 { + self.accept(content, styles)?; + } + Ok(()) + } +} + +/// Accepts pagebreaks and pages. +struct DocBuilder<'a> { + /// The page runs built so far. + pages: StyleVecBuilder<'a, PageNode>, + /// Whether to keep a following page even if it is empty. + keep_next: bool, +} + +impl<'a> DocBuilder<'a> { + fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) { + if let Some(pagebreak) = content.downcast::() { + self.keep_next = !pagebreak.weak; + } + + if let Some(page) = content.downcast::() { + self.pages.push(page.clone(), styles); + self.keep_next = false; + } + } +} + +impl Default for DocBuilder<'_> { + fn default() -> Self { + Self { + pages: StyleVecBuilder::new(), + keep_next: true, + } + } +} + +/// Accepts flow content. +#[derive(Default)] +struct FlowBuilder<'a>(CollapsingBuilder<'a, FlowChild>); + +impl<'a> FlowBuilder<'a> { + fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { + // Weak flow elements: + // Weakness | Element + // 0 | weak colbreak + // 1 | weak fractional spacing + // 2 | weak spacing + // 3 | generated weak spacing + // 4 | generated weak fractional spacing + // 5 | par spacing + + if content.is::() { + /* Nothing to do */ + } else if let Some(colbreak) = content.downcast::() { + if colbreak.weak { + self.0.weak(FlowChild::Colbreak, styles, 0); + } else { + self.0.destructive(FlowChild::Colbreak, styles); + } + } else if let Some(vertical) = content.downcast::() { + let child = FlowChild::Spacing(vertical.amount); + let frac = vertical.amount.is_fractional(); + if vertical.weak { + let weakness = 1 + u8::from(frac) + 2 * u8::from(vertical.generated); + self.0.weak(child, styles, weakness); + } else if frac { + self.0.destructive(child, styles); + } else { + self.0.ignorant(child, styles); + } + } else if content.has::() { + let child = FlowChild::Block(content.clone()); + if content.is::() { + self.0.ignorant(child, styles); + } else { + self.0.supportive(child, styles); + } + } else { + return false; + } + + true + } + + fn par(&mut self, par: ParNode, styles: StyleChain<'a>, indent: bool) { + let amount = if indent && !styles.get(ParNode::SPACING_AND_INDENT) { + styles.get(ParNode::LEADING).into() + } else { + styles.get(ParNode::SPACING).into() + }; + + self.0.weak(FlowChild::Spacing(amount), styles, 5); + self.0.supportive(FlowChild::Block(par.pack()), styles); + self.0.weak(FlowChild::Spacing(amount), styles, 5); + } + + fn finish(self, doc: &mut DocBuilder<'a>, styles: StyleChain<'a>) { + let (flow, shared) = self.0.finish(); + let styles = if flow.is_empty() { styles } else { shared }; + let node = PageNode(FlowNode(flow).pack()); + doc.pages.push(node, styles); + } + + fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +/// Accepts paragraph content. +#[derive(Default)] +struct ParBuilder<'a>(CollapsingBuilder<'a, ParChild>); + +impl<'a> ParBuilder<'a> { + fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { + // Weak par elements: + // Weakness | Element + // 0 | weak fractional spacing + // 1 | weak spacing + // 2 | space + + if content.is::() { + self.0.weak(ParChild::Text(' '.into()), styles, 2); + } else if let Some(linebreak) = content.downcast::() { + let c = if linebreak.justify { '\u{2028}' } else { '\n' }; + self.0.destructive(ParChild::Text(c.into()), styles); + } else if let Some(horizontal) = content.downcast::() { + let child = ParChild::Spacing(horizontal.amount); + let frac = horizontal.amount.is_fractional(); + if horizontal.weak { + let weakness = u8::from(!frac); + self.0.weak(child, styles, weakness); + } else if frac { + self.0.destructive(child, styles); + } else { + self.0.ignorant(child, styles); + } + } else if let Some(quote) = content.downcast::() { + self.0.supportive(ParChild::Quote { double: quote.double }, styles); + } else if let Some(text) = content.downcast::() { + self.0.supportive(ParChild::Text(text.0.clone()), styles); + } else if content.has::() { + self.0.supportive(ParChild::Inline(content.clone()), styles); + } else { + return false; + } + + true + } + + fn finish(self, parent: &mut Builder<'a>) { + let (mut children, shared) = self.0.finish(); + if children.is_empty() { + return; + } + + // Paragraph indent should only apply if the paragraph starts with + // text and follows directly after another paragraph. + let indent = shared.get(ParNode::INDENT); + if !indent.is_zero() + && children + .items() + .find_map(|child| match child { + ParChild::Spacing(_) => None, + ParChild::Text(_) | ParChild::Quote { .. } => Some(true), + ParChild::Inline(_) => Some(false), + }) + .unwrap_or_default() + && parent + .flow + .0 + .items() + .rev() + .find_map(|child| match child { + FlowChild::Spacing(_) => None, + FlowChild::Block(content) => Some(content.is::()), + FlowChild::Colbreak => Some(false), + }) + .unwrap_or_default() + { + children.push_front(ParChild::Spacing(indent.into())); + } + + parent.flow.par(ParNode(children), shared, !indent.is_zero()); + } + + fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +/// Accepts list / enum items, spaces, paragraph breaks. +struct ListBuilder<'a> { + /// The list items collected so far. + items: StyleVecBuilder<'a, ListItem>, + /// Whether the list contains no paragraph breaks. + tight: bool, + /// Whether the list can be attached. + attachable: 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() { + if content.is::() { + self.attachable = false; + } else if !content.is::() && !content.is::() { + self.attachable = true; + } + } + + if let Some(item) = content.downcast::() { + if self + .items + .items() + .next() + .map_or(true, |first| item.kind() == first.kind()) + { + self.items.push(item.clone(), styles); + self.tight &= self.staged.drain(..).all(|(t, _)| !t.is::()); + } else { + return false; + } + } else if !self.items.is_empty() + && (content.is::() || content.is::()) + { + self.staged.push((content, styles)); + } else { + return false; + } + + true + } + + fn finish(self, parent: &mut Builder<'a>) -> SourceResult<()> { + let (items, shared) = self.items.finish(); + let kind = match items.items().next() { + Some(item) => item.kind(), + None => return Ok(()), + }; + + let tight = self.tight; + let attached = tight && self.attachable; + let content = match kind { + LIST => ListNode:: { tight, attached, items }.pack(), + ENUM => ListNode:: { tight, attached, items }.pack(), + DESC | _ => ListNode:: { tight, attached, items }.pack(), + }; + + let stored = parent.scratch.templates.alloc(content); + parent.accept(stored, shared)?; + + for (content, styles) in self.staged { + parent.accept(content, styles)?; + } + + parent.list.attachable = true; + + Ok(()) + } + + fn is_empty(&self) -> bool { + self.items.is_empty() + } +} + +impl Default for ListBuilder<'_> { + fn default() -> Self { + Self { + items: StyleVecBuilder::default(), + tight: true, + attachable: true, + staged: vec![], + } + } +} + +/// A wrapper around a [`StyleVecBuilder`] that allows to collapse items. +struct CollapsingBuilder<'a, T> { + /// The internal builder. + builder: StyleVecBuilder<'a, T>, + /// Staged weak and ignorant items that we can't yet commit to the builder. + /// The option is `Some(_)` for weak items and `None` for ignorant items. + staged: Vec<(T, StyleChain<'a>, Option)>, + /// What the last non-ignorant item was. + last: Last, +} + +/// What the last non-ignorant item was. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum Last { + Weak, + Destructive, + Supportive, +} + +impl<'a, T> CollapsingBuilder<'a, T> { + /// Create a new style-vec builder. + pub fn new() -> Self { + Self { + builder: StyleVecBuilder::new(), + staged: vec![], + last: Last::Destructive, + } + } + + /// Whether the builder is empty. + pub fn is_empty(&self) -> bool { + self.builder.is_empty() && self.staged.is_empty() + } + + /// Can only exist when there is at least one supportive item to its left + /// and to its right, with no destructive items in between. There may be + /// ignorant items in between in both directions. + /// + /// Between weak items, there may be at least one per layer and among the + /// candidates the strongest one (smallest `weakness`) wins. When tied, + /// the one that compares larger through `PartialOrd` wins. + pub fn weak(&mut self, item: T, styles: StyleChain<'a>, weakness: u8) + where + T: PartialOrd, + { + if self.last == Last::Destructive { + return; + } + + if self.last == Last::Weak { + if let Some(i) = + self.staged.iter().position(|(prev_item, _, prev_weakness)| { + prev_weakness.map_or(false, |prev_weakness| { + weakness < prev_weakness + || (weakness == prev_weakness && item > *prev_item) + }) + }) + { + self.staged.remove(i); + } else { + return; + } + } + + self.staged.push((item, styles, Some(weakness))); + self.last = Last::Weak; + } + + /// Forces nearby weak items to collapse. + pub fn destructive(&mut self, item: T, styles: StyleChain<'a>) { + self.flush(false); + self.builder.push(item, styles); + self.last = Last::Destructive; + } + + /// Allows nearby weak items to exist. + pub fn supportive(&mut self, item: T, styles: StyleChain<'a>) { + self.flush(true); + self.builder.push(item, styles); + self.last = Last::Supportive; + } + + /// Has no influence on other items. + pub fn ignorant(&mut self, item: T, styles: StyleChain<'a>) { + self.staged.push((item, styles, None)); + } + + /// Iterate over the contained items. + pub fn items(&self) -> impl DoubleEndedIterator { + self.builder.items().chain(self.staged.iter().map(|(item, ..)| item)) + } + + /// Return the finish style vec and the common prefix chain. + pub fn finish(mut self) -> (StyleVec, StyleChain<'a>) { + self.flush(false); + self.builder.finish() + } + + /// Push the staged items, filtering out weak items if `supportive` is + /// false. + fn flush(&mut self, supportive: bool) { + for (item, styles, meta) in self.staged.drain(..) { + if supportive || meta.is_none() { + self.builder.push(item, styles); + } + } + } +} + +impl<'a, T> Default for CollapsingBuilder<'a, T> { + fn default() -> Self { + Self::new() + } +} diff --git a/library/src/layout/pad.rs b/library/src/layout/pad.rs new file mode 100644 index 00000000..318d9f27 --- /dev/null +++ b/library/src/layout/pad.rs @@ -0,0 +1,83 @@ +use crate::prelude::*; + +/// Pad content at the sides. +#[derive(Debug, Hash)] +pub struct PadNode { + /// The amount of padding. + pub padding: Sides>, + /// The content whose sides to pad. + pub child: Content, +} + +#[node(LayoutBlock)] +impl PadNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let all = args.named("rest")?.or(args.find()?); + let x = args.named("x")?; + let y = args.named("y")?; + let left = args.named("left")?.or(x).or(all).unwrap_or_default(); + let top = args.named("top")?.or(y).or(all).unwrap_or_default(); + let right = args.named("right")?.or(x).or(all).unwrap_or_default(); + let bottom = args.named("bottom")?.or(y).or(all).unwrap_or_default(); + let body = args.expect::("body")?; + let padding = Sides::new(left, top, right, bottom); + Ok(body.padded(padding)) + } +} + +impl LayoutBlock for PadNode { + fn layout_block( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + // Layout child into padded regions. + let padding = self.padding.resolve(styles); + let pod = regions.map(|size| shrink(size, padding)); + let mut frames = self.child.layout_block(world, &pod, styles)?; + + for frame in &mut frames { + // 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(frames) + } +} + +/// Shrink a size by padding relative to the size itself. +fn shrink(size: Size, padding: Sides>) -> 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>) -> Size { + size.zip(padding.sum_by_axis()) + .map(|(s, p)| (s + p.abs).safe_div(1.0 - p.rel.get())) +} diff --git a/library/src/layout/page.rs b/library/src/layout/page.rs new file mode 100644 index 00000000..53a8cbc7 --- /dev/null +++ b/library/src/layout/page.rs @@ -0,0 +1,421 @@ +use std::str::FromStr; + +use super::ColumnsNode; +use crate::prelude::*; + +/// Layouts its child onto one or multiple pages. +#[derive(PartialEq, Clone, Hash)] +pub struct PageNode(pub Content); + +#[node] +impl PageNode { + /// The unflipped width of the page. + #[property(resolve)] + pub const WIDTH: Smart = Smart::Custom(Paper::A4.width().into()); + /// The unflipped height of the page. + #[property(resolve)] + pub const HEIGHT: Smart = Smart::Custom(Paper::A4.height().into()); + /// Whether the page is flipped into landscape orientation. + pub const FLIPPED: bool = false; + + /// The page's margins. + #[property(fold)] + pub const MARGINS: Sides>>> = Sides::splat(Smart::Auto); + + /// How many columns the page has. + pub const COLUMNS: NonZeroUsize = NonZeroUsize::new(1).unwrap(); + /// The page's background color. + pub const FILL: Option = None; + + /// The page's header. + #[property(referenced)] + pub const HEADER: Marginal = Marginal::None; + /// The page's footer. + #[property(referenced)] + pub const FOOTER: Marginal = Marginal::None; + /// Content in the page's background. + #[property(referenced)] + pub const BACKGROUND: Marginal = Marginal::None; + /// Content in the page's foreground. + #[property(referenced)] + pub const FOREGROUND: Marginal = Marginal::None; + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("body")?).pack()) + } + + fn set(...) { + if let Some(paper) = args.named_or_find::("paper")? { + styles.set(Self::WIDTH, Smart::Custom(paper.width().into())); + styles.set(Self::HEIGHT, Smart::Custom(paper.height().into())); + } + } +} + +impl PageNode { + /// Layout the page run into a sequence of frames, one per page. + pub fn layout( + &self, + world: Tracked, + mut page: usize, + styles: StyleChain, + ) -> SourceResult> { + // When one of the lengths is infinite the page fits its content along + // that axis. + let width = styles.get(Self::WIDTH).unwrap_or(Abs::inf()); + let height = styles.get(Self::HEIGHT).unwrap_or(Abs::inf()); + let mut size = Size::new(width, height); + if styles.get(Self::FLIPPED) { + 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::from(0.1190 * min); + let padding = styles.get(Self::MARGINS).map(|side| side.unwrap_or(default)); + + let mut child = self.0.clone(); + + // Realize columns. + let columns = styles.get(Self::COLUMNS); + if columns.get() > 1 { + child = ColumnsNode { columns, child: self.0.clone() }.pack(); + } + + // Realize margins. + child = child.padded(padding); + + // Realize background fill. + if let Some(fill) = styles.get(Self::FILL) { + child = child.filled(fill); + } + + // Layout the child. + let regions = Regions::repeat(size, size, size.map(Abs::is_finite)); + let mut frames = child.layout_block(world, ®ions, styles)?; + + let header = styles.get(Self::HEADER); + let footer = styles.get(Self::FOOTER); + let foreground = styles.get(Self::FOREGROUND); + let background = styles.get(Self::BACKGROUND); + + // Realize overlays. + for frame in &mut frames { + let size = frame.size(); + let pad = padding.resolve(styles).relative_to(size); + let pw = size.x - pad.left - pad.right; + let py = size.y - pad.bottom; + for (role, marginal, pos, area) in [ + ( + Role::Header, + header, + Point::with_x(pad.left), + Size::new(pw, pad.top), + ), + ( + Role::Footer, + footer, + Point::new(pad.left, py), + Size::new(pw, pad.bottom), + ), + (Role::Foreground, foreground, Point::zero(), size), + (Role::Background, background, Point::zero(), size), + ] { + if let Some(content) = marginal.resolve(world, page)? { + let pod = Regions::one(area, area, Axes::splat(true)); + let mut sub = content.layout_block(world, &pod, styles)?.remove(0); + sub.apply_role(role); + + if role == Role::Background { + frame.prepend_frame(pos, sub); + } else { + frame.push_frame(pos, sub); + } + } + } + + page += 1; + } + + Ok(frames) + } +} + +impl Debug for PageNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Page(")?; + self.0.fmt(f)?; + f.write_str(")") + } +} + +/// A page break. +#[derive(Debug, Copy, Clone, Hash)] +pub struct PagebreakNode { + pub weak: bool, +} + +#[node] +impl PagebreakNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let weak = args.named("weak")?.unwrap_or(false); + Ok(Self { weak }.pack()) + } +} + +/// A header, footer, foreground or background definition. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum Marginal { + /// Nothing, + None, + /// Bare content. + Content(Content), + /// A closure mapping from a page number to content. + Func(Func, Span), +} + +impl Marginal { + /// Resolve the marginal based on the page number. + pub fn resolve( + &self, + world: Tracked, + page: usize, + ) -> SourceResult> { + Ok(match self { + Self::None => None, + Self::Content(content) => Some(content.clone()), + Self::Func(func, span) => { + let args = Args::new(*span, [Value::Int(page as i64)]); + Some(func.call_detached(world, args)?.display(world)) + } + }) + } +} + +impl Cast> for Marginal { + fn is(value: &Spanned) -> bool { + matches!(&value.v, Value::Content(_) | Value::Func(_)) + } + + fn cast(value: Spanned) -> StrResult { + match value.v { + Value::None => Ok(Self::None), + Value::Str(v) => Ok(Self::Content(TextNode(v.into()).pack())), + Value::Content(v) => Ok(Self::Content(v)), + Value::Func(v) => Ok(Self::Func(v, value.span)), + v => Err(format!( + "expected none, content or function, found {}", + v.type_name(), + )), + } + } +} + +/// Specification of a paper. +#[derive(Debug, Copy, Clone)] +pub struct Paper { + /// The width of the paper in millimeters. + width: f64, + /// The height of the paper in millimeters. + height: f64, +} + +impl Paper { + /// The width of the paper. + pub fn width(self) -> Abs { + Abs::mm(self.width) + } + + /// The height of the paper. + pub fn height(self) -> Abs { + Abs::mm(self.height) + } +} + +/// Defines paper constants and a paper parsing implementation. +macro_rules! papers { + ($(($var:ident: $width:expr, $height: expr, $($pats:tt)*))*) => { + /// Predefined papers. + /// + /// Each paper is parsable from its name in kebab-case. + impl Paper { + $(pub const $var: Self = Self { width: $width, height: $height };)* + } + + impl FromStr for Paper { + type Err = &'static str; + + fn from_str(name: &str) -> Result { + match name.to_lowercase().as_str() { + $($($pats)* => Ok(Self::$var),)* + _ => Err("invalid paper name"), + } + } + } + }; +} + +castable! { + Paper, + Expected: "string", + Value::Str(string) => Self::from_str(&string)?, +} + +// 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/library/src/layout/place.rs b/library/src/layout/place.rs new file mode 100644 index 00000000..7d760ab6 --- /dev/null +++ b/library/src/layout/place.rs @@ -0,0 +1,56 @@ +use super::AlignNode; +use crate::prelude::*; + +/// Place content at an absolute position. +#[derive(Debug, Hash)] +pub struct PlaceNode(pub Content); + +#[node(LayoutBlock)] +impl PlaceNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let aligns = args.find()?.unwrap_or(Axes::with_x(Some(GenAlign::Start))); + let dx = args.named("dx")?.unwrap_or_default(); + let dy = args.named("dy")?.unwrap_or_default(); + let body = args.expect::("body")?; + Ok(Self(body.moved(Axes::new(dx, dy)).aligned(aligns)).pack()) + } +} + +impl LayoutBlock for PlaceNode { + fn layout_block( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + let out_of_flow = self.out_of_flow(); + + // 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, regions.base, expand) + }; + + let mut frames = self.0.layout_block(world, &pod, styles)?; + + // 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.first, Size::zero()); + frames[0].resize(target, Align::LEFT_TOP); + + Ok(frames) + } +} + +impl PlaceNode { + /// Whether this node 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) -> bool { + self.0 + .downcast::() + .map_or(false, |node| node.aligns.y.is_some()) + } +} diff --git a/library/src/layout/spacing.rs b/library/src/layout/spacing.rs new file mode 100644 index 00000000..67fff5db --- /dev/null +++ b/library/src/layout/spacing.rs @@ -0,0 +1,100 @@ +use std::cmp::Ordering; + +use crate::prelude::*; +use crate::text::ParNode; + +/// Horizontal spacing. +#[derive(Debug, Clone, Hash)] +pub struct HNode { + pub amount: Spacing, + pub weak: bool, +} + +#[node] +impl HNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let amount = args.expect("spacing")?; + let weak = args.named("weak")?.unwrap_or(false); + Ok(Self { amount, weak }.pack()) + } +} + +/// Vertical spacing. +#[derive(Debug, Clone, Hash)] +pub struct VNode { + pub amount: Spacing, + pub weak: bool, + pub generated: bool, +} + +#[node] +impl VNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let amount = args.expect("spacing")?; + let weak = args.named("weak")?.unwrap_or(false); + Ok(Self { amount, weak, generated: false }.pack()) + } +} + +/// 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. + Relative(Rel), + /// Spacing specified as a fraction of the remaining free space in the + /// parent. + Fractional(Fr), +} + +impl Spacing { + /// Whether this is fractional spacing. + pub fn is_fractional(self) -> bool { + matches!(self, Self::Fractional(_)) + } +} + +impl From for Spacing { + fn from(abs: Abs) -> Self { + Self::Relative(abs.into()) + } +} + +impl PartialOrd for Spacing { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (Self::Relative(a), Self::Relative(b)) => a.partial_cmp(b), + (Self::Fractional(a), Self::Fractional(b)) => a.partial_cmp(b), + _ => None, + } + } +} + +castable! { + Spacing, + Expected: "relative length or fraction", + Value::Length(v) => Self::Relative(v.into()), + Value::Ratio(v) => Self::Relative(v.into()), + Value::Relative(v) => Self::Relative(v), + Value::Fraction(v) => Self::Fractional(v), +} + +/// Spacing around and between blocks, relative to paragraph spacing. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct BlockSpacing(Rel); + +castable!(BlockSpacing: Rel); + +impl Resolve for BlockSpacing { + type Output = Abs; + + fn resolve(self, styles: StyleChain) -> Self::Output { + let whole = styles.get(ParNode::SPACING); + self.0.resolve(styles).relative_to(whole) + } +} + +impl From for BlockSpacing { + fn from(ratio: Ratio) -> Self { + Self(ratio.into()) + } +} diff --git a/library/src/layout/stack.rs b/library/src/layout/stack.rs new file mode 100644 index 00000000..44bcbf67 --- /dev/null +++ b/library/src/layout/stack.rs @@ -0,0 +1,322 @@ +use typst::model::StyledNode; + +use super::{AlignNode, Spacing}; +use crate::prelude::*; +use crate::text::ParNode; + +/// Arrange content and spacing along an axis. +#[derive(Debug, Hash)] +pub struct StackNode { + /// The stacking direction. + pub dir: Dir, + /// The spacing between non-spacing children. + pub spacing: Option, + /// The children to be stacked. + pub children: Vec, +} + +#[node(LayoutBlock)] +impl StackNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(Self { + dir: args.named("dir")?.unwrap_or(Dir::TTB), + spacing: args.named("spacing")?, + children: args.all()?, + } + .pack()) + } +} + +impl LayoutBlock for StackNode { + fn layout_block( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + let mut layouter = StackLayouter::new(self.dir, regions, styles); + + // Spacing to insert before the next block. + 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(world, block, styles)?; + deferred = self.spacing; + } + } + } + + Ok(layouter.finish()) + } +} + +/// A child of a stack node. +#[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), + } + } +} + +castable! { + StackChild, + Expected: "relative length, fraction, or content", + Value::Length(v) => Self::Spacing(Spacing::Relative(v.into())), + Value::Ratio(v) => Self::Spacing(Spacing::Relative(v.into())), + Value::Relative(v) => Self::Spacing(Spacing::Relative(v)), + Value::Fraction(v) => Self::Spacing(Spacing::Fractional(v)), + Value::Content(v) => Self::Block(v), +} + +/// Performs stack layout. +pub struct StackLayouter<'a> { + /// The stacking direction. + dir: Dir, + /// The axis of the stacking direction. + axis: Axis, + /// The regions to layout children into. + regions: Regions, + /// The inherited styles. + styles: StyleChain<'a>, + /// Whether the stack itself should expand to fill the region. + expand: Axes, + /// The full size of the current region that was available at the start. + full: Size, + /// The generic size used by the frames for the current region. + used: Gen, + /// 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, + /// Finished frames for previous regions. + finished: Vec, +} + +/// 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, Align), +} + +impl<'a> StackLayouter<'a> { + /// Create a new stack layouter. + pub fn new(dir: Dir, regions: &Regions, styles: StyleChain<'a>) -> Self { + let axis = dir.axis(); + let expand = regions.expand; + let full = regions.first; + + // Disable expansion along the block axis for children. + let mut regions = regions.clone(); + regions.expand.set(axis, false); + + Self { + dir, + axis, + regions, + styles, + expand, + full, + used: Gen::zero(), + fr: Fr::zero(), + items: vec![], + finished: vec![], + } + } + + /// Add spacing along the spacing direction. + pub fn layout_spacing(&mut self, spacing: Spacing) { + match spacing { + Spacing::Relative(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.first.get_mut(self.axis); + let limited = resolved.min(*remaining); + *remaining -= limited; + self.used.main += limited; + self.items.push(StackItem::Absolute(resolved)); + } + Spacing::Fractional(v) => { + self.fr += v; + self.items.push(StackItem::Fractional(v)); + } + } + } + + /// Layout an arbitrary block. + pub fn layout_block( + &mut self, + world: Tracked, + block: &Content, + styles: StyleChain, + ) -> SourceResult<()> { + if self.regions.is_full() { + self.finish_region(); + } + + // Block-axis alignment of the `AlignNode` is respected + // by the stack node. + let align = block + .downcast::() + .and_then(|node| node.aligns.get(self.axis)) + .map(|align| align.resolve(styles)) + .unwrap_or_else(|| { + if let Some(styled) = block.downcast::() { + let map = &styled.map; + if map.contains(ParNode::ALIGN) { + return StyleChain::with_root(map).get(ParNode::ALIGN); + } + } + + self.dir.start().into() + }); + + let frames = block.layout_block(world, &self.regions, styles)?; + let len = frames.len(); + for (i, mut frame) in frames.into_iter().enumerate() { + // Set the generic block role. + frame.apply_role(Role::GenericBlock); + + // Grow our size, shrink the region and save the frame for later. + let size = frame.size(); + let size = match self.axis { + Axis::X => Gen::new(size.y, size.x), + Axis::Y => Gen::new(size.x, size.y), + }; + + self.used.main += size.main; + self.used.cross.set_max(size.cross); + *self.regions.first.get_mut(self.axis) -= size.main; + self.items.push(StackItem::Frame(frame, align)); + + if i + 1 < len { + self.finish_region(); + } + } + + Ok(()) + } + + /// Advance to the next region. + pub fn finish_region(&mut self) { + // Determine the size of the stack in this region dependening on whether + // the region expands. + let used = self.used.to_axes(self.axis); + let mut size = self.expand.select(self.full, used); + + // Expand fully if there are fr spacings. + let full = self.full.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, align) => { + if self.dir.is_positive() { + ruler = ruler.max(align); + } else { + ruler = ruler.min(align); + } + + // Align along the block axis. + let parent = size.get(self.axis); + let child = frame.size().get(self.axis); + let block = ruler.position(parent - self.used.main) + + if self.dir.is_positive() { + cursor + } else { + self.used.main - child - cursor + }; + + let pos = Gen::new(Abs::zero(), block).to_point(self.axis); + cursor += child; + output.push_frame(pos, frame); + } + } + } + + // Advance to the next region. + self.regions.next(); + self.full = self.regions.first; + self.used = Gen::zero(); + self.fr = Fr::zero(); + self.finished.push(output); + } + + /// Finish layouting and return the resulting frames. + pub fn finish(mut self) -> Vec { + self.finish_region(); + self.finished + } +} + +/// A container with a main and cross component. +#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Gen { + /// The main component. + pub cross: T, + /// The cross component. + pub main: T, +} + +impl Gen { + /// Create a new instance from the two components. + pub const fn new(cross: T, main: T) -> Self { + Self { cross, main } + } + + /// Convert to the specific representation, given the current main axis. + pub fn to_axes(self, main: Axis) -> Axes { + match main { + Axis::X => Axes::new(self.main, self.cross), + Axis::Y => Axes::new(self.cross, self.main), + } + } +} + +impl Gen { + /// The zero value. + pub fn zero() -> Self { + Self { cross: Abs::zero(), main: Abs::zero() } + } + + /// Convert to a point. + pub fn to_point(self, main: Axis) -> Point { + self.to_axes(main).to_point() + } +} diff --git a/library/src/layout/transform.rs b/library/src/layout/transform.rs new file mode 100644 index 00000000..4e0b8ac2 --- /dev/null +++ b/library/src/layout/transform.rs @@ -0,0 +1,117 @@ +use typst::geom::Transform; + +use crate::prelude::*; + +/// Move content without affecting layout. +#[derive(Debug, Hash)] +pub struct MoveNode { + /// The offset by which to move the content. + pub delta: Axes>, + /// The content that should be moved. + pub child: Content, +} + +#[node(LayoutInline)] +impl MoveNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let dx = args.named("dx")?.unwrap_or_default(); + let dy = args.named("dy")?.unwrap_or_default(); + Ok(Self { + delta: Axes::new(dx, dy), + child: args.expect("body")?, + } + .pack()) + } +} + +impl LayoutInline for MoveNode { + fn layout_inline( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + let mut frames = self.child.layout_inline(world, regions, styles)?; + + let delta = self.delta.resolve(styles); + for frame in &mut frames { + let delta = delta.zip(frame.size()).map(|(d, s)| d.relative_to(s)); + frame.translate(delta.to_point()); + } + + Ok(frames) + } +} + +/// Transform content without affecting layout. +#[derive(Debug, Hash)] +pub struct TransformNode { + /// Transformation to apply to the content. + pub transform: Transform, + /// The content that should be transformed. + pub child: Content, +} + +/// Rotate content without affecting layout. +pub type RotateNode = TransformNode; + +/// Scale content without affecting layout. +pub type ScaleNode = TransformNode; + +#[node(LayoutInline)] +impl TransformNode { + /// The origin of the transformation. + #[property(resolve)] + pub const ORIGIN: Axes> = Axes::default(); + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let transform = match T { + ROTATE => { + let angle = args.named_or_find("angle")?.unwrap_or_default(); + Transform::rotate(angle) + } + SCALE | _ => { + let all = args.find()?; + let sx = args.named("x")?.or(all).unwrap_or(Ratio::one()); + let sy = args.named("y")?.or(all).unwrap_or(Ratio::one()); + Transform::scale(sx, sy) + } + }; + + Ok(Self { transform, child: args.expect("body")? }.pack()) + } +} + +impl LayoutInline for TransformNode { + fn layout_inline( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + let origin = styles.get(Self::ORIGIN).unwrap_or(Align::CENTER_HORIZON); + let mut frames = self.child.layout_inline(world, regions, styles)?; + + for frame in &mut frames { + let Axes { x, y } = origin.zip(frame.size()).map(|(o, s)| o.position(s)); + let transform = Transform::translate(x, y) + .pre_concat(self.transform) + .pre_concat(Transform::translate(-x, -y)); + + frame.transform(transform); + } + + Ok(frames) + } +} + +/// Kinds of transformations. +/// +/// The move transformation is handled separately. +pub type TransformKind = usize; + +/// A rotational transformation. +const ROTATE: TransformKind = 1; + +/// A scale transformation. +const SCALE: TransformKind = 2; diff --git a/library/src/lib.rs b/library/src/lib.rs new file mode 100644 index 00000000..ed332a06 --- /dev/null +++ b/library/src/lib.rs @@ -0,0 +1,191 @@ +//! Typst's standard library. + +pub mod graphics; +pub mod layout; +pub mod math; +pub mod prelude; +pub mod structure; +pub mod text; +pub mod utility; + +mod ext; + +use typst::geom::{Align, Color, Dir, GenAlign}; +use typst::model::{LangItems, Node, Scope, StyleMap}; + +use self::layout::Layout; + +/// Construct the standard library scope. +pub fn scope() -> Scope { + let mut std = Scope::new(); + + // Text. + std.def_node::("space"); + std.def_node::("linebreak"); + std.def_node::("smartquote"); + std.def_node::("text"); + std.def_node::("par"); + std.def_node::("parbreak"); + std.def_node::("strong"); + std.def_node::("emph"); + std.def_node::("raw"); + std.def_node::("underline"); + std.def_node::("strike"); + std.def_node::("overline"); + std.def_node::("super"); + std.def_node::("sub"); + std.def_node::("link"); + std.def_node::("repeat"); + std.def_fn("lower", text::lower); + std.def_fn("upper", text::upper); + std.def_fn("smallcaps", text::smallcaps); + + // Structure. + std.def_node::("ref"); + std.def_node::("heading"); + std.def_node::("list"); + std.def_node::("enum"); + std.def_node::("desc"); + std.def_node::("table"); + + // Layout. + std.def_node::("page"); + std.def_node::("pagebreak"); + std.def_node::("h"); + std.def_node::("v"); + std.def_node::("box"); + std.def_node::("block"); + std.def_node::("align"); + std.def_node::("pad"); + std.def_node::("stack"); + std.def_node::("grid"); + std.def_node::("columns"); + std.def_node::("colbreak"); + std.def_node::("place"); + std.def_node::("move"); + std.def_node::("scale"); + std.def_node::("rotate"); + + // Graphics. + std.def_node::("image"); + std.def_node::("line"); + std.def_node::("rect"); + std.def_node::("square"); + std.def_node::("ellipse"); + std.def_node::("circle"); + std.def_node::("hide"); + + // Math. + std.def_node::("math"); + std.define("sum", "∑"); + std.define("in", "∈"); + std.define("arrow", "→"); + std.define("NN", "ℕ"); + std.define("RR", "ℝ"); + + // Utility. + std.def_fn("type", utility::type_); + std.def_fn("assert", utility::assert); + std.def_fn("eval", utility::eval); + std.def_fn("int", utility::int); + std.def_fn("float", utility::float); + std.def_fn("abs", utility::abs); + std.def_fn("min", utility::min); + std.def_fn("max", utility::max); + std.def_fn("even", utility::even); + std.def_fn("odd", utility::odd); + std.def_fn("mod", utility::mod_); + std.def_fn("range", utility::range); + std.def_fn("luma", utility::luma); + std.def_fn("rgb", utility::rgb); + std.def_fn("cmyk", utility::cmyk); + std.def_fn("repr", utility::repr); + std.def_fn("str", utility::str); + std.def_fn("regex", utility::regex); + std.def_fn("letter", utility::letter); + std.def_fn("roman", utility::roman); + std.def_fn("symbol", utility::symbol); + std.def_fn("lorem", utility::lorem); + std.def_fn("csv", utility::csv); + std.def_fn("json", utility::json); + std.def_fn("xml", utility::xml); + + // Predefined colors. + std.define("black", Color::BLACK); + std.define("gray", Color::GRAY); + std.define("silver", Color::SILVER); + std.define("white", Color::WHITE); + std.define("navy", Color::NAVY); + std.define("blue", Color::BLUE); + std.define("aqua", Color::AQUA); + std.define("teal", Color::TEAL); + std.define("eastern", Color::EASTERN); + std.define("purple", Color::PURPLE); + std.define("fuchsia", Color::FUCHSIA); + std.define("maroon", Color::MAROON); + std.define("red", Color::RED); + std.define("orange", Color::ORANGE); + std.define("yellow", Color::YELLOW); + std.define("olive", Color::OLIVE); + std.define("green", Color::GREEN); + std.define("lime", Color::LIME); + + // Other constants. + std.define("ltr", Dir::LTR); + std.define("rtl", Dir::RTL); + std.define("ttb", Dir::TTB); + std.define("btt", Dir::BTT); + std.define("start", GenAlign::Start); + std.define("end", GenAlign::End); + std.define("left", GenAlign::Specific(Align::Left)); + std.define("center", GenAlign::Specific(Align::Center)); + std.define("right", GenAlign::Specific(Align::Right)); + std.define("top", GenAlign::Specific(Align::Top)); + std.define("horizon", GenAlign::Specific(Align::Horizon)); + std.define("bottom", GenAlign::Specific(Align::Bottom)); + + std +} + +/// Construct the standard style map. +pub fn styles() -> StyleMap { + StyleMap::new() +} + +/// Construct the standard lang item mapping. +pub fn items() -> LangItems { + LangItems { + root: |world, content| content.layout(world), + em: |styles| styles.get(text::TextNode::SIZE), + dir: |styles| styles.get(text::TextNode::DIR), + space: || text::SpaceNode.pack(), + linebreak: |justify| text::LinebreakNode { justify }.pack(), + text: |text| text::TextNode(text).pack(), + smart_quote: |double| text::SmartQuoteNode { double }.pack(), + parbreak: || text::ParbreakNode.pack(), + strong: |body| text::StrongNode(body).pack(), + emph: |body| text::EmphNode(body).pack(), + raw: |text, lang, block| { + let content = text::RawNode { text, block }.pack(); + match lang { + Some(_) => content.styled(text::RawNode::LANG, lang), + None => content, + } + }, + link: |url| text::LinkNode::from_url(url).pack(), + ref_: |target| structure::RefNode(target).pack(), + heading: |level, body| structure::HeadingNode { level, body }.pack(), + list_item: |body| structure::ListItem::List(Box::new(body)).pack(), + enum_item: |number, body| { + structure::ListItem::Enum(number, Box::new(body)).pack() + }, + desc_item: |term, body| { + structure::ListItem::Desc(Box::new(structure::DescItem { term, body })).pack() + }, + math: |children, display| math::MathNode { children, display }.pack(), + math_atom: |atom| math::AtomNode(atom).pack(), + math_script: |base, sub, sup| math::ScriptNode { base, sub, sup }.pack(), + math_frac: |num, denom| math::FracNode { num, denom }.pack(), + math_align: |count| math::AlignNode(count).pack(), + } +} diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs new file mode 100644 index 00000000..e46ba040 --- /dev/null +++ b/library/src/math/mod.rs @@ -0,0 +1,188 @@ +//! Mathematical formulas. + +mod tex; + +use std::fmt::Write; + +use self::tex::{layout_tex, Texify}; +use crate::layout::BlockSpacing; +use crate::prelude::*; +use crate::text::FontFamily; + +/// A piece of a mathematical formula. +#[derive(Debug, Clone, Hash)] +pub struct MathNode { + /// The pieces of the formula. + pub children: Vec, + /// Whether the formula is display-level. + pub display: bool, +} + +#[node(Show, LayoutInline, Texify)] +impl MathNode { + /// The math font family. + #[property(referenced)] + pub const FAMILY: FontFamily = FontFamily::new("NewComputerModernMath"); + /// The spacing above display math. + #[property(resolve, shorthand(around))] + pub const ABOVE: Option = Some(Ratio::one().into()); + /// The spacing below display math. + #[property(resolve, shorthand(around))] + pub const BELOW: Option = Some(Ratio::one().into()); +} + +impl Show for MathNode { + fn unguard_parts(&self, _: Selector) -> Content { + self.clone().pack() + } + + fn field(&self, _: &str) -> Option { + None + } + + fn realize(&self, _: Tracked, _: StyleChain) -> SourceResult { + Ok(if self.display { + self.clone().pack().aligned(Axes::with_x(Some(Align::Center.into()))) + } else { + self.clone().pack() + }) + } + + fn finalize( + &self, + _: Tracked, + styles: StyleChain, + realized: Content, + ) -> SourceResult { + Ok(if self.display { + realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW)) + } else { + realized + }) + } +} + +impl LayoutInline for MathNode { + fn layout_inline( + &self, + world: Tracked, + _: &Regions, + styles: StyleChain, + ) -> SourceResult> { + Ok(vec![layout_tex( + &self.texify(), + self.display, + world, + styles, + )?]) + } +} + +impl Texify for MathNode { + fn texify(&self) -> EcoString { + self.children.iter().map(Texify::texify).collect() + } +} + +/// An atom in a math formula: `x`, `+`, `12`. +#[derive(Debug, Hash)] +pub struct AtomNode(pub EcoString); + +#[node(Texify)] +impl AtomNode {} + +impl Texify for AtomNode { + fn texify(&self) -> EcoString { + self.0.chars().map(escape_char).collect() + } +} + +/// A fraction in a mathematical formula. +#[derive(Debug, Hash)] +pub struct FracNode { + /// The numerator. + pub num: Content, + /// The denominator. + pub denom: Content, +} + +#[node(Texify)] +impl FracNode {} + +impl Texify for FracNode { + fn texify(&self) -> EcoString { + format_eco!( + "\\frac{{{}}}{{{}}}", + unparen(self.num.texify()), + unparen(self.denom.texify()) + ) + } +} + +/// A sub- and/or superscript in a mathematical formula. +#[derive(Debug, Hash)] +pub struct ScriptNode { + /// The base. + pub base: Content, + /// The subscript. + pub sub: Option, + /// The superscript. + pub sup: Option, +} + +#[node(Texify)] +impl ScriptNode {} + +impl Texify for ScriptNode { + fn texify(&self) -> EcoString { + let mut tex = self.base.texify(); + + if let Some(sub) = &self.sub { + write!(tex, "_{{{}}}", unparen(sub.texify())).unwrap(); + } + + if let Some(sup) = &self.sup { + write!(tex, "^{{{}}}", unparen(sup.texify())).unwrap(); + } + + tex + } +} + +/// A math alignment indicator: `&`, `&&`. +#[derive(Debug, Hash)] +pub struct AlignNode(pub usize); + +#[node(Texify)] +impl AlignNode {} + +impl Texify for AlignNode { + fn texify(&self) -> EcoString { + EcoString::new() + } +} + +/// Escape a char for TeX usage. +#[rustfmt::skip] +fn escape_char(c: char) -> EcoString { + match c { + '{' | '}' | '%' | '&' | '$' | '#' => format_eco!(" \\{c} "), + 'a' ..= 'z' | 'A' ..= 'Z' | '0' ..= '9' | 'Α' ..= 'Ω' | 'α' ..= 'ω' | + '*' | '+' | '-' | '[' | '(' | ']' | ')' | '?' | '!' | '=' | '<' | '>' | + ':' | ',' | ';' | '|' | '/' | '@' | '.' | '"' => c.into(), + c => unicode_math::SYMBOLS + .iter() + .find(|sym| sym.codepoint == c) + .map(|sym| format_eco!("\\{} ", sym.name)) + .unwrap_or_default(), + } +} + +/// Trim grouping parenthesis≤. +fn unparen(s: EcoString) -> EcoString { + if s.starts_with('(') && s.ends_with(')') { + s[1 .. s.len() - 1].into() + } else { + s + } +} diff --git a/library/src/math/tex.rs b/library/src/math/tex.rs new file mode 100644 index 00000000..7b40aa2b --- /dev/null +++ b/library/src/math/tex.rs @@ -0,0 +1,164 @@ +use rex::error::{Error, LayoutError}; +use rex::font::FontContext; +use rex::layout::{LayoutSettings, Style}; +use rex::parser::color::RGBA; +use rex::render::{Backend, Cursor, Renderer}; +use typst::font::Font; + +use super::*; +use crate::prelude::*; +use crate::text::{variant, LinebreakNode, SpaceNode, TextNode}; + +/// Turn a math node into TeX math code. +#[capability] +pub trait Texify: 'static + Sync + Send { + /// Perform the conversion. + fn texify(&self) -> EcoString; +} + +impl Texify for Content { + fn texify(&self) -> EcoString { + if self.is::() { + return EcoString::new(); + } + + if self.is::() { + return r"\\".into(); + } + + if let Some(node) = self.to::() { + return node.texify(); + } + + panic!("{self:?} is not math"); + } +} + +/// Layout a TeX formula into a frame. +pub fn layout_tex( + tex: &str, + display: bool, + world: Tracked, + styles: StyleChain, +) -> SourceResult { + // Load the font. + let font = world + .book() + .select(styles.get(MathNode::FAMILY).as_str(), variant(styles)) + .and_then(|id| world.font(id)) + .expect("failed to find math font"); + + // Prepare the font context. + let ctx = font + .math() + .map(|math| FontContext::new(font.ttf(), math)) + .expect("font is not suitable for math"); + + // Layout the formula. + let em = styles.get(TextNode::SIZE); + let style = if display { Style::Display } else { Style::Text }; + let settings = LayoutSettings::new(&ctx, em.to_pt(), style); + let renderer = Renderer::new(); + let layout = renderer + .layout(&tex, settings) + .map_err(|err| match err { + Error::Parse(err) => err.to_string(), + Error::Layout(LayoutError::Font(err)) => err.to_string(), + }) + .expect("failed to layout with rex"); + + // Determine the metrics. + let (x0, y0, x1, y1) = renderer.size(&layout); + let width = Abs::pt(x1 - x0); + let mut top = Abs::pt(y1); + let mut bottom = Abs::pt(-y0); + if style != Style::Display { + let metrics = font.metrics(); + top = styles.get(TextNode::TOP_EDGE).resolve(styles, metrics); + bottom = -styles.get(TextNode::BOTTOM_EDGE).resolve(styles, metrics); + }; + + // Prepare a frame rendering backend. + let size = Size::new(width, top + bottom); + let mut backend = FrameBackend { + frame: { + let mut frame = Frame::new(size); + frame.set_baseline(top); + frame.apply_role(Role::Formula); + frame + }, + baseline: top, + font: font.clone(), + fill: styles.get(TextNode::FILL), + lang: styles.get(TextNode::LANG), + colors: vec![], + }; + + // Render into the frame. + renderer.render(&layout, &mut backend); + Ok(backend.frame) +} + +/// A ReX rendering backend that renders into a frame. +struct FrameBackend { + frame: Frame, + baseline: Abs, + font: Font, + fill: Paint, + lang: Lang, + colors: Vec, +} + +impl FrameBackend { + /// The currently active fill paint. + fn fill(&self) -> Paint { + self.colors + .last() + .map(|&RGBA(r, g, b, a)| RgbaColor::new(r, g, b, a).into()) + .unwrap_or(self.fill) + } + + /// Convert a cursor to a point. + fn transform(&self, cursor: Cursor) -> Point { + Point::new(Abs::pt(cursor.x), self.baseline + Abs::pt(cursor.y)) + } +} + +impl Backend for FrameBackend { + fn symbol(&mut self, pos: Cursor, gid: u16, scale: f64) { + self.frame.push( + self.transform(pos), + Element::Text(Text { + font: self.font.clone(), + size: Abs::pt(scale), + fill: self.fill(), + lang: self.lang, + glyphs: vec![Glyph { + id: gid, + x_advance: Em::new(0.0), + x_offset: Em::new(0.0), + c: ' ', + }], + }), + ); + } + + fn rule(&mut self, pos: Cursor, width: f64, height: f64) { + self.frame.push( + self.transform(pos), + Element::Shape(Shape { + geometry: Geometry::Rect(Size::new(Abs::pt(width), Abs::pt(height))), + fill: Some(self.fill()), + stroke: None, + }), + ); + } + + fn begin_color(&mut self, color: RGBA) { + self.colors.push(color); + } + + fn end_color(&mut self) { + self.colors.pop(); + } +} diff --git a/library/src/prelude.rs b/library/src/prelude.rs new file mode 100644 index 00000000..0c3b0eb1 --- /dev/null +++ b/library/src/prelude.rs @@ -0,0 +1,27 @@ +//! Helpful imports for creating library functionality. + +pub use std::fmt::{self, Debug, Formatter}; +pub use std::hash::Hash; +pub use std::io; +pub use std::num::NonZeroUsize; +pub use std::sync::Arc; + +pub use comemo::Tracked; +pub use typst::diag::{ + bail, error, with_alternative, At, FileError, FileResult, SourceError, SourceResult, + StrResult, +}; +pub use typst::frame::*; +pub use typst::geom::*; +pub use typst::model::{ + array, capability, castable, dict, dynamic, format_str, node, Args, Array, + Capability, Cast, Content, Dict, Dynamic, Fold, Func, Key, LangItems, Node, Resolve, + Scope, Selector, Show, Smart, Str, StyleChain, StyleMap, StyleVec, Value, Vm, +}; +pub use typst::syntax::{Span, Spanned}; +pub use typst::util::{format_eco, EcoString}; +pub use typst::World; + +pub use super::ext::{ContentExt, StyleMapExt}; +pub use super::layout::{Layout, LayoutBlock, LayoutInline, Regions}; +pub use super::text::{FallbackList, TextNode}; diff --git a/library/src/structure/doc.rs b/library/src/structure/doc.rs new file mode 100644 index 00000000..ac12c3ab --- /dev/null +++ b/library/src/structure/doc.rs @@ -0,0 +1,29 @@ +use crate::layout::PageNode; +use crate::prelude::*; + +/// A sequence of page runs. +#[derive(Hash)] +pub struct DocNode(pub StyleVec); + +impl DocNode { + /// Layout the document into a sequence of frames, one per page. + pub fn layout( + &self, + world: Tracked, + styles: StyleChain, + ) -> SourceResult> { + let mut frames = vec![]; + for (page, map) in self.0.iter() { + let number = 1 + frames.len(); + frames.extend(page.layout(world, number, map.chain(&styles))?); + } + Ok(frames) + } +} + +impl Debug for DocNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Doc ")?; + self.0.fmt(f) + } +} diff --git a/library/src/structure/heading.rs b/library/src/structure/heading.rs new file mode 100644 index 00000000..62a67000 --- /dev/null +++ b/library/src/structure/heading.rs @@ -0,0 +1,176 @@ +use crate::layout::{BlockNode, BlockSpacing}; +use crate::prelude::*; +use crate::text::{FontFamily, TextNode, TextSize}; + +/// A section heading. +#[derive(Debug, Hash)] +pub struct HeadingNode { + /// The logical nesting depth of the section, starting from one. In the + /// default style, this controls the text size of the heading. + pub level: NonZeroUsize, + /// The heading's contents. + pub body: Content, +} + +#[node(Show)] +impl HeadingNode { + /// The heading's font family. Just the normal text family if `auto`. + #[property(referenced)] + pub const FAMILY: Leveled> = Leveled::Value(Smart::Auto); + /// The color of text in the heading. Just the normal text color if `auto`. + #[property(referenced)] + pub const FILL: Leveled> = Leveled::Value(Smart::Auto); + /// The size of text in the heading. + #[property(referenced)] + pub const SIZE: Leveled = Leveled::Mapping(|level| { + let size = match level.get() { + 1 => 1.4, + 2 => 1.2, + _ => 1.0, + }; + TextSize(Em::new(size).into()) + }); + + /// Whether text in the heading is strengthend. + #[property(referenced)] + pub const STRONG: Leveled = Leveled::Value(true); + /// Whether text in the heading is emphasized. + #[property(referenced)] + pub const EMPH: Leveled = Leveled::Value(false); + /// Whether the heading is underlined. + #[property(referenced)] + pub const UNDERLINE: Leveled = Leveled::Value(false); + + /// The spacing above the heading. + #[property(referenced, shorthand(around))] + pub const ABOVE: Leveled> = Leveled::Mapping(|level| { + let ratio = match level.get() { + 1 => 1.5, + _ => 1.2, + }; + Some(Ratio::new(ratio).into()) + }); + /// The spacing below the heading. + #[property(referenced, shorthand(around))] + pub const BELOW: Leveled> = + Leveled::Value(Some(Ratio::new(0.55).into())); + + /// Whether the heading appears in the outline. + pub const OUTLINED: bool = true; + /// Whether the heading is numbered. + pub const NUMBERED: bool = true; + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(Self { + body: args.expect("body")?, + level: args.named("level")?.unwrap_or(NonZeroUsize::new(1).unwrap()), + } + .pack()) + } +} + +impl Show for HeadingNode { + fn unguard_parts(&self, sel: Selector) -> Content { + Self { body: self.body.unguard(sel), ..*self }.pack() + } + + fn field(&self, name: &str) -> Option { + match name { + "level" => Some(Value::Int(self.level.get() as i64)), + "body" => Some(Value::Content(self.body.clone())), + _ => None, + } + } + + fn realize(&self, _: Tracked, _: StyleChain) -> SourceResult { + Ok(BlockNode(self.body.clone()).pack()) + } + + fn finalize( + &self, + world: Tracked, + styles: StyleChain, + mut realized: Content, + ) -> SourceResult { + macro_rules! resolve { + ($key:expr) => { + styles.get($key).resolve(world, self.level)? + }; + } + + let mut map = StyleMap::new(); + map.set(TextNode::SIZE, resolve!(Self::SIZE)); + + if let Smart::Custom(family) = resolve!(Self::FAMILY) { + map.set_family(family, styles); + } + + if let Smart::Custom(fill) = resolve!(Self::FILL) { + map.set(TextNode::FILL, fill); + } + + if resolve!(Self::STRONG) { + realized = realized.strong(); + } + + if resolve!(Self::EMPH) { + realized = realized.emph(); + } + + if resolve!(Self::UNDERLINE) { + realized = realized.underlined(); + } + + realized = realized.styled_with_map(map); + realized = realized.spaced( + resolve!(Self::ABOVE).resolve(styles), + resolve!(Self::BELOW).resolve(styles), + ); + + Ok(realized) + } +} + +/// Either the value or a closure mapping to the value. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum Leveled { + /// A bare value. + Value(T), + /// A simple mapping from a heading level to a value. + Mapping(fn(NonZeroUsize) -> T), + /// A closure mapping from a heading level to a value. + Func(Func, Span), +} + +impl Leveled { + /// Resolve the value based on the level. + pub fn resolve( + &self, + world: Tracked, + level: NonZeroUsize, + ) -> SourceResult { + Ok(match self { + Self::Value(value) => value.clone(), + Self::Mapping(mapping) => mapping(level), + Self::Func(func, span) => { + let args = Args::new(*span, [Value::Int(level.get() as i64)]); + func.call_detached(world, args)?.cast().at(*span)? + } + }) + } +} + +impl Cast> for Leveled { + fn is(value: &Spanned) -> bool { + matches!(&value.v, Value::Func(_)) || T::is(&value.v) + } + + fn cast(value: Spanned) -> StrResult { + match value.v { + Value::Func(v) => Ok(Self::Func(v, value.span)), + v => T::cast(v) + .map(Self::Value) + .map_err(|msg| with_alternative(msg, "function")), + } + } +} diff --git a/library/src/structure/list.rs b/library/src/structure/list.rs new file mode 100644 index 00000000..2015f19b --- /dev/null +++ b/library/src/structure/list.rs @@ -0,0 +1,371 @@ +use unscanny::Scanner; + +use crate::layout::{BlockSpacing, GridNode, HNode, TrackSizing}; +use crate::prelude::*; +use crate::text::{ParNode, SpaceNode}; +use crate::utility::Numbering; + +/// An unordered (bulleted) or ordered (numbered) list. +#[derive(Debug, Hash)] +pub struct ListNode { + /// If true, the items are separated by leading instead of list spacing. + pub tight: bool, + /// If true, the spacing above the list is leading instead of above spacing. + pub attached: bool, + /// The individual bulleted or numbered items. + pub items: StyleVec, +} + +/// An ordered list. +pub type EnumNode = ListNode; + +/// A description list. +pub type DescNode = ListNode; + +#[node(Show)] +impl ListNode { + /// How the list is labelled. + #[property(referenced)] + pub const LABEL: Label = Label::Default; + /// The indentation of each item's label. + #[property(resolve)] + pub const INDENT: Length = Length::zero(); + /// The space between the label and the body of each item. + #[property(resolve)] + pub const BODY_INDENT: Length = Em::new(match L { + LIST | ENUM => 0.5, + DESC | _ => 1.0, + }) + .into(); + + /// The spacing above the list. + #[property(resolve, shorthand(around))] + pub const ABOVE: Option = Some(Ratio::one().into()); + /// The spacing below the list. + #[property(resolve, shorthand(around))] + pub const BELOW: Option = Some(Ratio::one().into()); + /// The spacing between the items of a wide (non-tight) list. + #[property(resolve)] + pub const SPACING: BlockSpacing = Ratio::one().into(); + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let items = match L { + LIST => args + .all()? + .into_iter() + .map(|body| ListItem::List(Box::new(body))) + .collect(), + ENUM => { + let mut number: usize = args.named("start")?.unwrap_or(1); + args.all()? + .into_iter() + .map(|body| { + let item = ListItem::Enum(Some(number), Box::new(body)); + number += 1; + item + }) + .collect() + } + DESC | _ => args + .all()? + .into_iter() + .map(|item| ListItem::Desc(Box::new(item))) + .collect(), + }; + + Ok(Self { + tight: args.named("tight")?.unwrap_or(true), + attached: args.named("attached")?.unwrap_or(false), + items, + } + .pack()) + } +} + +impl Show for ListNode { + fn unguard_parts(&self, sel: Selector) -> Content { + Self { + items: self.items.map(|item| item.unguard(sel)), + ..*self + } + .pack() + } + + fn field(&self, name: &str) -> Option { + match name { + "tight" => Some(Value::Bool(self.tight)), + "attached" => Some(Value::Bool(self.attached)), + "items" => Some(Value::Array( + self.items.items().map(|item| item.encode()).collect(), + )), + _ => None, + } + } + + fn realize( + &self, + world: Tracked, + styles: StyleChain, + ) -> SourceResult { + let mut cells = vec![]; + let mut number = 1; + + let label = styles.get(Self::LABEL); + let indent = styles.get(Self::INDENT); + let body_indent = styles.get(Self::BODY_INDENT); + let gutter = if self.tight { + styles.get(ParNode::LEADING) + } else { + styles.get(Self::SPACING) + }; + + for (item, map) in self.items.iter() { + if let &ListItem::Enum(Some(n), _) = item { + number = n; + } + + cells.push(Content::empty()); + + let label = if L == LIST || L == ENUM { + label.resolve(world, L, number)?.styled_with_map(map.clone()) + } else { + Content::empty() + }; + + cells.push(label); + cells.push(Content::empty()); + + let body = match &item { + ListItem::List(body) => body.as_ref().clone(), + ListItem::Enum(_, body) => body.as_ref().clone(), + ListItem::Desc(item) => Content::sequence(vec![ + HNode { + amount: (-body_indent).into(), + weak: false, + } + .pack(), + (item.term.clone() + TextNode(':'.into()).pack()).strong(), + SpaceNode.pack(), + item.body.clone(), + ]), + }; + + cells.push(body.styled_with_map(map.clone())); + number += 1; + } + + Ok(GridNode { + tracks: Axes::with_x(vec![ + TrackSizing::Relative(indent.into()), + TrackSizing::Auto, + TrackSizing::Relative(body_indent.into()), + TrackSizing::Auto, + ]), + gutter: Axes::with_y(vec![TrackSizing::Relative(gutter.into())]), + cells, + } + .pack()) + } + + fn finalize( + &self, + _: Tracked, + styles: StyleChain, + realized: Content, + ) -> SourceResult { + let mut above = styles.get(Self::ABOVE); + let mut below = styles.get(Self::BELOW); + + if self.attached { + if above.is_some() { + above = Some(styles.get(ParNode::LEADING)); + } + if below.is_some() { + below = Some(styles.get(ParNode::SPACING)); + } + } + + Ok(realized.spaced(above, below)) + } +} + +/// An item in a list. +#[derive(Clone, PartialEq, Hash)] +pub enum ListItem { + /// An item of an unordered list. + List(Box), + /// An item of an ordered list. + Enum(Option, Box), + /// An item of a description list. + Desc(Box), +} + +impl ListItem { + /// What kind of item this is. + pub fn kind(&self) -> ListKind { + match self { + Self::List(_) => LIST, + Self::Enum { .. } => ENUM, + Self::Desc { .. } => DESC, + } + } + + fn unguard(&self, sel: Selector) -> Self { + match self { + Self::List(body) => Self::List(Box::new(body.unguard(sel))), + Self::Enum(number, body) => Self::Enum(*number, Box::new(body.unguard(sel))), + Self::Desc(item) => Self::Desc(Box::new(DescItem { + term: item.term.unguard(sel), + body: item.body.unguard(sel), + })), + } + } + + /// Encode the item into a value. + fn encode(&self) -> Value { + match self { + Self::List(body) => Value::Content(body.as_ref().clone()), + Self::Enum(number, body) => Value::Dict(dict! { + "number" => match *number { + Some(n) => Value::Int(n as i64), + None => Value::None, + }, + "body" => Value::Content(body.as_ref().clone()), + }), + Self::Desc(item) => Value::Dict(dict! { + "term" => Value::Content(item.term.clone()), + "body" => Value::Content(item.body.clone()), + }), + } + } +} + +impl Debug for ListItem { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::List(body) => write!(f, "- {body:?}"), + Self::Enum(number, body) => match number { + Some(n) => write!(f, "{n}. {body:?}"), + None => write!(f, "+ {body:?}"), + }, + Self::Desc(item) => item.fmt(f), + } + } +} + +#[node] +impl ListItem {} + +/// A description list item. +#[derive(Clone, PartialEq, Hash)] +pub struct DescItem { + /// The term described by the list item. + pub term: Content, + /// The description of the term. + pub body: Content, +} + +castable! { + DescItem, + Expected: "dictionary with `term` and `body` keys", + Value::Dict(dict) => { + let term: Content = dict.get("term")?.clone().cast()?; + let body: Content = dict.get("body")?.clone().cast()?; + Self { term, body } + }, +} + +impl Debug for DescItem { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "/ {:?}: {:?}", self.term, self.body) + } +} + +/// How to label a list. +pub type ListKind = usize; + +/// An unordered list. +pub const LIST: ListKind = 0; + +/// An ordered list. +pub const ENUM: ListKind = 1; + +/// A description list. +pub const DESC: ListKind = 2; + +/// How to label a list or enumeration. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum Label { + /// The default labelling. + Default, + /// A pattern with prefix, numbering, lower / upper case and suffix. + Pattern(EcoString, Numbering, bool, EcoString), + /// Bare content. + Content(Content), + /// A closure mapping from an item number to a value. + Func(Func, Span), +} + +impl Label { + /// Resolve the value based on the level. + pub fn resolve( + &self, + world: Tracked, + kind: ListKind, + number: usize, + ) -> SourceResult { + Ok(match self { + Self::Default => match kind { + LIST => TextNode('•'.into()).pack(), + ENUM => TextNode(format_eco!("{}.", number)).pack(), + DESC | _ => panic!("description lists don't have a label"), + }, + Self::Pattern(prefix, numbering, upper, suffix) => { + let fmt = numbering.apply(number); + let mid = if *upper { fmt.to_uppercase() } else { fmt.to_lowercase() }; + TextNode(format_eco!("{}{}{}", prefix, mid, suffix)).pack() + } + Self::Content(content) => content.clone(), + Self::Func(func, span) => { + let args = Args::new(*span, [Value::Int(number as i64)]); + func.call_detached(world, args)?.display(world) + } + }) + } +} + +impl Cast> for Label { + fn is(value: &Spanned) -> bool { + matches!(&value.v, Value::Content(_) | Value::Func(_)) + } + + fn cast(value: Spanned) -> StrResult { + match value.v { + Value::None => Ok(Self::Content(Content::empty())), + Value::Str(pattern) => { + let mut s = Scanner::new(&pattern); + let mut prefix; + let numbering = loop { + prefix = s.before(); + match s.eat().map(|c| c.to_ascii_lowercase()) { + Some('1') => break Numbering::Arabic, + Some('a') => break Numbering::Letter, + Some('i') => break Numbering::Roman, + Some('*') => break Numbering::Symbol, + Some(_) => {} + None => Err("invalid pattern")?, + } + }; + let upper = s.scout(-1).map_or(false, char::is_uppercase); + let suffix = s.after().into(); + Ok(Self::Pattern(prefix.into(), numbering, upper, suffix)) + } + Value::Content(v) => Ok(Self::Content(v)), + Value::Func(v) => Ok(Self::Func(v, value.span)), + v => Err(format!( + "expected string, content or function, found {}", + v.type_name(), + )), + } + } +} diff --git a/library/src/structure/mod.rs b/library/src/structure/mod.rs new file mode 100644 index 00000000..088d1e6c --- /dev/null +++ b/library/src/structure/mod.rs @@ -0,0 +1,13 @@ +//! Document structuring. + +mod doc; +mod heading; +mod list; +mod reference; +mod table; + +pub use doc::*; +pub use heading::*; +pub use list::*; +pub use reference::*; +pub use table::*; diff --git a/library/src/structure/reference.rs b/library/src/structure/reference.rs new file mode 100644 index 00000000..632ecba5 --- /dev/null +++ b/library/src/structure/reference.rs @@ -0,0 +1,29 @@ +use crate::prelude::*; + +/// A reference to a label. +#[derive(Debug, Hash)] +pub struct RefNode(pub EcoString); + +#[node(Show)] +impl RefNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("label")?).pack()) + } +} + +impl Show for RefNode { + fn unguard_parts(&self, _: Selector) -> Content { + Self(self.0.clone()).pack() + } + + fn field(&self, name: &str) -> Option { + match name { + "label" => Some(Value::Str(self.0.clone().into())), + _ => None, + } + } + + fn realize(&self, _: Tracked, _: StyleChain) -> SourceResult { + Ok(TextNode(format_eco!("@{}", self.0)).pack()) + } +} diff --git a/library/src/structure/table.rs b/library/src/structure/table.rs new file mode 100644 index 00000000..722f11e6 --- /dev/null +++ b/library/src/structure/table.rs @@ -0,0 +1,160 @@ +use crate::layout::{BlockSpacing, GridNode, TrackSizing, TrackSizings}; +use crate::prelude::*; + +/// A table of items. +#[derive(Debug, Hash)] +pub struct TableNode { + /// Defines sizing for content rows and columns. + pub tracks: Axes>, + /// Defines sizing of gutter rows and columns between content. + pub gutter: Axes>, + /// The content to be arranged in the table. + pub cells: Vec, +} + +#[node(Show)] +impl TableNode { + /// How to fill the cells. + #[property(referenced)] + pub const FILL: Celled> = Celled::Value(None); + /// How to stroke the cells. + #[property(resolve, fold)] + pub const STROKE: Option = Some(PartialStroke::default()); + /// How much to pad the cells's content. + pub const PADDING: Rel = Abs::pt(5.0).into(); + + /// The spacing above the table. + #[property(resolve, shorthand(around))] + pub const ABOVE: Option = Some(Ratio::one().into()); + /// The spacing below the table. + #[property(resolve, shorthand(around))] + pub const BELOW: Option = Some(Ratio::one().into()); + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let TrackSizings(columns) = args.named("columns")?.unwrap_or_default(); + let TrackSizings(rows) = args.named("rows")?.unwrap_or_default(); + let TrackSizings(base_gutter) = args.named("gutter")?.unwrap_or_default(); + let column_gutter = args.named("column-gutter")?.map(|TrackSizings(v)| v); + let row_gutter = args.named("row-gutter")?.map(|TrackSizings(v)| v); + Ok(Self { + tracks: Axes::new(columns, rows), + gutter: Axes::new( + column_gutter.unwrap_or_else(|| base_gutter.clone()), + row_gutter.unwrap_or(base_gutter), + ), + cells: args.all()?, + } + .pack()) + } +} + +impl Show for TableNode { + fn unguard_parts(&self, sel: Selector) -> Content { + Self { + tracks: self.tracks.clone(), + gutter: self.gutter.clone(), + cells: self.cells.iter().map(|cell| cell.unguard(sel)).collect(), + } + .pack() + } + + fn field(&self, name: &str) -> Option { + match name { + "cells" => Some(Value::Array( + self.cells.iter().cloned().map(Value::Content).collect(), + )), + _ => None, + } + } + + fn realize( + &self, + world: Tracked, + styles: StyleChain, + ) -> SourceResult { + let fill = styles.get(Self::FILL); + let stroke = styles.get(Self::STROKE).map(PartialStroke::unwrap_or_default); + let padding = styles.get(Self::PADDING); + + let cols = self.tracks.x.len().max(1); + let cells = self + .cells + .iter() + .cloned() + .enumerate() + .map(|(i, child)| { + let mut child = child.padded(Sides::splat(padding)); + + if let Some(stroke) = stroke { + child = child.stroked(stroke); + } + + let x = i % cols; + let y = i / cols; + if let Some(fill) = fill.resolve(world, x, y)? { + child = child.filled(fill); + } + + Ok(child) + }) + .collect::>()?; + + Ok(GridNode { + tracks: self.tracks.clone(), + gutter: self.gutter.clone(), + cells, + } + .pack()) + } + + fn finalize( + &self, + _: Tracked, + styles: StyleChain, + realized: Content, + ) -> SourceResult { + Ok(realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW))) + } +} + +/// A value that can be configured per cell. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum Celled { + /// A bare value, the same for all cells. + Value(T), + /// A closure mapping from cell coordinates to a value. + Func(Func, Span), +} + +impl Celled { + /// Resolve the value based on the cell position. + pub fn resolve( + &self, + world: Tracked, + x: usize, + y: usize, + ) -> SourceResult { + Ok(match self { + Self::Value(value) => value.clone(), + Self::Func(func, span) => { + let args = Args::new(*span, [Value::Int(x as i64), Value::Int(y as i64)]); + func.call_detached(world, args)?.cast().at(*span)? + } + }) + } +} + +impl Cast> for Celled { + fn is(value: &Spanned) -> bool { + matches!(&value.v, Value::Func(_)) || T::is(&value.v) + } + + fn cast(value: Spanned) -> StrResult { + match value.v { + Value::Func(v) => Ok(Self::Func(v, value.span)), + v => T::cast(v) + .map(Self::Value) + .map_err(|msg| with_alternative(msg, "function")), + } + } +} diff --git a/library/src/text/deco.rs b/library/src/text/deco.rs new file mode 100644 index 00000000..cd3acef5 --- /dev/null +++ b/library/src/text/deco.rs @@ -0,0 +1,248 @@ +use kurbo::{BezPath, Line, ParamCurve}; +use ttf_parser::{GlyphId, OutlineBuilder}; + +use super::TextNode; +use crate::prelude::*; + +/// Typeset underline, stricken-through or overlined text. +#[derive(Debug, Hash)] +pub struct DecoNode(pub Content); + +/// Typeset underlined text. +pub type UnderlineNode = DecoNode; + +/// Typeset stricken-through text. +pub type StrikethroughNode = DecoNode; + +/// Typeset overlined text. +pub type OverlineNode = DecoNode; + +#[node(Show)] +impl DecoNode { + /// How to stroke the line. The text color and thickness are read from the + /// font tables if `auto`. + #[property(shorthand, resolve, fold)] + pub const STROKE: Smart = Smart::Auto; + /// Position of the line relative to the baseline, read from the font tables + /// if `auto`. + #[property(resolve)] + pub const OFFSET: Smart = Smart::Auto; + /// Amount that the line will be longer or shorter than its associated text. + #[property(resolve)] + pub const EXTENT: Length = Length::zero(); + /// Whether the line skips sections in which it would collide + /// with the glyphs. Does not apply to strikethrough. + pub const EVADE: bool = true; + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("body")?).pack()) + } +} + +impl Show for DecoNode { + fn unguard_parts(&self, sel: Selector) -> Content { + Self(self.0.unguard(sel)).pack() + } + + fn field(&self, name: &str) -> Option { + match name { + "body" => Some(Value::Content(self.0.clone())), + _ => None, + } + } + + fn realize( + &self, + _: Tracked, + styles: StyleChain, + ) -> SourceResult { + Ok(self.0.clone().styled(TextNode::DECO, Decoration { + line: L, + stroke: styles.get(Self::STROKE).unwrap_or_default(), + offset: styles.get(Self::OFFSET), + extent: styles.get(Self::EXTENT), + evade: styles.get(Self::EVADE), + })) + } +} + +/// Defines a line that is positioned over, under or on top of text. +/// +/// For more details, see [`DecoNode`]. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Decoration { + pub line: DecoLine, + pub stroke: PartialStroke, + pub offset: Smart, + pub extent: Abs, + pub evade: bool, +} + +/// A kind of decorative line. +pub type DecoLine = usize; + +/// A line under text. +pub const UNDERLINE: DecoLine = 0; + +/// A line through text. +pub const STRIKETHROUGH: DecoLine = 1; + +/// A line over text. +pub const OVERLINE: DecoLine = 2; + +/// Add line decorations to a single run of shaped text. +pub fn decorate( + frame: &mut Frame, + deco: &Decoration, + text: &Text, + shift: Abs, + pos: Point, + width: Abs, +) { + let font_metrics = text.font.metrics(); + let metrics = match deco.line { + STRIKETHROUGH => font_metrics.strikethrough, + OVERLINE => font_metrics.overline, + UNDERLINE | _ => font_metrics.underline, + }; + + let evade = deco.evade && deco.line != STRIKETHROUGH; + let offset = deco.offset.unwrap_or(-metrics.position.at(text.size)) - shift; + let stroke = deco.stroke.unwrap_or(Stroke { + paint: text.fill, + thickness: metrics.thickness.at(text.size), + }); + + let gap_padding = 0.08 * text.size; + let min_width = 0.162 * text.size; + + let mut 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 || !evade { + let shape = Geometry::Line(target).stroked(stroke); + frame.push(origin, Element::Shape(shape)); + } + }; + + if !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. + if 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 + }) { + // 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)), + ); + } + } + + // When emitting the decorative line segments, we move from left to + // right. The intersections are not necessarily in this order, yet. + intersections.sort(); + + for gap in intersections.chunks_exact(2) { + let l = gap[0] - gap_padding; + let r = gap[1] + gap_padding; + + if start >= end { + break; + } + + if start >= l { + start = r; + continue; + } + + push_segment(start, l); + start = r; + } + + if start < end { + push_segment(start, end); + } +} + +/// 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/library/src/text/link.rs b/library/src/text/link.rs new file mode 100644 index 00000000..82abe5cd --- /dev/null +++ b/library/src/text/link.rs @@ -0,0 +1,102 @@ +use super::TextNode; +use crate::prelude::*; + +/// Link text and other elements to a destination. +#[derive(Debug, Hash)] +pub struct LinkNode { + /// The destination the link points to. + pub dest: Destination, + /// How the link is represented. + pub body: Option, +} + +impl LinkNode { + /// Create a link node from a URL with its bare text. + pub fn from_url(url: EcoString) -> Self { + Self { dest: Destination::Url(url), body: None } + } +} + +#[node(Show)] +impl LinkNode { + /// The fill color of text in the link. Just the surrounding text color + /// if `auto`. + pub const FILL: Smart = Smart::Auto; + /// Whether to underline the link. + pub const UNDERLINE: Smart = Smart::Auto; + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let dest = args.expect::("destination")?; + let body = match dest { + Destination::Url(_) => args.eat()?, + Destination::Internal(_) => Some(args.expect("body")?), + }; + Ok(Self { dest, body }.pack()) + } +} + +impl Show for LinkNode { + fn unguard_parts(&self, sel: Selector) -> Content { + Self { + dest: self.dest.clone(), + body: self.body.as_ref().map(|body| body.unguard(sel)), + } + .pack() + } + + fn field(&self, name: &str) -> Option { + match name { + "url" => Some(match &self.dest { + Destination::Url(url) => Value::Str(url.clone().into()), + Destination::Internal(loc) => Value::Dict(loc.encode()), + }), + "body" => Some(match &self.body { + Some(body) => Value::Content(body.clone()), + None => Value::None, + }), + _ => None, + } + } + + fn realize(&self, _: Tracked, _: StyleChain) -> SourceResult { + Ok(self + .body + .clone() + .unwrap_or_else(|| match &self.dest { + Destination::Url(url) => { + let mut text = url.as_str(); + for prefix in ["mailto:", "tel:"] { + text = text.trim_start_matches(prefix); + } + let shorter = text.len() < url.len(); + TextNode(if shorter { text.into() } else { url.clone() }).pack() + } + Destination::Internal(_) => Content::empty(), + }) + .styled(TextNode::LINK, Some(self.dest.clone()))) + } + + fn finalize( + &self, + _: Tracked, + styles: StyleChain, + mut realized: Content, + ) -> SourceResult { + let mut map = StyleMap::new(); + if let Smart::Custom(fill) = styles.get(Self::FILL) { + map.set(TextNode::FILL, fill); + } + + if match styles.get(Self::UNDERLINE) { + Smart::Auto => match &self.dest { + Destination::Url(_) => true, + Destination::Internal(_) => false, + }, + Smart::Custom(underline) => underline, + } { + realized = realized.underlined(); + } + + Ok(realized.styled_with_map(map)) + } +} diff --git a/library/src/text/mod.rs b/library/src/text/mod.rs new file mode 100644 index 00000000..d793f614 --- /dev/null +++ b/library/src/text/mod.rs @@ -0,0 +1,555 @@ +//! Text handling and paragraph layout. + +mod deco; +mod link; +mod par; +mod quotes; +mod raw; +mod shaping; +mod shift; + +pub use deco::*; +pub use link::*; +pub use par::*; +pub use quotes::*; +pub use raw::*; +pub use shaping::*; +pub use shift::*; + +use std::borrow::Cow; + +use rustybuzz::Tag; +use typst::font::{FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontMetric}; +use typst::util::EcoString; + +use crate::prelude::*; + +/// A single run of text with the same style. +#[derive(Debug, Clone, Hash)] +pub struct TextNode(pub EcoString); + +#[node] +impl TextNode { + /// A prioritized sequence of font families. + #[property(skip, referenced)] + pub const FAMILY: FallbackList = FallbackList(vec![FontFamily::new("IBM Plex Sans")]); + /// Whether to allow font fallback when the primary font list contains no + /// match. + pub const FALLBACK: bool = true; + + /// How the font is styled. + pub const STYLE: FontStyle = FontStyle::Normal; + /// The boldness / thickness of the font's glyphs. + pub const WEIGHT: FontWeight = FontWeight::REGULAR; + /// The width of the glyphs. + pub const STRETCH: FontStretch = FontStretch::NORMAL; + + /// The size of the glyphs. + #[property(shorthand, fold)] + pub const SIZE: TextSize = Abs::pt(11.0); + /// The glyph fill color. + #[property(shorthand)] + pub const FILL: Paint = Color::BLACK.into(); + /// The amount of space that should be added between characters. + #[property(resolve)] + pub const TRACKING: Length = Length::zero(); + /// The width of spaces relative to the font's space width. + #[property(resolve)] + pub const SPACING: Rel = Rel::one(); + /// The offset of the baseline. + #[property(resolve)] + pub const BASELINE: Length = Length::zero(); + /// Whether certain glyphs can hang over into the margin. + pub const OVERHANG: bool = true; + /// The top end of the text bounding box. + pub const TOP_EDGE: TextEdge = TextEdge::Metric(VerticalFontMetric::CapHeight); + /// The bottom end of the text bounding box. + pub const BOTTOM_EDGE: TextEdge = TextEdge::Metric(VerticalFontMetric::Baseline); + + /// An ISO 639-1/2/3 language code. + pub const LANG: Lang = Lang::ENGLISH; + /// An ISO 3166-1 alpha-2 region code. + pub const REGION: Option = None; + /// The direction for text and inline objects. When `auto`, the direction is + /// automatically inferred from the language. + #[property(resolve)] + pub const DIR: HorizontalDir = HorizontalDir(Smart::Auto); + /// Whether to hyphenate text to improve line breaking. When `auto`, words + /// will will be hyphenated if and only if justification is enabled. + #[property(resolve)] + pub const HYPHENATE: Hyphenate = Hyphenate(Smart::Auto); + /// Whether to apply smart quotes. + pub const SMART_QUOTES: bool = true; + + /// Whether to apply kerning ("kern"). + pub const KERNING: bool = true; + /// Whether to apply stylistic alternates. ("salt") + pub const ALTERNATES: bool = false; + /// Which stylistic set to apply. ("ss01" - "ss20") + pub const STYLISTIC_SET: Option = None; + /// Whether standard ligatures are active. ("liga", "clig") + pub const LIGATURES: bool = true; + /// Whether ligatures that should be used sparingly are active. ("dlig") + pub const DISCRETIONARY_LIGATURES: bool = false; + /// Whether historical ligatures are active. ("hlig") + pub const HISTORICAL_LIGATURES: bool = false; + /// Which kind of numbers / figures to select. + pub const NUMBER_TYPE: Smart = Smart::Auto; + /// The width of numbers / figures. + pub const NUMBER_WIDTH: Smart = Smart::Auto; + /// Whether to have a slash through the zero glyph. ("zero") + pub const SLASHED_ZERO: bool = false; + /// Whether to convert fractions. ("frac") + pub const FRACTIONS: bool = false; + /// Raw OpenType features to apply. + #[property(fold)] + pub const FEATURES: FontFeatures = FontFeatures(vec![]); + + /// Whether the font weight should be increased by 300. + #[property(skip, fold)] + pub const BOLD: Toggle = false; + /// Whether the font style should be inverted. + #[property(skip, fold)] + pub const ITALIC: Toggle = false; + /// A case transformation that should be applied to the text. + #[property(skip)] + pub const CASE: Option = None; + /// Whether small capital glyphs should be used. ("smcp") + #[property(skip)] + pub const SMALLCAPS: bool = false; + /// A destination the text should be linked to. + #[property(skip, referenced)] + pub const LINK: Option = None; + /// Decorative lines. + #[property(skip, fold)] + pub const DECO: Decoration = vec![]; + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + // The text constructor is special: It doesn't create a text node. + // Instead, it leaves the passed argument structurally unchanged, but + // styles all text in it. + args.expect("body") + } + + fn set(...) { + if let Some(family) = args.named("family")? { + styles.set(Self::FAMILY, family); + } else { + let mut count = 0; + let mut content = false; + for item in args.items.iter().filter(|item| item.name.is_none()) { + if EcoString::is(&item.value) { + count += 1; + } else if >>::is(&item.value) { + content = true; + } + } + + // Skip the final string if it's needed as the body. + if constructor && !content && count > 0 { + count -= 1; + } + + if count > 0 { + let mut list = Vec::with_capacity(count); + for _ in 0 .. count { + list.push(args.find()?.unwrap()); + } + + styles.set(Self::FAMILY, FallbackList(list)); + } + } + } +} + +/// 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) + } +} + +castable! { + FontFamily, + Expected: "string", + Value::Str(string) => Self::new(&string), +} + +/// Font family fallback list. +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct FallbackList(pub Vec); + +castable! { + FallbackList, + Expected: "string or array of strings", + Value::Str(string) => Self(vec![FontFamily::new(&string)]), + Value::Array(values) => Self(values + .into_iter() + .filter_map(|v| v.cast().ok()) + .map(|string: EcoString| FontFamily::new(&string)) + .collect()), +} + +/// 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 + } +} + +castable!(TextSize: Length); + +/// 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), + } + } +} + +castable! { + TextEdge, + Expected: "string or length", + Value::Length(v) => Self::Length(v), + Value::Str(string) => Self::Metric(match string.as_str() { + "ascender" => VerticalFontMetric::Ascender, + "cap-height" => VerticalFontMetric::CapHeight, + "x-height" => VerticalFontMetric::XHeight, + "baseline" => VerticalFontMetric::Baseline, + "descender" => VerticalFontMetric::Descender, + _ => Err("unknown font metric")?, + }), +} + +/// The direction of text and inline objects in their line. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct HorizontalDir(pub Smart); + +castable! { + HorizontalDir, + Expected: "direction or auto", + Value::Auto => Self(Smart::Auto), + @dir: Dir => match dir.axis() { + Axis::X => Self(Smart::Custom(*dir)), + Axis::Y => Err("must be horizontal")?, + }, +} + +impl Resolve for HorizontalDir { + type Output = Dir; + + fn resolve(self, styles: StyleChain) -> Self::Output { + match self.0 { + Smart::Auto => styles.get(TextNode::LANG).dir(), + Smart::Custom(dir) => dir, + } + } +} + +/// Whether to hyphenate text. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Hyphenate(pub Smart); + +castable! { + Hyphenate, + Expected: "boolean or auto", + Value::Auto => Self(Smart::Auto), + Value::Bool(v) => Self(Smart::Custom(v)), +} + +impl Resolve for Hyphenate { + type Output = bool; + + fn resolve(self, styles: StyleChain) -> Self::Output { + match self.0 { + Smart::Auto => styles.get(ParNode::JUSTIFY), + 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 + } +} + +castable! { + StylisticSet, + Expected: "integer", + Value::Int(v) => match v { + 1 ..= 20 => Self::new(v as u8), + _ => Err("must be between 1 and 20")?, + }, +} + +/// Which kind of numbers / figures to select. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum NumberType { + /// Numbers that fit well with capital text. ("lnum") + Lining, + /// Numbers that fit well into a flow of upper- and lowercase text. ("onum") + OldStyle, +} + +castable! { + NumberType, + Expected: "string", + Value::Str(string) => match string.as_str() { + "lining" => Self::Lining, + "old-style" => Self::OldStyle, + _ => Err(r#"expected "lining" or "old-style""#)?, + }, +} + +/// The width of numbers / figures. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum NumberWidth { + /// Number widths are glyph specific. ("pnum") + Proportional, + /// All numbers are of equal width / monospaced. ("tnum") + Tabular, +} + +castable! { + NumberWidth, + Expected: "string", + Value::Str(string) => match string.as_str() { + "proportional" => Self::Proportional, + "tabular" => Self::Tabular, + _ => Err(r#"expected "proportional" or "tabular""#)?, + }, +} + +/// OpenType font features settings. +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct FontFeatures(pub Vec<(Tag, u32)>); + +castable! { + FontFeatures, + Expected: "array of strings or dictionary mapping tags to integers", + Value::Array(values) => Self(values + .into_iter() + .filter_map(|v| v.cast().ok()) + .map(|string: EcoString| (Tag::from_bytes_lossy(string.as_bytes()), 1)) + .collect()), + Value::Dict(values) => Self(values + .into_iter() + .filter_map(|(k, v)| { + let tag = Tag::from_bytes_lossy(k.as_bytes()); + let num = v.cast::().ok()?.try_into().ok()?; + Some((tag, num)) + }) + .collect()), +} + +impl Fold for FontFeatures { + type Output = Self; + + fn fold(mut self, outer: Self::Output) -> Self::Output { + self.0.extend(outer.0); + self + } +} + +/// A text space. +#[derive(Debug, Clone, Hash)] +pub struct SpaceNode; + +#[node] +impl SpaceNode { + fn construct(_: &mut Vm, _: &mut Args) -> SourceResult { + Ok(Self.pack()) + } +} + +/// A line break. +#[derive(Debug, Clone, Hash)] +pub struct LinebreakNode { + pub justify: bool, +} + +#[node] +impl LinebreakNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let justify = args.named("justify")?.unwrap_or(false); + Ok(Self { justify }.pack()) + } +} + +/// A smart quote. +#[derive(Debug, Clone, Hash)] +pub struct SmartQuoteNode { + pub double: bool, +} + +#[node] +impl SmartQuoteNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + let double = args.named("double")?.unwrap_or(true); + Ok(Self { double }.pack()) + } +} + +/// Convert a string or content to lowercase. +pub fn lower(_: &mut Vm, args: &mut Args) -> SourceResult { + case(Case::Lower, args) +} + +/// Convert a string or content to uppercase. +pub fn upper(_: &mut Vm, args: &mut Args) -> SourceResult { + case(Case::Upper, args) +} + +/// Change the case of text. +fn case(case: Case, args: &mut Args) -> SourceResult { + let Spanned { v, span } = args.expect("string or content")?; + Ok(match v { + Value::Str(v) => Value::Str(case.apply(&v).into()), + Value::Content(v) => Value::Content(v.styled(TextNode::CASE, Some(case))), + v => bail!(span, "expected string or content, found {}", v.type_name()), + }) +} + +/// A case transformation on text. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Case { + /// Everything is uppercased. + Upper, + /// Everything is lowercased. + Lower, +} + +impl Case { + /// Apply the case to a string. + pub fn apply(self, text: &str) -> String { + match self { + Self::Upper => text.to_uppercase(), + Self::Lower => text.to_lowercase(), + } + } +} + +/// Display text in small capitals. +pub fn smallcaps(_: &mut Vm, args: &mut Args) -> SourceResult { + let body: Content = args.expect("content")?; + Ok(Value::Content(body.styled(TextNode::SMALLCAPS, true))) +} + +/// Strong content, rendered in boldface by default. +#[derive(Debug, Hash)] +pub struct StrongNode(pub Content); + +#[node(Show)] +impl StrongNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("body")?).pack()) + } +} + +impl Show for StrongNode { + fn unguard_parts(&self, sel: Selector) -> Content { + Self(self.0.unguard(sel)).pack() + } + + fn field(&self, name: &str) -> Option { + match name { + "body" => Some(Value::Content(self.0.clone())), + _ => None, + } + } + + fn realize(&self, _: Tracked, _: StyleChain) -> SourceResult { + Ok(self.0.clone().styled(TextNode::BOLD, Toggle)) + } +} + +/// Emphasized content, rendered with an italic font by default. +#[derive(Debug, Hash)] +pub struct EmphNode(pub Content); + +#[node(Show)] +impl EmphNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("body")?).pack()) + } +} + +impl Show for EmphNode { + fn unguard_parts(&self, sel: Selector) -> Content { + Self(self.0.unguard(sel)).pack() + } + + fn field(&self, name: &str) -> Option { + match name { + "body" => Some(Value::Content(self.0.clone())), + _ => None, + } + } + + fn realize(&self, _: Tracked, _: StyleChain) -> SourceResult { + Ok(self.0.clone().styled(TextNode::ITALIC, Toggle)) + } +} + +/// A toggle that turns on and off alternatingly if folded. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Toggle; + +impl Fold for Toggle { + type Output = bool; + + fn fold(self, outer: Self::Output) -> Self::Output { + !outer + } +} + +impl Fold for Decoration { + type Output = Vec; + + fn fold(self, mut outer: Self::Output) -> Self::Output { + outer.insert(0, self); + outer + } +} diff --git a/library/src/text/par.rs b/library/src/text/par.rs new file mode 100644 index 00000000..95371e1a --- /dev/null +++ b/library/src/text/par.rs @@ -0,0 +1,1259 @@ +use std::cmp::Ordering; + +use typst::util::EcoString; +use unicode_bidi::{BidiInfo, Level as BidiLevel}; +use unicode_script::{Script, UnicodeScript}; +use xi_unicode::LineBreakIterator; + +use super::{shape, Lang, Quoter, Quotes, ShapedText, TextNode}; +use crate::layout::Spacing; +use crate::prelude::*; + +/// Arrange text, spacing and inline-level nodes into a paragraph. +#[derive(Hash)] +pub struct ParNode(pub StyleVec); + +/// A uniformly styled atomic piece of a paragraph. +#[derive(Hash, PartialEq)] +pub enum ParChild { + /// A chunk of text. + Text(EcoString), + /// A single or double smart quote. + Quote { double: bool }, + /// Horizontal spacing between other children. + Spacing(Spacing), + /// Arbitrary inline-level content. + Inline(Content), +} + +#[node(LayoutBlock)] +impl ParNode { + /// The spacing between lines. + #[property(resolve)] + pub const LEADING: Length = Em::new(0.65).into(); + /// The extra spacing between paragraphs. + #[property(resolve)] + pub const SPACING: Length = Em::new(1.2).into(); + /// The indent the first line of a consecutive paragraph should have. + #[property(resolve)] + pub const INDENT: Length = Length::zero(); + /// Whether to allow paragraph spacing when there is paragraph indent. + pub const SPACING_AND_INDENT: bool = false; + + /// How to align text and inline objects in their line. + #[property(resolve)] + pub const ALIGN: HorizontalAlign = HorizontalAlign(GenAlign::Start); + /// Whether to justify text in its line. + pub const JUSTIFY: bool = false; + /// How to determine line breaks. + pub const LINEBREAKS: Smart = Smart::Auto; + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + // The paragraph constructor is special: It doesn't create a paragraph + // node. Instead, it just ensures that the passed content lives is in a + // separate paragraph and styles it. + Ok(Content::sequence(vec![ + ParbreakNode.pack(), + args.expect("body")?, + ParbreakNode.pack(), + ])) + } +} + +impl LayoutBlock for ParNode { + fn layout_block( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + // Collect all text into one string for BiDi analysis. + let (text, segments) = collect(self, &styles); + + // 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(world, self, &text, segments, regions, styles)?; + + // Break the paragraph into lines. + let lines = linebreak(&p, world, regions.first.x); + + // Stack the lines into one frame per region. + stack(&p, world, &lines, regions) + } +} + +impl Debug for ParNode { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Par ")?; + self.0.fmt(f) + } +} + +impl Debug for ParChild { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Text(text) => write!(f, "Text({:?})", text), + Self::Quote { double } => write!(f, "Quote({double})"), + Self::Spacing(kind) => write!(f, "{:?}", kind), + Self::Inline(inline) => inline.fmt(f), + } + } +} + +impl PartialOrd for ParChild { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (Self::Spacing(a), Self::Spacing(b)) => a.partial_cmp(b), + _ => None, + } + } +} + +/// A horizontal alignment. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct HorizontalAlign(pub GenAlign); + +castable! { + HorizontalAlign, + Expected: "alignment", + @align: GenAlign => match align.axis() { + Axis::X => Self(*align), + Axis::Y => Err("must be horizontal")?, + }, +} + +impl Resolve for HorizontalAlign { + type Output = Align; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.0.resolve(styles) + } +} + +/// How to determine line breaks in a paragraph. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Linebreaks { + /// Determine the linebreaks in a simple first-fit style. + Simple, + /// Optimize the linebreaks for the whole paragraph. + Optimized, +} + +castable! { + Linebreaks, + Expected: "string", + Value::Str(string) => match string.as_str() { + "simple" => Self::Simple, + "optimized" => Self::Optimized, + _ => Err(r#"expected "simple" or "optimized""#)?, + }, +} + +/// A paragraph break. +#[derive(Debug, Clone, Hash)] +pub struct ParbreakNode; + +#[node] +impl ParbreakNode { + fn construct(_: &mut Vm, _: &mut Args) -> SourceResult { + Ok(Self.pack()) + } +} + +/// Repeats content to fill a line. +#[derive(Debug, Hash)] +pub struct RepeatNode(pub Content); + +#[node(LayoutInline)] +impl RepeatNode { + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("body")?).pack()) + } +} + +impl LayoutInline for RepeatNode { + fn layout_inline( + &self, + world: Tracked, + regions: &Regions, + styles: StyleChain, + ) -> SourceResult> { + self.0.layout_inline(world, regions, styles) + } +} + +/// Range of a substring of text. +type Range = std::ops::Range; + +// The characters by which spacing, inline content and pins are replaced in the +// paragraph's full text. +const SPACING_REPLACE: char = ' '; // Space +const NODE_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 nodes. + items: Vec>, + /// The styles shared by all children. + styles: StyleChain<'a>, + /// Whether to hyphenate if it's the same for all children. + hyphenate: Option, + /// The text language if it's the same for all children. + lang: Option, + /// The resolved leading between lines. + leading: Abs, + /// The paragraph's resolved alignment. + align: Align, + /// Whether to justify the paragraph. + justify: bool, +} + +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), + /// Arbitrary inline-level content. + Inline(&'a Content), +} + +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::Inline(_) => NODE_REPLACE.len_utf8(), + } + } +} + +/// 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), + /// Layouted inline-level content. + Frame(Frame), + /// A repeating node that fills the remaining space in a line. + Repeat(&'a RepeatNode, StyleChain<'a>), +} + +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, + } + } + + /// 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(_) | Self::Repeat(_, _) => NODE_REPLACE.len_utf8(), + } + } + + /// 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::Repeat(_, _) => Abs::zero(), + } + } +} + +/// 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 comitting 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>, + /// 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>, + /// 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> { + 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> { + 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 justifiable glyphs the line contains. + fn justifiables(&self) -> usize { + let mut count = 0; + for shaped in self.items().filter_map(Item::text) { + count += shaped.justifiables(); + } + count + } + + /// How much of the line is stretchable spaces. + fn stretch(&self) -> Abs { + let mut stretch = Abs::zero(); + for shaped in self.items().filter_map(Item::text) { + stretch += shaped.stretch(); + } + stretch + } + + /// The sum of fractions in the line. + fn fr(&self) -> Fr { + self.items() + .filter_map(|item| match item { + Item::Fractional(fr) => Some(*fr), + Item::Repeat(_, _) => Some(Fr::one()), + _ => None, + }) + .sum() + } +} + +/// Collect all text of the paragraph into one string. This also performs +/// string-level preprocessing like case transformations. +fn collect<'a>( + par: &'a ParNode, + styles: &'a StyleChain<'a>, +) -> (String, Vec<(Segment<'a>, StyleChain<'a>)>) { + let mut full = String::new(); + let mut quoter = Quoter::new(); + let mut segments = vec![]; + let mut iter = par.0.iter().peekable(); + + while let Some((child, map)) = iter.next() { + let styles = map.chain(styles); + let segment = match child { + ParChild::Text(text) => { + let prev = full.len(); + if let Some(case) = styles.get(TextNode::CASE) { + full.push_str(&case.apply(text)); + } else { + full.push_str(text); + } + Segment::Text(full.len() - prev) + } + &ParChild::Quote { double } => { + let prev = full.len(); + if styles.get(TextNode::SMART_QUOTES) { + let lang = styles.get(TextNode::LANG); + let region = styles.get(TextNode::REGION); + let quotes = Quotes::from_lang(lang, region); + let peeked = iter.peek().and_then(|(child, _)| match child { + ParChild::Text(text) => text.chars().next(), + ParChild::Quote { .. } => Some('"'), + ParChild::Spacing(_) => Some(SPACING_REPLACE), + ParChild::Inline(_) => Some(NODE_REPLACE), + }); + + full.push_str(quoter.quote("es, double, peeked)); + } else { + full.push(if double { '"' } else { '\'' }); + } + Segment::Text(full.len() - prev) + } + &ParChild::Spacing(spacing) => { + full.push(SPACING_REPLACE); + Segment::Spacing(spacing) + } + ParChild::Inline(inline) => { + full.push(NODE_REPLACE); + Segment::Inline(inline) + } + }; + + if let Some(last) = full.chars().last() { + quoter.last(last); + } + + 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)); + } + + (full, segments) +} + +/// Prepare paragraph layout by shaping the whole paragraph and layouting all +/// contained inline-level content. +fn prepare<'a>( + world: Tracked, + par: &'a ParNode, + text: &'a str, + segments: Vec<(Segment<'a>, StyleChain<'a>)>, + regions: &Regions, + styles: StyleChain<'a>, +) -> SourceResult> { + let bidi = BidiInfo::new(text, match styles.get(TextNode::DIR) { + 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, world, &bidi, cursor .. end, styles); + } + Segment::Spacing(spacing) => match spacing { + Spacing::Relative(v) => { + let resolved = v.resolve(styles).relative_to(regions.base.x); + items.push(Item::Absolute(resolved)); + } + Spacing::Fractional(v) => { + items.push(Item::Fractional(v)); + } + }, + Segment::Inline(inline) => { + if let Some(repeat) = inline.downcast::() { + items.push(Item::Repeat(repeat, styles)); + } else { + let size = Size::new(regions.first.x, regions.base.y); + let pod = Regions::one(size, regions.base, Axes::splat(false)); + let mut frame = inline.layout_inline(world, &pod, styles)?.remove(0); + frame.translate(Point::with_y(styles.get(TextNode::BASELINE))); + frame.apply_role(Role::GenericInline); + items.push(Item::Frame(frame)); + } + } + } + + cursor = end; + } + + Ok(Preparation { + bidi, + items, + styles, + hyphenate: shared_get(styles, &par.0, TextNode::HYPHENATE), + lang: shared_get(styles, &par.0, TextNode::LANG), + leading: styles.get(ParNode::LEADING), + align: styles.get(ParNode::ALIGN), + justify: styles.get(ParNode::JUSTIFY), + }) +} + +/// 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>, + world: Tracked, + bidi: &BidiInfo<'a>, + range: Range, + styles: StyleChain<'a>, +) { + let mut process = |text, level: BidiLevel| { + let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL }; + let shaped = shape(world, text, styles, dir); + 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 cursor .. range.end { + 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(&bidi.text[cursor .. i], prev_level); + } + cursor = i; + prev_level = level; + prev_script = script; + } else if is_generic_script(prev_script) { + prev_script = script; + } + } + + process(&bidi.text[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<'a, K: Key<'a>>( + styles: StyleChain<'a>, + children: &StyleVec, + key: K, +) -> Option { + children + .styles() + .all(|map| !map.contains(key)) + .then(|| styles.get(key)) +} + +/// Find suitable linebreaks. +fn linebreak<'a>( + p: &'a Preparation<'a>, + world: Tracked, + width: Abs, +) -> Vec> { + let linebreaks = p.styles.get(ParNode::LINEBREAKS).unwrap_or_else(|| { + if p.styles.get(ParNode::JUSTIFY) { + Linebreaks::Optimized + } else { + Linebreaks::Simple + } + }); + + match linebreaks { + Linebreaks::Simple => linebreak_simple(p, world, width), + Linebreaks::Optimized => linebreak_optimized(p, world, 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>( + p: &'a Preparation<'a>, + world: Tracked, + width: Abs, +) -> Vec> { + 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(p, world, 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(p, world, 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>( + p: &'a Preparation<'a>, + world: Tracked, + width: Abs, +) -> Vec> { + /// 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 = 30.0; + const MAX_COST: Cost = 1_000_000.0; + const MIN_COST: Cost = -MAX_COST; + const MIN_RATIO: f64 = -0.15; + + // Dynamic programming table. + let mut active = 0; + let mut table = vec![Entry { + pred: 0, + total: 0.0, + line: line(p, world, 0 .. 0, false, false), + }]; + + let em = p.styles.get(TextNode::SIZE); + + for (end, mandatory, hyphen) in breakpoints(p) { + let k = table.len(); + let eof = end == p.bidi.text.len(); + let mut best: Option = 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(p, world, 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; + let mut ratio = delta / attempt.stretch(); + if ratio.is_infinite() { + ratio = delta / (em / 2.0); + } + + // At some point, it doesn't matter any more. + ratio = ratio.min(10.0); + + // Determine the cost of the line. + let min_ratio = if attempt.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 deactive + // this breakpoint. + active = i + 1; + MAX_COST + } else if mandatory || eof { + // This is a mandatory break and the line is not overfull, so it + // has minimum cost. All breakpoints before this one become + // inactive since no line can span above the mandatory break. + active = k; + MIN_COST + if attempt.justify { 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; + } + + // 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 +} + +/// 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) -> Breakpoints<'a> { + Breakpoints { + p, + linebreaks: LineBreakIterator::new(p.bidi.text), + 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: LineBreakIterator<'a>, + /// Iterator over syllables of the current word. + syllables: Option>, + /// 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 { + // 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)); + } + + // Get the next "word". + (self.end, self.mandatory) = self.linebreaks.next()?; + + // 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(shaped.styles.get(TextNode::HYPHENATE)) + }) + .unwrap_or(false) + } + + /// The text language at the given offset. + fn lang(&self, offset: usize) -> Option { + let lang = self.p.lang.or_else(|| { + let shaped = self.p.find(offset)?.text()?; + Some(shaped.styles.get(TextNode::LANG)) + })?; + + 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>( + p: &'a Preparation, + world: Tracked, + 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]; + let trimmed = text.trim_end(); + 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}'); + + // 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 { + if hyphen || start < range.end || before.is_empty() { + let shifted = start - base .. range.end - base; + let mut reshaped = shaped.reshape(world, shifted); + if hyphen || shy { + reshaped.push_hyphen(world); + } + width += reshaped.width; + last = Some(Item::Text(reshaped)); + } + + inner = before; + } + } + + // 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 { + if range.start < end { + let shifted = range.start - base .. end - base; + let reshaped = shaped.reshape(world, shifted); + width += reshaped.width; + first = Some(Item::Text(reshaped)); + } + + inner = after; + } + } + + // 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 stack( + p: &Preparation, + world: Tracked, + lines: &[Line], + regions: &Regions, +) -> SourceResult> { + // Determine the paragraph's width: Full width of the region if we + // should expand or there's fractional spacing, fit-to-width otherwise. + let mut width = regions.first.x; + if !regions.expand.x && lines.iter().all(|line| line.fr().is_zero()) { + width = lines.iter().map(|line| line.width).max().unwrap_or_default(); + } + + // State for final frame building. + let mut regions = regions.clone(); + let mut finished = vec![]; + let mut first = true; + let mut output = Frame::new(Size::with_x(width)); + output.apply_role(Role::Paragraph); + + // Stack the lines into one frame per region. + for line in lines { + let frame = commit(p, world, line, ®ions, width)?; + let height = frame.size().y; + + while !regions.first.y.fits(height) && !regions.in_last() { + finished.push(output); + output = Frame::new(Size::with_x(width)); + output.apply_role(Role::Paragraph); + regions.next(); + first = true; + } + + if !first { + output.size_mut().y += p.leading; + } + + let pos = Point::with_y(output.height()); + output.size_mut().y += height; + output.push_frame(pos, frame); + + regions.first.y -= height + p.leading; + first = false; + } + + finished.push(output); + Ok(finished) +} + +/// Commit to a line and build its frame. +fn commit( + p: &Preparation, + world: Tracked, + line: &Line, + regions: &Regions, + width: Abs, +) -> SourceResult { + let mut remaining = width - line.width; + let mut offset = Abs::zero(); + + // Reorder the line from logical to visual order. + let reordered = reorder(line); + + // 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() + && text.styles.get(TextNode::OVERHANG) + && (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() + && text.styles.get(TextNode::OVERHANG) + && (reordered.len() > 1 || text.glyphs.len() > 1) + { + let amount = overhang(glyph.c) * glyph.x_advance.at(text.size); + remaining += amount; + } + } + } + + // Determine how much to justify each space. + let fr = line.fr(); + let mut justification = Abs::zero(); + if remaining < Abs::zero() || (line.justify && fr.is_zero()) { + let justifiables = line.justifiables(); + if justifiables > 0 { + 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) => { + offset += v.share(fr, remaining); + } + Item::Text(shaped) => { + let frame = shaped.build(world, justification); + push(&mut offset, frame); + } + Item::Frame(frame) => { + push(&mut offset, frame.clone()); + } + Item::Repeat(repeat, styles) => { + let before = offset; + let fill = Fr::one().share(fr, remaining); + let size = Size::new(fill, regions.base.y); + let pod = Regions::one(size, regions.base, Axes::new(false, false)); + let frame = repeat.layout_inline(world, &pod, *styles)?.remove(0); + let width = frame.width(); + let count = (fill / width).floor(); + let remaining = fill % width; + let apart = remaining / (count - 1.0); + if count == 1.0 { + offset += p.align.position(remaining); + } + if width > Abs::zero() { + for _ in 0 .. (count as usize).min(1000) { + push(&mut offset, frame.clone()); + offset += apart; + } + } + offset = before + fill; + } + } + } + + // 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>> { + let mut reordered = vec![]; + + // The bidi crate doesn't like empty lines. + if line.trimmed.is_empty() { + return line.slice(line.trimmed.clone()).collect(); + } + + // 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()); + + // 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 +} + +/// 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 and Ideographic + '\u{60C}' | '\u{6D4}' => 0.4, + '\u{3001}' | '\u{3002}' => 1.0, + + _ => 0.0, + } +} diff --git a/library/src/text/quotes.rs b/library/src/text/quotes.rs new file mode 100644 index 00000000..ab4d3f9d --- /dev/null +++ b/library/src/text/quotes.rs @@ -0,0 +1,149 @@ +use typst::syntax::is_newline; + +use super::{Lang, Region}; + +/// State machine for smart quote subtitution. +#[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, + ) -> &'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 subtitute 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, 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) -> 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" => ("’", "’", "”", "”"), + "hu" | "pl" | "ro" => ("’", "’", "„", "”"), + "ru" | "no" | "nn" => ("’", "’", "«", "»"), + _ => 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/library/src/text/raw.rs b/library/src/text/raw.rs new file mode 100644 index 00000000..31f1517e --- /dev/null +++ b/library/src/text/raw.rs @@ -0,0 +1,206 @@ +use once_cell::sync::Lazy; +use syntect::easy::HighlightLines; +use syntect::highlighting::{ + Color, FontStyle, Style, StyleModifier, Theme, ThemeItem, ThemeSettings, +}; +use syntect::parsing::SyntaxSet; +use typst::syntax; + +use super::{FontFamily, Hyphenate, LinebreakNode, TextNode}; +use crate::layout::{BlockNode, BlockSpacing}; +use crate::prelude::*; + +/// Monospaced text with optional syntax highlighting. +#[derive(Debug, Hash)] +pub struct RawNode { + /// The raw text. + pub text: EcoString, + /// Whether the node is block-level. + pub block: bool, +} + +#[node(Show)] +impl RawNode { + /// The language to syntax-highlight in. + #[property(referenced)] + pub const LANG: Option = None; + /// The raw text's font family. + #[property(referenced)] + pub const FAMILY: FontFamily = FontFamily::new("IBM Plex Mono"); + /// The spacing above block-level raw. + #[property(resolve, shorthand(around))] + pub const ABOVE: Option = Some(Ratio::one().into()); + /// The spacing below block-level raw. + #[property(resolve, shorthand(around))] + pub const BELOW: Option = Some(Ratio::one().into()); + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(Self { + text: args.expect("text")?, + block: args.named("block")?.unwrap_or(false), + } + .pack()) + } +} + +impl Show for RawNode { + fn unguard_parts(&self, _: Selector) -> Content { + Self { text: self.text.clone(), ..*self }.pack() + } + + fn field(&self, name: &str) -> Option { + match name { + "text" => Some(Value::Str(self.text.clone().into())), + "block" => Some(Value::Bool(self.block)), + _ => None, + } + } + + fn realize( + &self, + _: Tracked, + styles: StyleChain, + ) -> SourceResult { + let lang = styles.get(Self::LANG).as_ref().map(|s| s.to_lowercase()); + let foreground = THEME + .settings + .foreground + .map(Color::from) + .unwrap_or(Color::BLACK) + .into(); + + let mut realized = if matches!(lang.as_deref(), Some("typ" | "typst" | "typc")) { + let root = match lang.as_deref() { + Some("typc") => syntax::parse_code(&self.text), + _ => syntax::parse(&self.text), + }; + + let mut seq = vec![]; + syntax::highlight::highlight_themed(&root, &THEME, |range, style| { + seq.push(styled(&self.text[range], foreground, 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 = HighlightLines::new(syntax, &THEME); + for (i, line) in self.text.lines().enumerate() { + if i != 0 { + seq.push(LinebreakNode { justify: false }.pack()); + } + + for (style, piece) in + highlighter.highlight_line(line, &SYNTAXES).into_iter().flatten() + { + seq.push(styled(piece, foreground, style)); + } + } + + Content::sequence(seq) + } else { + TextNode(self.text.clone()).pack() + }; + + if self.block { + realized = BlockNode(realized).pack(); + } + + let mut map = StyleMap::new(); + map.set(TextNode::OVERHANG, false); + map.set(TextNode::HYPHENATE, Hyphenate(Smart::Custom(false))); + map.set(TextNode::SMART_QUOTES, false); + + Ok(realized.styled_with_map(map)) + } + + fn finalize( + &self, + _: Tracked, + styles: StyleChain, + mut realized: Content, + ) -> SourceResult { + let mut map = StyleMap::new(); + map.set_family(styles.get(Self::FAMILY).clone(), styles); + + if self.block { + realized = realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW)); + } + + Ok(realized.styled_with_map(map)) + } +} + +/// Style a piece of text with a syntect style. +fn styled(piece: &str, foreground: Paint, style: Style) -> Content { + let mut body = TextNode(piece.into()).pack(); + + let paint = style.foreground.into(); + if paint != foreground { + body = body.styled(TextNode::FILL, paint); + } + + if style.font_style.contains(FontStyle::BOLD) { + body = body.strong(); + } + + if style.font_style.contains(FontStyle::ITALIC) { + body = body.emph(); + } + + if style.font_style.contains(FontStyle::UNDERLINE) { + body = body.underlined(); + } + + body +} + +/// The syntect syntax definitions. +static SYNTAXES: Lazy = Lazy::new(|| SyntaxSet::load_defaults_newlines()); + +/// The default theme used for syntax highlighting. +#[rustfmt::skip] +pub static THEME: Lazy = Lazy::new(|| Theme { + name: Some("Typst Light".into()), + author: Some("The Typst Project Developers".into()), + settings: ThemeSettings::default(), + scopes: vec![ + item("comment", Some("#8a8a8a"), None), + item("constant.character.escape", Some("#1d6c76"), None), + item("constant.character.shortcut", Some("#1d6c76"), None), + item("markup.bold", None, Some(FontStyle::BOLD)), + item("markup.italic", None, Some(FontStyle::ITALIC)), + item("markup.underline", None, Some(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(FontStyle::BOLD)), + item("markup.heading.typst", None, Some(FontStyle::BOLD | FontStyle::UNDERLINE)), + item("punctuation.definition.list", Some("#8b41b1"), None), + item("markup.list.term", None, Some(FontStyle::BOLD)), + item("entity.name.label, markup.other.reference", Some("#1d6c76"), None), + item("keyword, constant.language, variable.language", Some("#d73a49"), None), + item("storage.type, storage.modifier", Some("#d73a49"), None), + item("constant", Some("#b60157"), None), + item("string", Some("#298e0d"), None), + item("entity.name, variable.function, support", Some("#4b69c6"), None), + item("support.macro", Some("#16718d"), None), + item("meta.annotation", Some("#301414"), None), + item("entity.other, meta.interpolation", Some("#8b41b1"), None), + item("invalid", Some("#ff0000"), None), + ], +}); + +/// Create a syntect theme item. +fn item(scope: &str, color: Option<&str>, font_style: Option) -> ThemeItem { + ThemeItem { + scope: scope.parse().unwrap(), + style: StyleModifier { + foreground: color.map(|s| s.parse::().unwrap().into()), + background: None, + font_style, + }, + } +} diff --git a/library/src/text/shaping.rs b/library/src/text/shaping.rs new file mode 100644 index 00000000..32143862 --- /dev/null +++ b/library/src/text/shaping.rs @@ -0,0 +1,655 @@ +use std::ops::Range; +use std::str::FromStr; + +use rustybuzz::{Feature, Tag, UnicodeBuffer}; +use typst::font::{Font, FontVariant}; +use typst::util::SliceExt; + +use super::*; +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 text that was shaped. + pub text: &'a str, + /// The text direction. + pub dir: Dir, + /// 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 byte index in the source text where this glyph's cluster starts. A + /// cluster is a sequence of one or multiple glyphs that cannot be + /// separated and must always be treated as a union. + pub cluster: 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, +} + +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 { + self.is_space() || matches!(self.c, ',' | '。' | '、') + } +} + +/// 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, world: Tracked, justification: Abs) -> Frame { + let (top, bottom) = self.measure(world); + 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 = self.styles.get(TextNode::BASELINE); + let lang = self.styles.get(TextNode::LANG); + let decos = self.styles.get(TextNode::DECO); + let fill = self.styles.get(TextNode::FILL); + let link = self.styles.get(TextNode::LINK); + + for ((font, y_offset), group) in + self.glyphs.as_ref().group_by_key(|g| (g.font.clone(), g.y_offset)) + { + let pos = Point::new(offset, top + shift + y_offset.at(self.size)); + + let glyphs = group + .iter() + .map(|glyph| Glyph { + id: glyph.glyph_id, + x_advance: glyph.x_advance + + if glyph.is_justifiable() { + frame.size_mut().x += justification; + Em::from_length(justification, self.size) + } else { + Em::zero() + }, + x_offset: glyph.x_offset, + c: glyph.c, + }) + .collect(); + + let text = Text { + font, + size: self.size, + lang, + fill, + glyphs, + }; + + let text_layer = frame.layer(); + let width = text.width(); + + // Apply line decorations. + for deco in &decos { + decorate(&mut frame, deco, &text, shift, pos, width); + } + + frame.insert(text_layer, pos, Element::Text(text)); + offset += width; + } + + // Apply link if it exists. + if let Some(dest) = link { + frame.link(dest.clone()); + } + + frame + } + + /// Measure the top and bottom extent of this text. + fn measure(&self, world: Tracked) -> (Abs, Abs) { + let mut top = Abs::zero(); + let mut bottom = Abs::zero(); + + let top_edge = self.styles.get(TextNode::TOP_EDGE); + let bottom_edge = self.styles.get(TextNode::BOTTOM_EDGE); + + // 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. + for family in families(self.styles) { + if let Some(font) = world + .book() + .select(family, 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 justifiable glyphs the text contains. + pub fn justifiables(&self) -> usize { + self.glyphs.iter().filter(|g| g.is_justifiable()).count() + } + + /// The width of the spaces in the text. + pub fn stretch(&self) -> Abs { + self.glyphs + .iter() + .filter(|g| g.is_justifiable()) + .map(|g| g.x_advance) + .sum::() + .at(self.size) + } + + /// Reshape a range of the shaped text, reusing information from this + /// shaping process if possible. + pub fn reshape( + &'a self, + world: Tracked, + text_range: Range, + ) -> ShapedText<'a> { + if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { + Self { + text: &self.text[text_range], + dir: self.dir, + styles: self.styles, + size: self.size, + variant: self.variant, + width: glyphs.iter().map(|g| g.x_advance).sum::().at(self.size), + glyphs: Cow::Borrowed(glyphs), + } + } else { + shape(world, &self.text[text_range], self.styles, self.dir) + } + } + + /// Push a hyphen to end of the text. + pub fn push_hyphen(&mut self, world: Tracked) { + families(self.styles).find_map(|family| { + let font = world + .book() + .select(family, 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 cluster = self.glyphs.last().map(|g| g.cluster).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(), + cluster, + safe_to_break: true, + c: '-', + }); + 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) -> 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 { + let ltr = self.dir.is_positive(); + + // Handle edge cases. + let len = self.glyphs.len(); + if text_index == 0 { + return Some(if ltr { 0 } else { len }); + } else if text_index == 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.cluster.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.cluster != 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(|| idx + (!ltr) as usize) + } +} + +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> { + world: Tracked<'a, dyn World>, + glyphs: Vec, + used: Vec, + styles: StyleChain<'a>, + size: Abs, + variant: FontVariant, + tags: Vec, + fallback: bool, + dir: Dir, +} + +/// Shape text into [`ShapedText`]. +pub fn shape<'a>( + world: Tracked, + text: &'a str, + styles: StyleChain<'a>, + dir: Dir, +) -> ShapedText<'a> { + let size = styles.get(TextNode::SIZE); + + let mut ctx = ShapingContext { + world, + size, + glyphs: vec![], + used: vec![], + styles, + variant: variant(styles), + tags: tags(styles), + fallback: styles.get(TextNode::FALLBACK), + dir, + }; + + if !text.is_empty() { + shape_segment(&mut ctx, 0, text, families(styles)); + } + + track_and_space(&mut ctx); + + ShapedText { + text, + dir, + styles, + variant: ctx.variant, + size, + width: ctx.glyphs.iter().map(|g| g.x_advance).sum::().at(size), + glyphs: Cow::Owned(ctx.glyphs), + } +} + +/// Shape text with font fallback using the `families` iterator. +fn shape_segment<'a>( + ctx: &mut ShapingContext, + base: usize, + text: &str, + mut families: impl Iterator + Clone, +) { + // Fonts dont have newlines and tabs. + if text.chars().all(|c| c == '\n' || c == '\t') { + return; + } + + // Find the next available family. + let book = ctx.world.book(); + let mut selection = families.find_map(|family| { + book.select(family, ctx.variant) + .and_then(|id| ctx.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| ctx.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 font = if let Some(font) = selection { + font + } 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(); + + // 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; + + if info.glyph_id != 0 { + // Add the glyph to the shaped output. + // TODO: Don't ignore y_advance. + ctx.glyphs.push(ShapedGlyph { + font: font.clone(), + glyph_id: info.glyph_id as u16, + 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), + cluster: base + cluster, + safe_to_break: !info.unsafe_to_break(), + c: text[cluster ..].chars().next().unwrap(), + }); + } else { + // Determine the source text range for the tofu sequence. + let range = { + // 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. + // + // 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 ltr = ctx.dir.is_positive(); + let first = if ltr { k } else { i }; + let start = infos[first].cluster as usize; + let last = if ltr { i.checked_add(1) } else { k.checked_sub(1) }; + let end = last + .and_then(|last| infos.get(last)) + .map_or(text.len(), |info| info.cluster as usize); + + start .. end + }; + + // Trim half-baked cluster. + let remove = base + range.start .. base + range.end; + while ctx.glyphs.last().map_or(false, |g| remove.contains(&g.cluster)) { + ctx.glyphs.pop(); + } + + // Recursively shape the tofu sequence with the next family. + shape_segment(ctx, base + range.start, &text[range], 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(); + for (cluster, c) in text.char_indices() { + ctx.glyphs.push(ShapedGlyph { + font: font.clone(), + glyph_id: 0, + x_advance, + x_offset: Em::zero(), + y_offset: Em::zero(), + cluster: base + cluster, + safe_to_break: true, + c, + }); + } +} + +/// Apply tracking and spacing to the shaped glyphs. +fn track_and_space(ctx: &mut ShapingContext) { + let tracking = Em::from_length(ctx.styles.get(TextNode::TRACKING), ctx.size); + let spacing = ctx + .styles + .get(TextNode::SPACING) + .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.cluster != next.cluster) { + glyph.x_advance += tracking; + } + } +} + +/// Difference between non-breaking and normal space. +fn nbsp_delta(font: &Font) -> Option { + 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 with `BOLD` and `ITALIC` factored in. +pub fn variant(styles: StyleChain) -> FontVariant { + let mut variant = FontVariant::new( + styles.get(TextNode::STYLE), + styles.get(TextNode::WEIGHT), + styles.get(TextNode::STRETCH), + ); + + if styles.get(TextNode::BOLD) { + variant.weight = variant.weight.thicken(300); + } + + if styles.get(TextNode::ITALIC) { + 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. +fn families(styles: StyleChain) -> impl Iterator + Clone { + const FALLBACKS: &[&str] = &[ + "ibm plex sans", + "twitter color emoji", + "noto color emoji", + "apple color emoji", + "segoe ui emoji", + ]; + + let tail = if styles.get(TextNode::FALLBACK) { FALLBACKS } else { &[] }; + styles + .get(TextNode::FAMILY) + .0 + .iter() + .map(|family| family.as_str()) + .chain(tail.iter().copied()) +} + +/// Collect the tags of the OpenType features to apply. +fn tags(styles: StyleChain) -> Vec { + 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 !styles.get(TextNode::KERNING) { + feat(b"kern", 0); + } + + // Features that are off by default in Harfbuzz are only added if enabled. + if styles.get(TextNode::SMALLCAPS) { + feat(b"smcp", 1); + } + + if styles.get(TextNode::ALTERNATES) { + feat(b"salt", 1); + } + + let storage; + if let Some(set) = styles.get(TextNode::STYLISTIC_SET) { + storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10]; + feat(&storage, 1); + } + + if !styles.get(TextNode::LIGATURES) { + feat(b"liga", 0); + feat(b"clig", 0); + } + + if styles.get(TextNode::DISCRETIONARY_LIGATURES) { + feat(b"dlig", 1); + } + + if styles.get(TextNode::HISTORICAL_LIGATURES) { + feat(b"hilg", 1); + } + + match styles.get(TextNode::NUMBER_TYPE) { + Smart::Auto => {} + Smart::Custom(NumberType::Lining) => feat(b"lnum", 1), + Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1), + } + + match styles.get(TextNode::NUMBER_WIDTH) { + Smart::Auto => {} + Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1), + Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1), + } + + if styles.get(TextNode::SLASHED_ZERO) { + feat(b"zero", 1); + } + + if styles.get(TextNode::FRACTIONS) { + feat(b"frac", 1); + } + + for (tag, value) in styles.get(TextNode::FEATURES).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 = styles.get(TextNode::LANG).as_str().into(); + if let Some(region) = styles.get(TextNode::REGION) { + bcp.push('-'); + bcp.push_str(region.as_str()); + } + rustybuzz::Language::from_str(&bcp).unwrap() +} diff --git a/library/src/text/shift.rs b/library/src/text/shift.rs new file mode 100644 index 00000000..e5f142dd --- /dev/null +++ b/library/src/text/shift.rs @@ -0,0 +1,188 @@ +use typst::model::SequenceNode; +use typst::util::EcoString; + +use super::{variant, SpaceNode, TextNode, TextSize}; +use crate::prelude::*; + +/// Sub or superscript text. +/// +/// The text is rendered smaller and its baseline is raised. To provide the best +/// typography possible, we first try to transform the text to superscript +/// codepoints. If that fails, we fall back to rendering shrunk normal letters +/// in a raised way. +#[derive(Debug, Hash)] +pub struct ShiftNode(pub Content); + +/// Shift the text into superscript. +pub type SuperNode = ShiftNode; + +/// Shift the text into subscript. +pub type SubNode = ShiftNode; + +#[node(Show)] +impl ShiftNode { + /// Whether to prefer the dedicated sub- and superscript characters of the + /// font. + pub const TYPOGRAPHIC: bool = true; + /// The baseline shift for synthetic sub- and superscripts. + pub const BASELINE: Length = + Em::new(if S == SUPERSCRIPT { -0.5 } else { 0.2 }).into(); + /// The font size for synthetic sub- and superscripts. + pub const SIZE: TextSize = TextSize(Em::new(0.6).into()); + + fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("body")?).pack()) + } +} + +impl Show for ShiftNode { + fn unguard_parts(&self, _: Selector) -> Content { + Self(self.0.clone()).pack() + } + + fn field(&self, name: &str) -> Option { + match name { + "body" => Some(Value::Content(self.0.clone())), + _ => None, + } + } + + fn realize( + &self, + world: Tracked, + styles: StyleChain, + ) -> SourceResult { + let mut transformed = None; + if styles.get(Self::TYPOGRAPHIC) { + if let Some(text) = search_text(&self.0, S) { + if is_shapable(world, &text, styles) { + transformed = Some(TextNode(text).pack()); + } + } + }; + + Ok(transformed.unwrap_or_else(|| { + let mut map = StyleMap::new(); + map.set(TextNode::BASELINE, styles.get(Self::BASELINE)); + map.set(TextNode::SIZE, styles.get(Self::SIZE)); + self.0.clone().styled_with_map(map) + })) + } +} + +/// 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` leaf nodes. +fn search_text(content: &Content, mode: ScriptKind) -> Option { + if content.is_empty() { + Some(EcoString::new()) + } else if content.is::() { + Some(' '.into()) + } else if let Some(text) = content.downcast::() { + if let Some(sup) = convert_script(&text.0, mode) { + return Some(sup); + } + None + } else if let Some(seq) = content.downcast::() { + let mut full = EcoString::new(); + for item in seq.0.iter() { + match search_text(item, mode) { + 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(world: Tracked, text: &str, styles: StyleChain) -> bool { + for family in styles.get(TextNode::FAMILY).0.iter() { + 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, mode: ScriptKind) -> Option { + let mut result = EcoString::with_capacity(text.len()); + let converter = match mode { + SUPERSCRIPT => to_superscript_codepoint, + SUBSCRIPT | _ => to_subscript_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::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::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, + }) +} + +/// A category of script. +pub type ScriptKind = usize; + +/// Text that is rendered smaller and raised, also known as superior. +const SUPERSCRIPT: ScriptKind = 0; + +/// Text that is rendered smaller and lowered, also known as inferior. +const SUBSCRIPT: ScriptKind = 1; diff --git a/library/src/utility/color.rs b/library/src/utility/color.rs new file mode 100644 index 00000000..8bb12334 --- /dev/null +++ b/library/src/utility/color.rs @@ -0,0 +1,66 @@ +use std::str::FromStr; + +use crate::prelude::*; + +/// Create a grayscale color. +pub fn luma(_: &mut Vm, args: &mut Args) -> SourceResult { + let Component(luma) = args.expect("gray component")?; + Ok(Value::Color(LumaColor::new(luma).into())) +} + +/// Create an RGB(A) color. +pub fn rgb(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(Value::Color( + if let Some(string) = args.find::>()? { + 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() + }, + )) +} + +/// Create a CMYK color. +pub fn cmyk(_: &mut Vm, args: &mut Args) -> SourceResult { + let RatioComponent(c) = args.expect("cyan component")?; + let RatioComponent(m) = args.expect("magenta component")?; + let RatioComponent(y) = args.expect("yellow component")?; + let RatioComponent(k) = args.expect("key component")?; + Ok(Value::Color(CmykColor::new(c, m, y, k).into())) +} + +/// An integer or ratio component. +struct Component(u8); + +castable! { + Component, + Expected: "integer or ratio", + Value::Int(v) => match v { + 0 ..= 255 => Self(v as u8), + _ => Err("must be between 0 and 255")?, + }, + Value::Ratio(v) => if (0.0 ..= 1.0).contains(&v.get()) { + Self((v.get() * 255.0).round() as u8) + } else { + Err("must be between 0% and 100%")? + }, +} + +/// A component that must be a ratio. +struct RatioComponent(u8); + +castable! { + RatioComponent, + Expected: "ratio", + Value::Ratio(v) => if (0.0 ..= 1.0).contains(&v.get()) { + Self((v.get() * 255.0).round() as u8) + } else { + Err("must be between 0% and 100%")? + }, +} diff --git a/library/src/utility/data.rs b/library/src/utility/data.rs new file mode 100644 index 00000000..3edade55 --- /dev/null +++ b/library/src/utility/data.rs @@ -0,0 +1,132 @@ +use std::fmt::Write; + +use typst::diag::format_xml_like_error; + +use crate::prelude::*; + +/// Read structured data from a CSV file. +pub fn csv(vm: &mut Vm, args: &mut Args) -> SourceResult { + let Spanned { v: path, span } = + args.expect::>("path to csv file")?; + + let path = vm.locate(&path).at(span)?; + let data = vm.world.file(&path).at(span)?; + + let mut builder = csv::ReaderBuilder::new(); + builder.has_headers(false); + + let mut reader = builder.from_reader(data.as_slice()); + let mut vec = vec![]; + + for result in reader.records() { + let row = result.map_err(format_csv_error).at(span)?; + let array = row.iter().map(|field| Value::Str(field.into())).collect(); + vec.push(Value::Array(array)) + } + + Ok(Value::Array(Array::from_vec(vec))) +} + +/// Format the user-facing CSV error message. +fn format_csv_error(error: csv::Error) -> String { + match error.kind() { + csv::ErrorKind::Utf8 { .. } => "file is not valid utf-8".into(), + csv::ErrorKind::UnequalLengths { pos, expected_len, len } => { + let mut msg = format!( + "failed to parse csv file: found {len} instead of {expected_len} fields" + ); + if let Some(pos) = pos { + write!(msg, " in line {}", pos.line()).unwrap(); + } + msg + } + _ => "failed to parse csv file".into(), + } +} + +/// Read structured data from a JSON file. +pub fn json(vm: &mut Vm, args: &mut Args) -> SourceResult { + let Spanned { v: path, span } = + args.expect::>("path to json file")?; + + let path = vm.locate(&path).at(span)?; + let data = vm.world.file(&path).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) => Value::Bool(v), + serde_json::Value::Number(v) => match v.as_i64() { + Some(int) => Value::Int(int), + None => Value::Float(v.as_f64().unwrap_or(f64::NAN)), + }, + serde_json::Value::String(v) => Value::Str(v.into()), + serde_json::Value::Array(v) => { + Value::Array(v.into_iter().map(convert_json).collect()) + } + serde_json::Value::Object(v) => Value::Dict( + v.into_iter() + .map(|(key, value)| (key.into(), convert_json(value))) + .collect(), + ), + } +} + +/// Format the user-facing JSON error message. +fn format_json_error(error: serde_json::Error) -> String { + assert!(error.is_syntax() || error.is_eof()); + format!( + "failed to parse json file: syntax error in line {}", + error.line() + ) +} + +/// Read structured data from an XML file. +pub fn xml(vm: &mut Vm, args: &mut Args) -> SourceResult { + let Spanned { v: path, span } = + args.expect::>("path to xml file")?; + + let path = vm.locate(&path).at(span)?; + let data = vm.world.file(&path).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 Value::Str(node.text().unwrap_or_default().into()); + } + + 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() + .iter() + .map(|attr| (attr.name().into(), attr.value().into())) + .collect(); + + Value::Dict(dict! { + "tag" => tag, + "attrs" => attrs, + "children" => children, + }) +} + +/// Format the user-facing XML error message. +fn format_xml_error(error: roxmltree::Error) -> String { + format_xml_like_error("xml file", error) +} diff --git a/library/src/utility/math.rs b/library/src/utility/math.rs new file mode 100644 index 00000000..dd37e8e7 --- /dev/null +++ b/library/src/utility/math.rs @@ -0,0 +1,144 @@ +use std::cmp::Ordering; + +use crate::prelude::*; + +/// Convert a value to an integer. +pub fn int(_: &mut Vm, args: &mut Args) -> SourceResult { + let Spanned { v, span } = args.expect("value")?; + Ok(Value::Int(match v { + Value::Bool(v) => v as i64, + Value::Int(v) => v, + Value::Float(v) => v as i64, + Value::Str(v) => match v.parse() { + Ok(v) => v, + Err(_) => bail!(span, "invalid integer"), + }, + v => bail!(span, "cannot convert {} to integer", v.type_name()), + })) +} + +/// Convert a value to a float. +pub fn float(_: &mut Vm, args: &mut Args) -> SourceResult { + let Spanned { v, span } = args.expect("value")?; + Ok(Value::Float(match v { + Value::Int(v) => v as f64, + Value::Float(v) => v, + Value::Str(v) => match v.parse() { + Ok(v) => v, + Err(_) => bail!(span, "invalid float"), + }, + v => bail!(span, "cannot convert {} to float", v.type_name()), + })) +} + +/// The absolute value of a numeric value. +pub fn abs(_: &mut Vm, args: &mut Args) -> SourceResult { + let Spanned { v, span } = args.expect("numeric value")?; + Ok(match v { + Value::Int(v) => Value::Int(v.abs()), + Value::Float(v) => Value::Float(v.abs()), + Value::Angle(v) => Value::Angle(v.abs()), + Value::Ratio(v) => Value::Ratio(v.abs()), + Value::Fraction(v) => Value::Fraction(v.abs()), + Value::Length(_) | Value::Relative(_) => { + bail!(span, "cannot take absolute value of a length") + } + v => bail!(span, "expected numeric value, found {}", v.type_name()), + }) +} + +/// The minimum of a sequence of values. +pub fn min(_: &mut Vm, args: &mut Args) -> SourceResult { + minmax(args, Ordering::Less) +} + +/// The maximum of a sequence of values. +pub fn max(_: &mut Vm, args: &mut Args) -> SourceResult { + minmax(args, Ordering::Greater) +} + +/// Find the minimum or maximum of a sequence of values. +fn minmax(args: &mut Args, goal: Ordering) -> SourceResult { + let mut extremum = args.expect::("value")?; + for Spanned { v, span } in args.all::>()? { + match v.partial_cmp(&extremum) { + Some(ordering) => { + if ordering == goal { + extremum = v; + } + } + None => bail!( + span, + "cannot compare {} and {}", + extremum.type_name(), + v.type_name(), + ), + } + } + Ok(extremum) +} + +/// Whether an integer is even. +pub fn even(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(Value::Bool(args.expect::("integer")? % 2 == 0)) +} + +/// Whether an integer is odd. +pub fn odd(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(Value::Bool(args.expect::("integer")? % 2 != 0)) +} + +/// The modulo of two numbers. +pub fn mod_(_: &mut Vm, args: &mut Args) -> SourceResult { + let Spanned { v: v1, span: span1 } = args.expect("integer or float")?; + let Spanned { v: v2, span: span2 } = args.expect("integer or float")?; + + let (a, b) = match (v1, v2) { + (Value::Int(a), Value::Int(b)) => match a.checked_rem(b) { + Some(res) => return Ok(Value::Int(res)), + None => bail!(span2, "divisor must not be zero"), + }, + (Value::Int(a), Value::Float(b)) => (a as f64, b), + (Value::Float(a), Value::Int(b)) => (a, b as f64), + (Value::Float(a), Value::Float(b)) => (a, b), + (Value::Int(_), b) | (Value::Float(_), b) => bail!( + span2, + format!("expected integer or float, found {}", b.type_name()) + ), + (a, _) => bail!( + span1, + format!("expected integer or float, found {}", a.type_name()) + ), + }; + + if b == 0.0 { + bail!(span2, "divisor must not be zero"); + } + + Ok(Value::Float(a % b)) +} + +/// Create a sequence of numbers. +pub fn range(_: &mut Vm, args: &mut Args) -> SourceResult { + let first = args.expect::("end")?; + let (start, end) = match args.eat::()? { + Some(second) => (first, second), + None => (0, first), + }; + + let step: i64 = match args.named("step")? { + Some(Spanned { v: 0, span }) => bail!(span, "step must not be zero"), + Some(Spanned { v, .. }) => v, + None => 1, + }; + + let mut x = start; + let mut seq = vec![]; + + while x.cmp(&end) == 0.cmp(&step) { + seq.push(Value::Int(x)); + x += step; + } + + Ok(Value::Array(Array::from_vec(seq))) +} diff --git a/library/src/utility/mod.rs b/library/src/utility/mod.rs new file mode 100644 index 00000000..402944cd --- /dev/null +++ b/library/src/utility/mod.rs @@ -0,0 +1,54 @@ +//! Computational utility functions. + +mod color; +mod data; +mod math; +mod string; + +pub use color::*; +pub use data::*; +pub use math::*; +pub use string::*; + +use comemo::Track; +use typst::model::{Eval, Route, Scopes, Vm}; +use typst::syntax::Source; + +use crate::prelude::*; + +/// The name of a value's type. +pub fn type_(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(args.expect::("value")?.type_name().into()) +} + +/// Ensure that a condition is fulfilled. +pub fn assert(_: &mut Vm, args: &mut Args) -> SourceResult { + let Spanned { v, span } = args.expect::>("condition")?; + if !v { + bail!(span, "assertion failed"); + } + Ok(Value::None) +} + +/// Evaluate a string as Typst markup. +pub fn eval(vm: &mut Vm, args: &mut Args) -> SourceResult { + let Spanned { v: text, span } = args.expect::>("source")?; + + // Parse the source and set a synthetic span for all nodes. + let source = Source::synthesized(text, span); + let ast = source.ast()?; + + // Evaluate the source. + let std = &vm.world.config().scope; + let scopes = Scopes::new(Some(std)); + let route = Route::default(); + let mut sub = Vm::new(vm.world, route.track(), None, scopes); + let result = ast.eval(&mut sub); + + // Handle control flow. + if let Some(flow) = sub.flow { + bail!(flow.forbidden()); + } + + Ok(Value::Content(result?)) +} diff --git a/library/src/utility/string.rs b/library/src/utility/string.rs new file mode 100644 index 00000000..ed444d35 --- /dev/null +++ b/library/src/utility/string.rs @@ -0,0 +1,141 @@ +use typst::model::Regex; + +use crate::prelude::*; + +/// The string representation of a value. +pub fn repr(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(args.expect::("value")?.repr().into()) +} + +/// Convert a value to a string. +pub fn str(_: &mut Vm, args: &mut Args) -> SourceResult { + let Spanned { v, span } = args.expect("value")?; + Ok(Value::Str(match v { + Value::Int(v) => format_str!("{}", v), + Value::Float(v) => format_str!("{}", v), + Value::Str(v) => v, + v => bail!(span, "cannot convert {} to string", v.type_name()), + })) +} + +/// Create blind text. +pub fn lorem(_: &mut Vm, args: &mut Args) -> SourceResult { + let words: usize = args.expect("number of words")?; + Ok(Value::Str(lipsum::lipsum(words).into())) +} + +/// Create a regular expression. +pub fn regex(_: &mut Vm, args: &mut Args) -> SourceResult { + let Spanned { v, span } = args.expect::>("regular expression")?; + Ok(Regex::new(&v).at(span)?.into()) +} + +/// Converts an integer into one or multiple letters. +pub fn letter(_: &mut Vm, args: &mut Args) -> SourceResult { + numbered(Numbering::Letter, args) +} + +/// Converts an integer into a roman numeral. +pub fn roman(_: &mut Vm, args: &mut Args) -> SourceResult { + numbered(Numbering::Roman, args) +} + +/// Convert a number into a symbol. +pub fn symbol(_: &mut Vm, args: &mut Args) -> SourceResult { + numbered(Numbering::Symbol, args) +} + +fn numbered(numbering: Numbering, args: &mut Args) -> SourceResult { + let n = args.expect::("non-negative integer")?; + Ok(Value::Str(numbering.apply(n).into())) +} + +/// Allows to convert a number into letters, roman numerals and symbols. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Numbering { + Arabic, + Letter, + Roman, + Symbol, +} + +impl Numbering { + /// Apply the numbering to the given number. + pub fn apply(self, mut n: usize) -> EcoString { + match self { + Self::Arabic => { + format_eco!("{}", n) + } + Self::Letter => { + if n == 0 { + return '-'.into(); + } + + n -= 1; + + let mut letters = vec![]; + loop { + letters.push(b'a' + (n % 26) as u8); + n /= 26; + if n == 0 { + break; + } + } + + letters.reverse(); + String::from_utf8(letters).unwrap().into() + } + 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 ROMANS { + while n >= value { + n -= value; + fmt.push_str(name); + } + } + + fmt + } + Self::Symbol => { + if n == 0 { + return '-'.into(); + } + + let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()]; + let amount = ((n - 1) / SYMBOLS.len()) + 1; + std::iter::repeat(symbol).take(amount).collect() + } + } + } +} + +const ROMANS: &[(&str, usize)] = &[ + ("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), +]; + +const SYMBOLS: &[char] = &['*', '†', '‡', '§', '‖', '¶']; diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 823dbdc8..d3b3b30a 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -15,7 +15,7 @@ pub fn capability(_: TokenStream, item: TokenStream) -> TokenStream { let name = &item_trait.ident; quote! { #item_trait - impl crate::model::Capability for dyn #name {} + impl ::typst::model::Capability for dyn #name {} }.into() } @@ -70,7 +70,7 @@ fn expand_node( fn construct( _: &mut model::Vm, _: &mut model::Args, - ) -> crate::diag::SourceResult { + ) -> typst::diag::SourceResult { unimplemented!() } } @@ -84,7 +84,7 @@ fn expand_node( let checks = items.iter().map(|cap| { quote! { if id == TypeId::of::() { - return Some(unsafe { crate::util::fat::vtable(self as &dyn #cap) }); + return Some(unsafe { typst::util::fat::vtable(self as &dyn #cap) }); } } }); @@ -101,10 +101,10 @@ fn expand_node( Ok(quote! { #[allow(non_snake_case)] mod #module { - use std::any::TypeId; - use std::marker::PhantomData; - use once_cell::sync::Lazy; - use crate::model; + use ::std::any::TypeId; + use ::std::marker::PhantomData; + use ::once_cell::sync::Lazy; + use ::typst::model; use super::*; #impl_block @@ -370,7 +370,7 @@ fn generate_set( ) -> syn::ImplItemMethod { let user = user.map(|method| { let block = &method.block; - quote! { (|| -> crate::diag::SourceResult<()> { #block; Ok(()) } )()?; } + quote! { (|| -> typst::diag::SourceResult<()> { #block; Ok(()) } )()?; } }); let mut shorthands = vec![]; @@ -379,7 +379,7 @@ fn generate_set( .filter(|p| !p.skip) .map(|property| { let name = &property.name; - let string = name.to_string().replace("_", "-").to_lowercase(); + let string = name.to_string().replace('_', "-").to_lowercase(); let value = if let Some(short) = &property.shorthand { match short { @@ -409,7 +409,7 @@ fn generate_set( fn set( args: &mut model::Args, constructor: bool, - ) -> crate::diag::SourceResult { + ) -> typst::diag::SourceResult { let mut styles = model::StyleMap::new(); #user #(#bindings)* diff --git a/src/diag.rs b/src/diag.rs index f4725f00..0a532713 100644 --- a/src/diag.rs +++ b/src/diag.rs @@ -10,33 +10,41 @@ use std::string::FromUtf8Error; use comemo::Tracked; use crate::syntax::{ErrorPos, Span, Spanned}; -use crate::util::EcoString; +use crate::util::{format_eco, EcoString}; use crate::World; /// Early-return with a [`SourceError`]. #[macro_export] -macro_rules! bail { +#[doc(hidden)] +macro_rules! __bail { ($error:expr) => { return Err(Box::new(vec![$error])) }; ($($tts:tt)*) => { - $crate::bail!($crate::error!($($tts)*)) + $crate::diag::bail!($crate::diag::error!($($tts)*)) }; } +#[doc(inline)] +pub use crate::__bail as bail; + /// Construct a [`SourceError`]. #[macro_export] -macro_rules! error { +#[doc(hidden)] +macro_rules! __error { ($span:expr, $message:expr $(,)?) => { $crate::diag::SourceError::new($span, $message) }; ($span:expr, $fmt:expr, $($arg:expr),+ $(,)?) => { - $crate::error!($span, format!($fmt, $($arg),+)) + $crate::diag::error!($span, format!($fmt, $($arg),+)) }; } +#[doc(inline)] +pub use crate::__error as error; + /// A result that can carry multiple source errors. pub type SourceResult = Result>>; diff --git a/src/export/pdf/font.rs b/src/export/pdf/font.rs index 3398478a..075e3490 100644 --- a/src/export/pdf/font.rs +++ b/src/export/pdf/font.rs @@ -5,7 +5,7 @@ use pdf_writer::{Filter, Finish, Name, Rect, Str}; use ttf_parser::{name_id, GlyphId, Tag}; use super::{deflate, EmExt, PdfContext, RefExt}; -use crate::util::SliceExt; +use crate::util::{format_eco, SliceExt}; /// Embed all used fonts into the PDF. pub fn write_fonts(ctx: &mut PdfContext) { diff --git a/src/export/pdf/outline.rs b/src/export/pdf/outline.rs index 73dd9e96..add167b4 100644 --- a/src/export/pdf/outline.rs +++ b/src/export/pdf/outline.rs @@ -71,7 +71,7 @@ pub fn write_outline_item( let current_child = Ref::new(id.get() + 1); outline.first(current_child); outline.last(Ref::new(next_ref.get() - 1)); - outline.count(-1 * node.children.len() as i32); + outline.count(-(node.children.len() as i32)); } outline.title(TextStr(&node.heading.content)); diff --git a/src/export/pdf/page.rs b/src/export/pdf/page.rs index 7fbf7833..3167989c 100644 --- a/src/export/pdf/page.rs +++ b/src/export/pdf/page.rs @@ -12,6 +12,7 @@ use crate::geom::{ Transform, }; use crate::image::Image; +use crate::util::format_eco; /// Construct page objects. pub fn construct_pages(ctx: &mut PdfContext, frames: &[Frame]) { diff --git a/src/export/render.rs b/src/export/render.rs index df8f512f..41fff863 100644 --- a/src/export/render.rs +++ b/src/export/render.rs @@ -312,8 +312,10 @@ fn render_shape( if let Some(Stroke { paint, thickness }) = shape.stroke { let paint = paint.into(); - let mut stroke = sk::Stroke::default(); - stroke.width = thickness.to_f32(); + let stroke = sk::Stroke { + width: thickness.to_f32(), + ..Default::default() + }; canvas.stroke_path(&path, &paint, &stroke, ts, mask); } @@ -364,14 +366,16 @@ fn render_image( let scale_x = view_width / pixmap.width() as f32; let scale_y = view_height / pixmap.height() as f32; - let mut paint = sk::Paint::default(); - paint.shader = sk::Pattern::new( - pixmap.as_ref(), - sk::SpreadMode::Pad, - sk::FilterQuality::Nearest, - 1.0, - sk::Transform::from_scale(scale_x, scale_y), - ); + let paint = sk::Paint { + shader: sk::Pattern::new( + pixmap.as_ref(), + sk::SpreadMode::Pad, + sk::FilterQuality::Nearest, + 1.0, + sk::Transform::from_scale(scale_x, 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); diff --git a/src/font/book.rs b/src/font/book.rs index d900d3c4..49225c04 100644 --- a/src/font/book.rs +++ b/src/font/book.rs @@ -170,7 +170,7 @@ bitflags::bitflags! { impl FontInfo { /// Compute metadata for all fonts in the given data. - pub fn from_data<'a>(data: &'a [u8]) -> impl Iterator + 'a { + pub fn from_data(data: &[u8]) -> impl Iterator + '_ { let count = ttf_parser::fonts_in_collection(data).unwrap_or(1); (0 .. count).filter_map(move |index| { let ttf = ttf_parser::Face::parse(data, index).ok()?; diff --git a/src/font/variant.rs b/src/font/variant.rs index 9e16afc8..085f2ae7 100644 --- a/src/font/variant.rs +++ b/src/font/variant.rs @@ -111,7 +111,7 @@ impl FontWeight { /// 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).abs() as u16 + (self.0 as i16 - other.0 as i16).unsigned_abs() } } diff --git a/src/frame.rs b/src/frame.rs index e85256f5..2eac6879 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -2,6 +2,7 @@ use std::fmt::{self, Debug, Formatter, Write}; use std::num::NonZeroUsize; +use std::str::FromStr; use std::sync::Arc; use crate::font::Font; @@ -9,7 +10,7 @@ use crate::geom::{ Abs, Align, Axes, Dir, Em, Numeric, Paint, Point, Shape, Size, Transform, }; use crate::image::Image; -use crate::model::{Dict, Value}; +use crate::model::{dict, Dict, Value}; use crate::util::EcoString; /// A finished layout with elements at fixed positions. @@ -396,7 +397,7 @@ pub struct Glyph { pub c: char, } -/// A code for a natural language. +/// An identifier for a natural language. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Lang([u8; 3], u8); @@ -404,19 +405,6 @@ impl Lang { /// The code for the english language. pub const ENGLISH: Self = Self(*b"en ", 2); - /// Construct a language from a two- or three-byte ISO 639-1/2/3 code. - pub fn from_str(iso: &str) -> Option { - 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(); - Some(Self(bytes, len as u8)) - } else { - None - } - } - /// 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() @@ -432,28 +420,49 @@ impl Lang { } } -/// A code for a region somewhere in the world. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct Region([u8; 2]); +impl FromStr for Lang { + type Err = &'static str; -impl Region { - /// Construct a region from its two-byte ISO 3166-1 alpha-2 code. - pub fn from_str(iso: &str) -> Option { - if iso.is_ascii() { - let mut bytes: [u8; 2] = iso.as_bytes().try_into().ok()?; - bytes.make_ascii_uppercase(); - Some(Self(bytes)) + /// Construct a language from a two- or three-byte ISO 639-1/2/3 code. + fn from_str(iso: &str) -> Result { + let len = iso.len(); + if matches!(len, 2 ..= 3) && iso.is_ascii() { + let mut bytes = [b' '; 3]; + bytes[.. len].copy_from_slice(iso.as_bytes()); + bytes.make_ascii_lowercase(); + Ok(Self(bytes, len as u8)) } else { - None + Err("expected two or three letter language code (ISO 639-1/2/3)") } } +} +/// 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 FromStr for Region { + type Err = &'static str; + + /// Construct a region from its two-byte ISO 3166-1 alpha-2 code. + fn from_str(iso: &str) -> Result { + if iso.len() == 2 && iso.is_ascii() { + let mut bytes: [u8; 2] = iso.as_bytes().try_into().unwrap(); + bytes.make_ascii_uppercase(); + Ok(Self(bytes)) + } else { + Err("expected two letter region code (ISO 3166-1 alpha-2)") + } + } +} + /// A link destination. #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum Destination { @@ -529,9 +538,9 @@ impl Role { pub fn is_weak(self) -> bool { // In Typst, all text is in a paragraph, so paragraph isn't very // descriptive. - match self { - Self::Paragraph | Self::GenericBlock | Self::GenericInline => true, - _ => false, - } + matches!( + self, + Self::Paragraph | Self::GenericBlock | Self::GenericInline + ) } } diff --git a/src/geom/align.rs b/src/geom/align.rs index a7ee2763..25409e3a 100644 --- a/src/geom/align.rs +++ b/src/geom/align.rs @@ -78,3 +78,40 @@ impl Debug for Align { }) } } + +/// The generic alignment representation. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum GenAlign { + /// Align at the start side of the text direction. + Start, + /// Align at the end side of the text direction. + End, + /// Align at a specific alignment. + Specific(Align), +} + +impl GenAlign { + /// The axis this alignment belongs to. + pub const fn axis(self) -> Axis { + match self { + Self::Start | Self::End => Axis::X, + Self::Specific(align) => align.axis(), + } + } +} + +impl From for GenAlign { + fn from(align: Align) -> Self { + Self::Specific(align) + } +} + +impl 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), + } + } +} diff --git a/src/geom/angle.rs b/src/geom/angle.rs index 33a864ca..8e80d72b 100644 --- a/src/geom/angle.rs +++ b/src/geom/angle.rs @@ -178,6 +178,6 @@ mod tests { #[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() - 0.7854) < 1e-4); + assert!((Angle::deg(45.0).to_rad() - std::f64::consts::FRAC_PI_4) < 1e-4); } } diff --git a/src/geom/mod.rs b/src/geom/mod.rs index 6e2b8f9b..c1469b26 100644 --- a/src/geom/mod.rs +++ b/src/geom/mod.rs @@ -21,6 +21,7 @@ mod rounded; mod scalar; mod sides; mod size; +mod stroke; mod transform; pub use abs::*; @@ -42,6 +43,7 @@ pub use rounded::*; pub use scalar::*; pub use sides::*; pub use size::*; +pub use stroke::*; pub use transform::*; use std::cmp::Ordering; diff --git a/src/geom/paint.rs b/src/geom/paint.rs index c5bbefa4..b07f09af 100644 --- a/src/geom/paint.rs +++ b/src/geom/paint.rs @@ -388,24 +388,6 @@ impl From for Color { } } -/// A stroke of a geometric shape. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Stroke { - /// The stroke's paint. - pub paint: Paint, - /// The stroke's thickness. - pub thickness: Abs, -} - -impl Default for Stroke { - fn default() -> Self { - Self { - paint: Paint::Solid(Color::BLACK.into()), - thickness: Abs::pt(1.0), - } - } -} - /// Convert to the closest u8. fn round_u8(value: f64) -> u8 { value.round() as u8 diff --git a/src/geom/stroke.rs b/src/geom/stroke.rs new file mode 100644 index 00000000..eae43c24 --- /dev/null +++ b/src/geom/stroke.rs @@ -0,0 +1,61 @@ +use super::*; +use crate::model::Smart; + +/// A stroke of a geometric shape. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Stroke { + /// The stroke's paint. + pub paint: Paint, + /// The stroke's thickness. + pub thickness: Abs, +} + +impl Default for Stroke { + fn default() -> Self { + Self { + paint: Paint::Solid(Color::BLACK), + thickness: Abs::pt(1.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, Copy, Clone, Eq, PartialEq, Hash)] +pub struct PartialStroke { + /// The stroke's paint. + pub paint: Smart, + /// The stroke's thickness. + pub thickness: Smart, +} + +impl PartialStroke { + /// Unpack the stroke, filling missing fields from the `default`. + pub fn unwrap_or(self, default: Stroke) -> Stroke { + Stroke { + paint: self.paint.unwrap_or(default.paint), + thickness: self.thickness.unwrap_or(default.thickness), + } + } + + /// Unpack the stroke, filling missing fields with the default values. + pub fn unwrap_or_default(self) -> Stroke { + self.unwrap_or(Stroke::default()) + } +} + +impl Debug for PartialStroke { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + 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(""), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 051a58f3..dbef46e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,9 +26,7 @@ //! [content]: model::Content //! [PDF]: export::pdf -#![allow(clippy::len_without_is_empty)] -#![allow(clippy::or_fun_call)] -#![allow(clippy::try_err)] +extern crate self as typst; #[macro_use] pub mod util; @@ -42,10 +40,8 @@ pub mod export; pub mod font; pub mod frame; pub mod image; -pub mod library; pub mod syntax; -use std::num::NonZeroUsize; use std::path::{Path, PathBuf}; use comemo::{Prehashed, Track}; @@ -53,9 +49,9 @@ use comemo::{Prehashed, Track}; use crate::diag::{FileResult, SourceResult}; use crate::font::{Font, FontBook}; use crate::frame::Frame; -use crate::model::{Content, Route, Scope, StyleMap}; +use crate::model::{LangItems, Route, Scope, StyleMap}; use crate::syntax::{Source, SourceId}; -use crate::util::{Buffer, EcoString}; +use crate::util::Buffer; /// Typeset a source file into a collection of layouted frames. /// @@ -66,9 +62,10 @@ pub fn typeset( world: &(dyn World + 'static), main: SourceId, ) -> SourceResult> { + crate::model::set_lang_items(world.config().items); let route = Route::default(); let module = model::eval(world.track(), route.track(), main)?; - library::layout::Layout::layout(&module.content, world.track()) + item!(root)(world.track(), &module.content) } /// The environment in which typesetting occurs. @@ -97,49 +94,11 @@ pub trait World { #[derive(Debug, Clone, Hash)] pub struct Config { /// The compilation root, relative to which absolute paths are. - /// - /// Default: Empty path. pub root: PathBuf, /// The scope containing definitions that are available everywhere. - /// - /// Default: Typst's standard library. - pub std: Scope, - /// Defines which standard library items fulfill which syntactical roles. - /// - /// Default: Typst's standard library's language map. - pub items: LangItems, + pub scope: Scope, /// The default properties for page size, font selection and so on. - /// - /// Default: Empty style map. pub styles: StyleMap, -} - -impl Default for Config { - fn default() -> Self { - Self { - root: PathBuf::new(), - std: library::scope(), - items: library::items(), - styles: StyleMap::new(), - } - } -} - -/// Definition of certain standard library items the language is aware of. -#[derive(Debug, Clone, Hash)] -pub struct LangItems { - pub space: fn() -> Content, - pub linebreak: fn(justify: bool) -> Content, - pub text: fn(text: EcoString) -> Content, - pub smart_quote: fn(double: bool) -> Content, - pub parbreak: fn() -> Content, - pub strong: fn(body: Content) -> Content, - pub emph: fn(body: Content) -> Content, - pub raw: fn(text: EcoString, tag: Option, block: bool) -> Content, - pub link: fn(label: EcoString) -> Content, - pub ref_: fn(target: EcoString) -> Content, - pub heading: fn(level: NonZeroUsize, body: Content) -> Content, - pub list_item: fn(body: Content) -> Content, - pub enum_item: fn(number: Option, body: Content) -> Content, - pub desc_item: fn(term: Content, body: Content) -> Content, + /// Defines which standard library items fulfill which syntactical roles. + pub items: LangItems, } diff --git a/src/library/ext.rs b/src/library/ext.rs deleted file mode 100644 index 07b55a7b..00000000 --- a/src/library/ext.rs +++ /dev/null @@ -1,179 +0,0 @@ -use super::*; -use crate::library::prelude::*; - -/// 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; - - /// Add weak vertical spacing above and below the content. - fn spaced(self, above: Option, below: Option) -> Self; - - /// Force a size for this content. - fn boxed(self, sizing: Axes>>) -> Self; - - /// Set alignments for this content. - fn aligned(self, aligns: Axes>) -> Self; - - /// Pad this content at the sides. - fn padded(self, padding: Sides>) -> Self; - - /// Transform this content's contents without affecting layout. - fn moved(self, delta: Axes>) -> Self; - - /// Fill the frames resulting from a content. - fn filled(self, fill: Paint) -> Self; - - /// Stroke the frames resulting from a content. - fn stroked(self, stroke: Stroke) -> Self; -} - -impl ContentExt for Content { - fn strong(self) -> Self { - text::StrongNode(self).pack() - } - - fn emph(self) -> Self { - text::EmphNode(self).pack() - } - - fn underlined(self) -> Self { - text::DecoNode::<{ text::UNDERLINE }>(self).pack() - } - - fn spaced(self, above: Option, below: Option) -> Self { - if above.is_none() && below.is_none() { - return self; - } - - let mut seq = vec![]; - if let Some(above) = above { - seq.push( - layout::VNode { - amount: above.into(), - weak: true, - generated: true, - } - .pack(), - ); - } - - seq.push(self); - if let Some(below) = below { - seq.push( - layout::VNode { - amount: below.into(), - weak: true, - generated: true, - } - .pack(), - ); - } - - Content::sequence(seq) - } - - fn boxed(self, sizing: Axes>>) -> Self { - layout::BoxNode { sizing, child: self }.pack() - } - - fn aligned(self, aligns: Axes>) -> Self { - layout::AlignNode { aligns, child: self }.pack() - } - - fn padded(self, padding: Sides>) -> Self { - layout::PadNode { padding, child: self }.pack() - } - - fn moved(self, delta: Axes>) -> Self { - layout::MoveNode { delta, child: self }.pack() - } - - fn filled(self, fill: Paint) -> Self { - FillNode { fill, child: self }.pack() - } - - fn stroked(self, stroke: Stroke) -> Self { - StrokeNode { stroke, child: self }.pack() - } -} - -/// Additional methods for the style chain. -pub trait StyleMapExt { - /// Set a font family composed of a preferred family and existing families - /// from a style chain. - fn set_family(&mut self, preferred: text::FontFamily, existing: StyleChain); -} - -impl StyleMapExt for StyleMap { - fn set_family(&mut self, preferred: text::FontFamily, existing: StyleChain) { - self.set( - text::TextNode::FAMILY, - std::iter::once(preferred) - .chain(existing.get(text::TextNode::FAMILY).iter().cloned()) - .collect(), - ); - } -} - -/// Fill the frames resulting from content. -#[derive(Debug, Hash)] -struct FillNode { - /// How to fill the frames resulting from the `child`. - fill: Paint, - /// The content whose frames should be filled. - child: Content, -} - -#[node(LayoutBlock)] -impl FillNode {} - -impl LayoutBlock for FillNode { - fn layout_block( - &self, - world: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult> { - let mut frames = self.child.layout_block(world, regions, styles)?; - for frame in &mut frames { - let shape = Geometry::Rect(frame.size()).filled(self.fill); - frame.prepend(Point::zero(), Element::Shape(shape)); - } - Ok(frames) - } -} - -/// Stroke the frames resulting from content. -#[derive(Debug, Hash)] -struct StrokeNode { - /// How to stroke the frames resulting from the `child`. - stroke: Stroke, - /// The content whose frames should be stroked. - child: Content, -} - -#[node(LayoutBlock)] -impl StrokeNode {} - -impl LayoutBlock for StrokeNode { - fn layout_block( - &self, - world: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult> { - let mut frames = self.child.layout_block(world, regions, styles)?; - for frame in &mut frames { - let shape = Geometry::Rect(frame.size()).stroked(self.stroke); - frame.prepend(Point::zero(), Element::Shape(shape)); - } - Ok(frames) - } -} diff --git a/src/library/graphics/hide.rs b/src/library/graphics/hide.rs deleted file mode 100644 index d320b06c..00000000 --- a/src/library/graphics/hide.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::library::prelude::*; - -/// Hide content without affecting layout. -#[derive(Debug, Hash)] -pub struct HideNode(pub Content); - -#[node(LayoutInline)] -impl HideNode { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - Ok(Self(args.expect("body")?).pack()) - } -} - -impl LayoutInline for HideNode { - fn layout_inline( - &self, - world: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult> { - let mut frames = self.0.layout_inline(world, regions, styles)?; - for frame in &mut frames { - frame.clear(); - } - Ok(frames) - } -} diff --git a/src/library/graphics/image.rs b/src/library/graphics/image.rs deleted file mode 100644 index e27ea488..00000000 --- a/src/library/graphics/image.rs +++ /dev/null @@ -1,120 +0,0 @@ -use std::ffi::OsStr; - -use crate::image::{Image, ImageFormat, RasterFormat, VectorFormat}; -use crate::library::prelude::*; -use crate::library::text::TextNode; - -/// Show a raster or vector graphic. -#[derive(Debug, Hash)] -pub struct ImageNode(pub Image); - -#[node(LayoutInline)] -impl ImageNode { - /// How the image should adjust itself to a given area. - pub const FIT: ImageFit = ImageFit::Cover; - - fn construct(vm: &mut Vm, args: &mut Args) -> SourceResult { - let Spanned { v: path, span } = - args.expect::>("path to image file")?; - - let full = vm.locate(&path).at(span)?; - let buffer = vm.world.file(&full).at(span)?; - let ext = full.extension().and_then(OsStr::to_str).unwrap_or_default(); - let format = match ext.to_lowercase().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!(span, "unknown image format"), - }; - - let image = Image::new(buffer, format).at(span)?; - let width = args.named("width")?; - let height = args.named("height")?; - - Ok(ImageNode(image).pack().boxed(Axes::new(width, height))) - } -} - -impl LayoutInline for ImageNode { - fn layout_inline( - &self, - _: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult> { - let pxw = self.0.width() as f64; - let pxh = self.0.height() as f64; - let px_ratio = pxw / pxh; - - // Find out whether the image is wider or taller than the target size. - let &Regions { first, expand, .. } = regions; - let region_ratio = first.x / first.y; - 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 { - first - } else if expand.x || (!expand.y && wide && first.x.is_finite()) { - Size::new(first.x, first.y.min(first.x.safe_div(px_ratio))) - } else if first.y.is_finite() { - Size::new(first.x.min(first.y * px_ratio), first.y) - } else { - Size::new(Abs::pt(pxw), Abs::pt(pxh)) - }; - - // Compute the actual size of the fitted image. - let fit = styles.get(Self::FIT); - 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(), Element::Image(self.0.clone(), fitted)); - 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 link if it exists. - if let Some(url) = styles.get(TextNode::LINK) { - frame.link(url.clone()); - } - - Ok(vec![frame]) - } -} - -/// How an image should adjust itself to a given area. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum ImageFit { - /// The image should completely cover the area. - Cover, - /// The image should be fully contained in the area. - Contain, - /// The image should be stretched so that it exactly fills the area. - Stretch, -} - -castable! { - ImageFit, - Expected: "string", - Value::Str(string) => match string.as_str() { - "cover" => Self::Cover, - "contain" => Self::Contain, - "stretch" => Self::Stretch, - _ => Err(r#"expected "cover", "contain" or "stretch""#)?, - }, -} diff --git a/src/library/graphics/line.rs b/src/library/graphics/line.rs deleted file mode 100644 index ee7813a5..00000000 --- a/src/library/graphics/line.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::library::prelude::*; - -/// Display a line without affecting the layout. -#[derive(Debug, Hash)] -pub struct LineNode { - /// Where the line starts. - origin: Axes>, - /// The offset from the `origin` where the line ends. - delta: Axes>, -} - -#[node(LayoutInline)] -impl LineNode { - /// How to stroke the line. - #[property(resolve, fold)] - pub const STROKE: RawStroke = RawStroke::default(); - - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - let origin = args.named("origin")?.unwrap_or_default(); - - let delta = match args.named::>>("to")? { - Some(to) => to.zip(origin).map(|(to, from)| to - from), - None => { - let length = - args.named::>("length")?.unwrap_or(Abs::cm(1.0).into()); - - let angle = args.named::("angle")?.unwrap_or_default(); - let x = angle.cos() * length; - let y = angle.sin() * length; - - Axes::new(x, y) - } - }; - - Ok(Self { origin, delta }.pack()) - } -} - -impl LayoutInline for LineNode { - fn layout_inline( - &self, - _: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult> { - let stroke = styles.get(Self::STROKE).unwrap_or_default(); - - let origin = self - .origin - .resolve(styles) - .zip(regions.base) - .map(|(l, b)| l.relative_to(b)); - - let delta = self - .delta - .resolve(styles) - .zip(regions.base) - .map(|(l, b)| l.relative_to(b)); - - let target = regions.expand.select(regions.first, Size::zero()); - let mut frame = Frame::new(target); - - let shape = Geometry::Line(delta.to_point()).stroked(stroke); - frame.push(origin.to_point(), Element::Shape(shape)); - - Ok(vec![frame]) - } -} - -castable! { - Axes>, - Expected: "array of two relative lengths", - 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()?), - _ => Err("point array must contain exactly two entries")?, - } - }, -} diff --git a/src/library/graphics/mod.rs b/src/library/graphics/mod.rs deleted file mode 100644 index 34182121..00000000 --- a/src/library/graphics/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Graphical elements and effects. - -mod hide; -mod image; -mod line; -mod shape; - -pub use self::image::*; -pub use hide::*; -pub use line::*; -pub use shape::*; diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs deleted file mode 100644 index 4804cd68..00000000 --- a/src/library/graphics/shape.rs +++ /dev/null @@ -1,201 +0,0 @@ -use std::f64::consts::SQRT_2; - -use crate::library::prelude::*; -use crate::library::text::TextNode; - -/// A sizable and fillable shape with optional content. -#[derive(Debug, Hash)] -pub struct ShapeNode(pub Option); - -/// A square with optional content. -pub type SquareNode = ShapeNode; - -/// A rectangle with optional content. -pub type RectNode = ShapeNode; - -/// A circle with optional content. -pub type CircleNode = ShapeNode; - -/// A ellipse with optional content. -pub type EllipseNode = ShapeNode; - -#[node(LayoutInline)] -impl ShapeNode { - /// How to fill the shape. - pub const FILL: Option = None; - /// How to stroke the shape. - #[property(skip, resolve, fold)] - pub const STROKE: Smart>> = Smart::Auto; - - /// How much to pad the shape's content. - #[property(resolve, fold)] - pub const INSET: Sides>> = Sides::splat(Rel::zero()); - /// How much to extend the shape's dimensions beyond the allocated space. - #[property(resolve, fold)] - pub const OUTSET: Sides>> = Sides::splat(Rel::zero()); - - /// How much to round the shape's corners. - #[property(skip, resolve, fold)] - pub const RADIUS: Corners>> = Corners::splat(Rel::zero()); - - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - let size = match S { - SQUARE => args.named::("size")?.map(Rel::from), - CIRCLE => args.named::("radius")?.map(|r| 2.0 * Rel::from(r)), - _ => None, - }; - - let width = match size { - None => args.named("width")?, - size => size, - }; - - let height = match size { - None => args.named("height")?, - size => size, - }; - - Ok(Self(args.eat()?).pack().boxed(Axes::new(width, height))) - } - - fn set(...) { - if is_round(S) { - styles.set_opt( - Self::STROKE, - args.named::>>("stroke")? - .map(|some| some.map(Sides::splat)), - ); - } else { - styles.set_opt(Self::STROKE, args.named("stroke")?); - styles.set_opt(Self::RADIUS, args.named("radius")?); - } - } -} - -impl LayoutInline for ShapeNode { - fn layout_inline( - &self, - world: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult> { - let mut frames; - if let Some(child) = &self.0 { - let mut inset = styles.get(Self::INSET); - if is_round(S) { - 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 mut pod = Regions::one(regions.first, regions.base, regions.expand); - frames = child.layout_inline(world, &pod, styles)?; - - for frame in frames.iter_mut() { - frame.apply_role(Role::GenericBlock); - } - - // Relayout with full expansion into square region to make sure - // the result is really a square or circle. - if is_quadratic(S) { - let length = if regions.expand.x || regions.expand.y { - let target = regions.expand.select(regions.first, Size::zero()); - target.x.max(target.y) - } else { - let size = frames[0].size(); - let desired = size.x.max(size.y); - desired.min(regions.first.x).min(regions.first.y) - }; - - pod.first = Size::splat(length); - pod.expand = Axes::splat(true); - frames = child.layout_inline(world, &pod, styles)?; - } - } else { - // The default size that a shape takes on if it has no child and - // enough space. - let mut size = Size::new(Abs::pt(45.0), Abs::pt(30.0)).min(regions.first); - - if is_quadratic(S) { - let length = if regions.expand.x || regions.expand.y { - let target = regions.expand.select(regions.first, Size::zero()); - target.x.max(target.y) - } else { - size.x.min(size.y) - }; - size = Size::splat(length); - } else { - size = regions.expand.select(regions.first, size); - } - - frames = vec![Frame::new(size)]; - } - - let frame = &mut frames[0]; - - // Add fill and/or stroke. - let fill = styles.get(Self::FILL); - let stroke = match styles.get(Self::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(RawStroke::unwrap_or_default)) - } - }; - - let outset = styles.get(Self::OUTSET).relative_to(frame.size()); - let size = frame.size() + outset.sum_by_axis(); - - let radius = styles - .get(Self::RADIUS) - .map(|side| side.relative_to(size.x.min(size.y) / 2.0)); - - let pos = Point::new(-outset.left, -outset.top); - - if fill.is_some() || stroke.iter().any(Option::is_some) { - if is_round(S) { - let shape = ellipse(size, fill, stroke.left); - frame.prepend(pos, Element::Shape(shape)); - } else { - frame.prepend_multiple( - rounded_rect(size, radius, fill, stroke) - .into_iter() - .map(|x| (pos, Element::Shape(x))), - ) - } - } - - // Apply link if it exists. - if let Some(url) = styles.get(TextNode::LINK) { - frame.link(url.clone()); - } - - Ok(frames) - } -} - -/// A category of shape. -pub type ShapeKind = usize; - -/// A rectangle with equal side lengths. -const SQUARE: ShapeKind = 0; - -/// A quadrilateral with four right angles. -const RECT: ShapeKind = 1; - -/// An ellipse with coinciding foci. -const CIRCLE: ShapeKind = 2; - -/// A curve around two focal points. -const ELLIPSE: ShapeKind = 3; - -/// Whether a shape kind is curvy. -fn is_round(kind: ShapeKind) -> bool { - matches!(kind, CIRCLE | ELLIPSE) -} - -/// Whether a shape kind has equal side length. -fn is_quadratic(kind: ShapeKind) -> bool { - matches!(kind, SQUARE | CIRCLE) -} diff --git a/src/library/layout/align.rs b/src/library/layout/align.rs deleted file mode 100644 index 2ee565cc..00000000 --- a/src/library/layout/align.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::library::prelude::*; -use crate::library::text::{HorizontalAlign, ParNode}; - -/// Align content along the layouting axes. -#[derive(Debug, Hash)] -pub struct AlignNode { - /// How to align the content horizontally and vertically. - pub aligns: Axes>, - /// The content to be aligned. - pub child: Content, -} - -#[node(LayoutBlock)] -impl AlignNode { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - let aligns: Axes> = args.find()?.unwrap_or_default(); - let body: Content = args.expect("body")?; - - if let Axes { x: Some(x), y: None } = aligns { - if !body.has::() { - return Ok(body.styled(ParNode::ALIGN, HorizontalAlign(x))); - } - } - - Ok(body.aligned(aligns)) - } -} - -impl LayoutBlock for AlignNode { - fn layout_block( - &self, - world: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult> { - // The child only needs to expand along an axis if there's no alignment. - let mut pod = regions.clone(); - pod.expand &= self.aligns.as_ref().map(Option::is_none); - - // Align paragraphs inside the child. - let mut passed = StyleMap::new(); - if let Some(align) = self.aligns.x { - passed.set(ParNode::ALIGN, HorizontalAlign(align)); - } - - // Layout the child. - let mut frames = self.child.layout_block(world, &pod, passed.chain(&styles))?; - for (region, frame) in regions.iter().zip(&mut frames) { - // Align in the target size. The target size depends on whether we - // should expand. - let target = regions.expand.select(region, frame.size()); - let aligns = self - .aligns - .map(|align| align.resolve(styles)) - .unwrap_or(Axes::new(Align::Left, Align::Top)); - - frame.resize(target, aligns); - } - - Ok(frames) - } -} diff --git a/src/library/layout/columns.rs b/src/library/layout/columns.rs deleted file mode 100644 index df259eab..00000000 --- a/src/library/layout/columns.rs +++ /dev/null @@ -1,117 +0,0 @@ -use crate::library::prelude::*; -use crate::library::text::TextNode; - -/// Separate a region into multiple equally sized columns. -#[derive(Debug, Hash)] -pub struct ColumnsNode { - /// How many columns there should be. - pub columns: NonZeroUsize, - /// The child to be layouted into the columns. Most likely, this should be a - /// flow or stack node. - pub child: Content, -} - -#[node(LayoutBlock)] -impl ColumnsNode { - /// The size of the gutter space between each column. - #[property(resolve)] - pub const GUTTER: Rel = Ratio::new(0.04).into(); - - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - Ok(Self { - columns: args.expect("column count")?, - child: args.expect("body")?, - } - .pack()) - } -} - -impl LayoutBlock for ColumnsNode { - fn layout_block( - &self, - world: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult> { - // Separating the infinite space into infinite columns does not make - // much sense. - if !regions.first.x.is_finite() { - return self.child.layout_block(world, regions, styles); - } - - // Determine the width of the gutter and each column. - let columns = self.columns.get(); - let gutter = styles.get(Self::GUTTER).relative_to(regions.base.x); - let width = (regions.first.x - gutter * (columns - 1) as f64) / columns as f64; - - // Create the pod regions. - let pod = Regions { - first: Size::new(width, regions.first.y), - base: Size::new(width, regions.base.y), - backlog: std::iter::once(®ions.first.y) - .chain(regions.backlog.as_slice()) - .flat_map(|&height| std::iter::repeat(height).take(columns)) - .skip(1) - .collect(), - last: regions.last, - expand: Axes::new(true, regions.expand.y), - }; - - // Layout the children. - let mut frames = self.child.layout_block(world, &pod, styles)?.into_iter(); - let mut finished = vec![]; - - let dir = styles.get(TextNode::DIR); - 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.first.x, height)); - let mut cursor = Abs::zero(); - - for _ in 0 .. columns { - let frame = match frames.next() { - Some(frame) => frame, - None => break, - }; - - if !regions.expand.y { - output.size_mut().y.set_max(frame.height()); - } - - let width = frame.width(); - let x = if dir.is_positive() { - cursor - } else { - regions.first.x - cursor - width - }; - - output.push_frame(Point::with_x(x), frame); - cursor += width + gutter; - } - - finished.push(output); - } - - Ok(finished) - } -} - -/// A column break. -#[derive(Debug, Clone, Hash)] -pub struct ColbreakNode { - pub weak: bool, -} - -#[node] -impl ColbreakNode { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - let weak = args.named("weak")?.unwrap_or(false); - Ok(Self { weak }.pack()) - } -} diff --git a/src/library/layout/container.rs b/src/library/layout/container.rs deleted file mode 100644 index 023809d0..00000000 --- a/src/library/layout/container.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::library::prelude::*; - -/// An inline-level container that sizes content. -#[derive(Debug, Clone, Hash)] -pub struct BoxNode { - /// How to size the content horizontally and vertically. - pub sizing: Axes>>, - /// The content to be sized. - pub child: Content, -} - -#[node(LayoutInline)] -impl BoxNode { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - let width = args.named("width")?; - let height = args.named("height")?; - let body = args.eat::()?.unwrap_or_default(); - Ok(body.boxed(Axes::new(width, height))) - } -} - -impl LayoutInline for BoxNode { - fn layout_inline( - &self, - world: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult> { - // The "pod" is the region into which the child will be layouted. - let pod = { - // Resolve the sizing to a concrete size. - let size = self - .sizing - .resolve(styles) - .zip(regions.base) - .map(|(s, b)| s.map(|v| v.relative_to(b))) - .unwrap_or(regions.first); - - // Select the appropriate base and expansion for the child depending - // on whether it is automatically or relatively sized. - let is_auto = self.sizing.as_ref().map(Option::is_none); - let base = is_auto.select(regions.base, size); - let expand = regions.expand | !is_auto; - - Regions::one(size, base, expand) - }; - - // Layout the child. - let mut frames = self.child.layout_inline(world, &pod, styles)?; - - // Ensure frame size matches regions size if expansion is on. - let frame = &mut frames[0]; - let target = regions.expand.select(regions.first, frame.size()); - frame.resize(target, Align::LEFT_TOP); - - Ok(frames) - } -} - -/// A block-level container that places content into a separate flow. -#[derive(Debug, Clone, Hash)] -pub struct BlockNode(pub Content); - -#[node(LayoutBlock)] -impl BlockNode { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - Ok(Self(args.eat()?.unwrap_or_default()).pack()) - } -} - -impl LayoutBlock for BlockNode { - fn layout_block( - &self, - world: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult> { - self.0.layout_block(world, regions, styles) - } -} diff --git a/src/library/layout/flow.rs b/src/library/layout/flow.rs deleted file mode 100644 index f4d18699..00000000 --- a/src/library/layout/flow.rs +++ /dev/null @@ -1,267 +0,0 @@ -use std::cmp::Ordering; - -use super::{AlignNode, PlaceNode, Spacing}; -use crate::library::prelude::*; -use crate::library::text::ParNode; - -/// Arrange spacing, paragraphs and block-level nodes into a flow. -/// -/// This node is reponsible for layouting both the top-level content flow and -/// the contents of boxes. -#[derive(Hash)] -pub struct FlowNode(pub StyleVec); - -/// A child of a flow node. -#[derive(Hash, PartialEq)] -pub enum FlowChild { - /// Vertical spacing between other children. - Spacing(Spacing), - /// Arbitrary block-level content. - Block(Content), - /// A column / region break. - Colbreak, -} - -#[node(LayoutBlock)] -impl FlowNode {} - -impl LayoutBlock for FlowNode { - fn layout_block( - &self, - world: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult> { - let mut layouter = FlowLayouter::new(regions); - - for (child, map) in self.0.iter() { - let styles = map.chain(&styles); - match child { - FlowChild::Spacing(kind) => { - layouter.layout_spacing(*kind, styles); - } - FlowChild::Block(block) => { - layouter.layout_block(world, block, styles)?; - } - FlowChild::Colbreak => { - layouter.finish_region(); - } - } - } - - Ok(layouter.finish()) - } -} - -impl Debug for FlowNode { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_str("Flow ")?; - self.0.fmt(f) - } -} - -impl Debug for FlowChild { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Spacing(kind) => write!(f, "{:?}", kind), - Self::Block(block) => block.fmt(f), - Self::Colbreak => f.pad("Colbreak"), - } - } -} - -impl PartialOrd for FlowChild { - fn partial_cmp(&self, other: &Self) -> Option { - match (self, other) { - (Self::Spacing(a), Self::Spacing(b)) => a.partial_cmp(b), - _ => None, - } - } -} - -/// Performs flow layout. -pub struct FlowLayouter { - /// The regions to layout children into. - regions: Regions, - /// Whether the flow should expand to fill the region. - expand: Axes, - /// The full size of `regions.size` that was available before we started - /// subtracting. - full: Size, - /// The size used by the frames for the current region. - used: Size, - /// The sum of fractions in the current region. - fr: Fr, - /// Spacing and layouted blocks. - items: Vec, - /// Finished frames for previous regions. - finished: Vec, -} - -/// A prepared item in a flow layout. -enum FlowItem { - /// Absolute spacing between other items. - Absolute(Abs), - /// Fractional spacing between other items. - Fractional(Fr), - /// A frame for a layouted block and how to align it. - Frame(Frame, Axes), - /// An absolutely placed frame. - Placed(Frame), -} - -impl FlowLayouter { - /// Create a new flow layouter. - pub fn new(regions: &Regions) -> Self { - let expand = regions.expand; - let full = regions.first; - - // Disable vertical expansion for children. - let mut regions = regions.clone(); - regions.expand.y = false; - - Self { - regions, - expand, - full, - used: Size::zero(), - fr: Fr::zero(), - items: vec![], - finished: vec![], - } - } - - /// Layout spacing. - pub fn layout_spacing(&mut self, spacing: Spacing, styles: StyleChain) { - match spacing { - Spacing::Relative(v) => { - // Resolve the spacing and limit it to the remaining space. - let resolved = v.resolve(styles).relative_to(self.full.y); - let limited = resolved.min(self.regions.first.y); - self.regions.first.y -= limited; - self.used.y += limited; - self.items.push(FlowItem::Absolute(resolved)); - } - Spacing::Fractional(v) => { - self.items.push(FlowItem::Fractional(v)); - self.fr += v; - } - } - } - - /// Layout a block. - pub fn layout_block( - &mut self, - world: Tracked, - block: &Content, - styles: StyleChain, - ) -> SourceResult<()> { - // Don't even try layouting into a full region. - if self.regions.is_full() { - self.finish_region(); - } - - // Placed nodes that are out of flow produce placed items which aren't - // aligned later. - if let Some(placed) = block.downcast::() { - if placed.out_of_flow() { - let frame = block.layout_block(world, &self.regions, styles)?.remove(0); - self.items.push(FlowItem::Placed(frame)); - return Ok(()); - } - } - - // How to align the block. - let aligns = Axes::new( - // For non-expanding paragraphs it is crucial that we align the - // whole paragraph as it is itself aligned. - styles.get(ParNode::ALIGN), - // Vertical align node alignment is respected by the flow. - block - .downcast::() - .and_then(|aligned| aligned.aligns.y) - .map(|align| align.resolve(styles)) - .unwrap_or(Align::Top), - ); - - let frames = block.layout_block(world, &self.regions, styles)?; - let len = frames.len(); - for (i, mut frame) in frames.into_iter().enumerate() { - // Set the generic block role. - frame.apply_role(Role::GenericBlock); - - // Grow our size, shrink the region and save the frame for later. - let size = frame.size(); - self.used.y += size.y; - self.used.x.set_max(size.x); - self.regions.first.y -= size.y; - self.items.push(FlowItem::Frame(frame, aligns)); - - if i + 1 < len { - self.finish_region(); - } - } - - Ok(()) - } - - /// Finish the frame for one region. - pub fn finish_region(&mut self) { - // Determine the size of the flow in this region dependening on whether - // the region expands. - let mut size = self.expand.select(self.full, self.used); - - // Account for fractional spacing in the size calculation. - let remaining = self.full.y - self.used.y; - if self.fr.get() > 0.0 && self.full.y.is_finite() { - self.used.y = self.full.y; - size.y = self.full.y; - } - - let mut output = Frame::new(size); - let mut offset = Abs::zero(); - let mut ruler = Align::Top; - - // Place all frames. - for item in self.items.drain(..) { - match item { - FlowItem::Absolute(v) => { - offset += v; - } - FlowItem::Fractional(v) => { - offset += v.share(self.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 - self.used.y); - let pos = Point::new(x, y); - offset += frame.height(); - output.push_frame(pos, frame); - } - FlowItem::Placed(frame) => { - output.push_frame(Point::zero(), frame); - } - } - } - - // Advance to the next region. - self.regions.next(); - self.full = self.regions.first; - self.used = Size::zero(); - self.fr = Fr::zero(); - self.finished.push(output); - } - - /// Finish layouting and return the resulting frames. - pub fn finish(mut self) -> Vec { - if self.expand.y { - while self.regions.backlog.len() > 0 { - self.finish_region(); - } - } - - self.finish_region(); - self.finished - } -} diff --git a/src/library/layout/grid.rs b/src/library/layout/grid.rs deleted file mode 100644 index 1bb67691..00000000 --- a/src/library/layout/grid.rs +++ /dev/null @@ -1,593 +0,0 @@ -use crate::library::prelude::*; - -/// Arrange content in a grid. -#[derive(Debug, Hash)] -pub struct GridNode { - /// Defines sizing for content rows and columns. - pub tracks: Axes>, - /// Defines sizing of gutter rows and columns between content. - pub gutter: Axes>, - /// The content to be arranged in a grid. - pub cells: Vec, -} - -#[node(LayoutBlock)] -impl GridNode { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - let columns = args.named("columns")?.unwrap_or_default(); - let rows = args.named("rows")?.unwrap_or_default(); - let base_gutter: Vec = args.named("gutter")?.unwrap_or_default(); - let column_gutter = args.named("column-gutter")?; - let row_gutter = args.named("row-gutter")?; - Ok(Self { - tracks: Axes::new(columns, rows), - gutter: Axes::new( - column_gutter.unwrap_or_else(|| base_gutter.clone()), - row_gutter.unwrap_or(base_gutter), - ), - cells: args.all()?, - } - .pack()) - } -} - -impl LayoutBlock for GridNode { - fn layout_block( - &self, - world: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult> { - // Prepare grid layout by unifying content and gutter tracks. - let layouter = GridLayouter::new( - world, - self.tracks.as_deref(), - self.gutter.as_deref(), - &self.cells, - regions, - styles, - ); - - // Measure the columns and layout the grid row-by-row. - layouter.layout() - } -} - -/// Defines how to size a grid cell along an axis. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum TrackSizing { - /// A track that fits its cell's contents. - Auto, - /// A track size specified in absolute terms and relative to the parent's - /// size. - Relative(Rel), - /// A track size specified as a fraction of the remaining free space in the - /// parent. - Fractional(Fr), -} - -castable! { - Vec, - Expected: "integer, auto, relative length, fraction, or array of the latter three", - Value::Auto => vec![TrackSizing::Auto], - Value::Length(v) => vec![TrackSizing::Relative(v.into())], - Value::Ratio(v) => vec![TrackSizing::Relative(v.into())], - Value::Relative(v) => vec![TrackSizing::Relative(v)], - Value::Fraction(v) => vec![TrackSizing::Fractional(v)], - Value::Int(v) => vec![TrackSizing::Auto; Value::Int(v).cast::()?.get()], - Value::Array(values) => values - .into_iter() - .filter_map(|v| v.cast().ok()) - .collect(), -} - -castable! { - TrackSizing, - Expected: "auto, relative length, or fraction", - Value::Auto => Self::Auto, - Value::Length(v) => Self::Relative(v.into()), - Value::Ratio(v) => Self::Relative(v.into()), - Value::Relative(v) => Self::Relative(v), - Value::Fraction(v) => Self::Fractional(v), -} - -/// Performs grid layout. -pub struct GridLayouter<'a> { - /// The core context. - world: Tracked<'a, dyn World>, - /// The grid cells. - cells: &'a [Content], - /// The column tracks including gutter tracks. - cols: Vec, - /// The row tracks including gutter tracks. - rows: Vec, - /// The regions to layout children into. - regions: Regions, - /// The inherited styles. - styles: StyleChain<'a>, - /// Resolved column sizes. - rcols: Vec, - /// Rows in the current region. - lrows: Vec, - /// The full height of the current region. - full: Abs, - /// The used-up size of the current region. The horizontal size is - /// determined once after columns are resolved and not touched again. - used: Size, - /// The sum of fractions in the current region. - fr: Fr, - /// Frames for finished regions. - finished: Vec, -} - -/// 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. - Frame(Frame), - /// 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( - world: Tracked<'a, dyn World>, - tracks: Axes<&[TrackSizing]>, - gutter: Axes<&[TrackSizing]>, - cells: &'a [Content], - regions: &Regions, - 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 auto = TrackSizing::Auto; - let zero = TrackSizing::Relative(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)); - 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)); - rows.push(get_or(gutter.y, y, zero)); - } - - // Remove superfluous gutter tracks. - cols.pop(); - rows.pop(); - - let full = regions.first.y; - let rcols = vec![Abs::zero(); cols.len()]; - let lrows = vec![]; - - // We use the regions for auto row measurement. Since at that moment, - // columns are already sized, we can enable horizontal expansion. - let mut regions = regions.clone(); - regions.expand = Axes::new(true, false); - - Self { - world, - cells, - cols, - rows, - regions, - styles, - rcols, - lrows, - full, - used: Size::zero(), - fr: Fr::zero(), - finished: vec![], - } - } - - /// Determines the columns sizes and then layouts the grid row-by-row. - pub fn layout(mut self) -> SourceResult> { - self.measure_columns()?; - - 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 y % 2 == 0 && self.regions.is_full() { - self.finish_region()?; - } - - match self.rows[y] { - TrackSizing::Auto => self.layout_auto_row(y)?, - TrackSizing::Relative(v) => self.layout_relative_row(v, y)?, - TrackSizing::Fractional(v) => { - self.lrows.push(Row::Fr(v, y)); - self.fr += v; - } - } - } - - self.finish_region()?; - Ok(self.finished) - } - - /// Determine all column sizes. - fn measure_columns(&mut self) -> 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 { - TrackSizing::Auto => {} - TrackSizing::Relative(v) => { - let resolved = - v.resolve(self.styles).relative_to(self.regions.base.x); - *rcol = resolved; - rel += resolved; - } - TrackSizing::Fractional(v) => fr += v, - } - } - - // Size that is not used by fixed-size columns. - let available = self.regions.first.x - rel; - if available >= Abs::zero() { - // Determine size of auto columns. - let (auto, count) = self.measure_auto_columns(available)?; - - // If there is remaining space, distribute it to fractional columns, - // otherwise shrink auto columns. - let remaining = available - auto; - if remaining >= Abs::zero() { - if !fr.is_zero() { - self.grow_fractional_columns(remaining, fr); - } - } else { - self.shrink_auto_columns(available, count); - } - } - - // Sum up the resolved column sizes once here. - self.used.x = self.rcols.iter().sum(); - - Ok(()) - } - - /// Measure the size that is available to auto columns. - fn measure_auto_columns(&mut self, 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 != TrackSizing::Auto { - continue; - } - - let mut resolved = Abs::zero(); - for y in 0 .. self.rows.len() { - if let Some(cell) = self.cell(x, y) { - let size = Size::new(available, self.regions.base.y); - let mut pod = - Regions::one(size, self.regions.base, Axes::splat(false)); - - // For relative rows, we can already resolve the correct - // base, for auto it's already correct and for fr we could - // only guess anyway. - if let TrackSizing::Relative(v) = self.rows[y] { - pod.base.y = - v.resolve(self.styles).relative_to(self.regions.base.y); - } - - let frame = - cell.layout_block(self.world, &pod, self.styles)?.remove(0); - 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) { - for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { - if let TrackSizing::Fractional(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) { - // The fair share each auto column may have. - let fair = available / count as f64; - - // The number of overlarge auto columns and the space that will be - // equally redistributed to them. - let mut overlarge: usize = 0; - let mut redistribute = available; - - // Find out the number of and space used by overlarge auto columns. - for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { - if col == TrackSizing::Auto { - if *rcol > fair { - overlarge += 1; - } else { - redistribute -= *rcol; - } - } - } - - // Redistribute the space equally. - let share = redistribute / overlarge as f64; - for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { - if col == TrackSizing::Auto && *rcol > fair { - *rcol = share; - } - } - } - - /// Layout a row with automatic height. Such a row may break across multiple - /// regions. - fn layout_auto_row(&mut self, y: usize) -> SourceResult<()> { - let mut resolved: Vec = vec![]; - - // Determine the size for each region of the row. - for (x, &rcol) in self.rcols.iter().enumerate() { - if let Some(cell) = self.cell(x, y) { - let mut pod = self.regions.clone(); - pod.first.x = rcol; - pod.base.x = rcol; - - // All widths should be `rcol` except the base for auto columns. - if self.cols[x] == TrackSizing::Auto { - pod.base.x = self.regions.base.x; - } - - let mut sizes = cell - .layout_block(self.world, &pod, self.styles)? - .into_iter() - .map(|frame| frame.height()); - - // For each region, we want to know the maximum height any - // column requires. - 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); - } - } - - // 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(first, y)?; - self.push_row(frame); - return Ok(()); - } - - // Expand all but the last region if the space is not - // eaten up by any fr rows. - if self.fr.is_zero() { - let len = resolved.len(); - for (region, target) in self.regions.iter().zip(&mut resolved[.. len - 1]) { - target.set_max(region.y); - } - } - - // Layout into multiple regions. - let frames = self.layout_multi_row(&resolved, y)?; - let len = frames.len(); - for (i, frame) in frames.into_iter().enumerate() { - self.push_row(frame); - if i + 1 < len { - self.finish_region()?; - } - } - - Ok(()) - } - - /// 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, v: Rel, y: usize) -> SourceResult<()> { - let resolved = v.resolve(self.styles).relative_to(self.regions.base.y); - let frame = self.layout_single_row(resolved, y)?; - - // Skip to fitting region. - let height = frame.height(); - while !self.regions.first.y.fits(height) && !self.regions.in_last() { - self.finish_region()?; - - // Don't skip multiple regions for gutter and don't push a row. - if y % 2 == 1 { - return Ok(()); - } - } - - self.push_row(frame); - - Ok(()) - } - - /// Layout a row with fixed height and return its frame. - fn layout_single_row(&mut self, height: Abs, y: usize) -> SourceResult { - let mut output = Frame::new(Size::new(self.used.x, 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); - - // Set the base to the region's base for auto rows and to the - // size for relative and fractional rows. - let base = Axes::new(self.cols[x], self.rows[y]) - .map(|s| s == TrackSizing::Auto) - .select(self.regions.base, size); - - let pod = Regions::one(size, base, Axes::splat(true)); - let frame = cell.layout_block(self.world, &pod, self.styles)?.remove(0); - match frame.role() { - Some(Role::ListLabel | Role::ListItemBody) => { - output.apply_role(Role::ListItem) - } - Some(Role::TableCell) => output.apply_role(Role::TableRow), - _ => {} - } - - output.push_frame(pos, frame); - } - - pos.x += rcol; - } - - Ok(output) - } - - /// Layout a row spanning multiple regions. - fn layout_multi_row( - &mut self, - heights: &[Abs], - y: usize, - ) -> SourceResult> { - // Prepare frames. - let mut outputs: Vec<_> = heights - .iter() - .map(|&h| Frame::new(Size::new(self.used.x, h))) - .collect(); - - // Prepare regions. - let size = Size::new(self.used.x, heights[0]); - let mut pod = Regions::one(size, self.regions.base, Axes::splat(true)); - pod.backlog = heights[1 ..].to_vec(); - - // 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.first.x = rcol; - pod.base.x = rcol; - - // All widths should be `rcol` except the base for auto columns. - if self.cols[x] == TrackSizing::Auto { - pod.base.x = self.regions.base.x; - } - - // Push the layouted frames into the individual output frames. - let frames = cell.layout_block(self.world, &pod, self.styles)?; - for (output, frame) in outputs.iter_mut().zip(frames) { - match frame.role() { - Some(Role::ListLabel | Role::ListItemBody) => { - output.apply_role(Role::ListItem) - } - Some(Role::TableCell) => output.apply_role(Role::TableRow), - _ => {} - } - output.push_frame(pos, frame); - } - } - - pos.x += rcol; - } - - Ok(outputs) - } - - /// Push a row frame into the current region. - fn push_row(&mut self, frame: Frame) { - self.regions.first.y -= frame.height(); - self.used.y += frame.height(); - self.lrows.push(Row::Frame(frame)); - } - - /// Finish rows for one region. - fn finish_region(&mut self) -> SourceResult<()> { - // Determine the size of the grid in this region, expanding fully if - // there are fr rows. - let mut size = self.used; - if self.fr.get() > 0.0 && self.full.is_finite() { - size.y = self.full; - } - - // The frame for the region. - let mut output = Frame::new(size); - let mut pos = Point::zero(); - - // Place finished rows and layout fractional rows. - for row in std::mem::take(&mut self.lrows) { - let frame = match row { - Row::Frame(frame) => frame, - Row::Fr(v, y) => { - let remaining = self.full - self.used.y; - let height = v.share(self.fr, remaining); - self.layout_single_row(height, y)? - } - }; - - let height = frame.height(); - output.push_frame(pos, frame); - pos.y += height; - } - - self.finished.push(output); - self.regions.next(); - self.full = self.regions.first.y; - self.used.y = Abs::zero(); - self.fr = Fr::zero(); - - 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, x: usize, y: usize) -> Option<&'a Content> { - assert!(x < self.cols.len()); - assert!(y < self.rows.len()); - - // 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 - } - } -} diff --git a/src/library/layout/mod.rs b/src/library/layout/mod.rs deleted file mode 100644 index 000cb212..00000000 --- a/src/library/layout/mod.rs +++ /dev/null @@ -1,814 +0,0 @@ -//! Composable layouts. - -mod align; -mod columns; -mod container; -mod flow; -mod grid; -mod pad; -mod page; -mod place; -mod spacing; -mod stack; -mod transform; - -pub use align::*; -pub use columns::*; -pub use container::*; -pub use flow::*; -pub use grid::*; -pub use pad::*; -pub use page::*; -pub use place::*; -pub use spacing::*; -pub use stack::*; -pub use transform::*; - -use std::mem; - -use comemo::Tracked; -use typed_arena::Arena; - -use crate::diag::SourceResult; -use crate::frame::Frame; -use crate::geom::*; -use crate::library::structure::{DocNode, ListItem, ListNode, DESC, ENUM, LIST}; -use crate::library::text::{ - LinebreakNode, ParChild, ParNode, ParbreakNode, SmartQuoteNode, SpaceNode, TextNode, -}; -use crate::model::{ - capability, Barrier, Content, Interruption, Node, SequenceNode, Show, StyleChain, - StyleEntry, StyleMap, StyleVec, StyleVecBuilder, StyledNode, Target, -}; -use crate::World; - -/// The root-level layout. -#[capability] -pub trait Layout: 'static + Sync + Send { - /// Layout into one frame per page. - fn layout(&self, world: Tracked) -> SourceResult>; -} - -impl Layout for Content { - #[comemo::memoize] - fn layout(&self, world: Tracked) -> SourceResult> { - let styles = StyleChain::with_root(&world.config().styles); - let scratch = Scratch::default(); - - let mut builder = Builder::new(world, &scratch, true); - builder.accept(self, styles)?; - - let (doc, shared) = builder.into_doc(styles)?; - doc.layout(world, shared) - } -} - -/// Block-level layout. -#[capability] -pub trait LayoutBlock: 'static + Sync + Send { - /// Layout into one frame per region. - fn layout_block( - &self, - world: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult>; -} - -impl LayoutBlock for Content { - #[comemo::memoize] - fn layout_block( - &self, - world: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult> { - if let Some(node) = self.to::() { - let barrier = StyleEntry::Barrier(Barrier::new(self.id())); - let styles = barrier.chain(&styles); - return node.layout_block(world, regions, styles); - } - - let scratch = Scratch::default(); - let mut builder = Builder::new(world, &scratch, false); - builder.accept(self, styles)?; - let (flow, shared) = builder.into_flow(styles)?; - flow.layout_block(world, regions, shared) - } -} - -/// Inline-level layout. -#[capability] -pub trait LayoutInline: 'static + Sync + Send { - /// Layout into a single frame. - fn layout_inline( - &self, - world: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult>; -} - -impl LayoutInline for Content { - #[comemo::memoize] - fn layout_inline( - &self, - world: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult> { - if let Some(node) = self.to::() { - let barrier = StyleEntry::Barrier(Barrier::new(self.id())); - let styles = barrier.chain(&styles); - return node.layout_inline(world, regions, styles); - } - - if let Some(node) = self.to::() { - let barrier = StyleEntry::Barrier(Barrier::new(self.id())); - let styles = barrier.chain(&styles); - return node.layout_block(world, regions, styles); - } - - let scratch = Scratch::default(); - let mut builder = Builder::new(world, &scratch, false); - builder.accept(self, styles)?; - let (flow, shared) = builder.into_flow(styles)?; - flow.layout_block(world, regions, shared) - } -} - -/// A sequence of regions to layout into. -#[derive(Debug, Clone, Hash)] -pub struct Regions { - /// The (remaining) size of the first region. - pub first: Size, - /// The base size for relative sizing. - pub base: Size, - /// The height of followup regions. The width is the same for all regions. - pub backlog: Vec, - /// 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, - /// Whether nodes should expand to fill the regions instead of shrinking to - /// fit the content. - pub expand: Axes, -} - -impl Regions { - /// Create a new region sequence with exactly one region. - pub fn one(size: Size, base: Size, expand: Axes) -> Self { - Self { - first: size, - base, - backlog: vec![], - last: None, - expand, - } - } - - /// Create a new sequence of same-size regions that repeats indefinitely. - pub fn repeat(size: Size, base: Size, expand: Axes) -> Self { - Self { - first: size, - base, - backlog: vec![], - last: Some(size.y), - expand, - } - } - - /// 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(&self, mut f: F) -> Self - where - F: FnMut(Size) -> Size, - { - let x = self.first.x; - Self { - first: f(self.first), - base: f(self.base), - backlog: self.backlog.iter().map(|&y| f(Size::new(x, y)).y).collect(), - last: self.last.map(|y| f(Size::new(x, y)).y), - expand: self.expand, - } - } - - /// Whether the first region is full and a region break is called for. - pub fn is_full(&self) -> bool { - Abs::zero().fits(self.first.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.len() == 0 && self.last.map_or(true, |height| self.first.y == height) - } - - /// Advance to the next region if there is any. - pub fn next(&mut self) { - if let Some(height) = (!self.backlog.is_empty()) - .then(|| self.backlog.remove(0)) - .or(self.last) - { - self.first.y = height; - self.base.y = 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 iterater may be infinite. - pub fn iter(&self) -> impl Iterator + '_ { - let first = std::iter::once(self.first); - let backlog = self.backlog.iter(); - let last = self.last.iter().cycle(); - first.chain(backlog.chain(last).map(|&h| Size::new(self.first.x, h))) - } -} - -/// Builds a document or a flow node from content. -struct Builder<'a> { - /// The core context. - world: Tracked<'a, dyn World>, - /// Scratch arenas for building. - scratch: &'a Scratch<'a>, - /// The current document building state. - doc: Option>, - /// 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>, - /// An arena where intermediate content resulting from show rules is stored. - templates: Arena, -} - -impl<'a> Builder<'a> { - pub fn new( - world: Tracked<'a, dyn World>, - scratch: &'a Scratch<'a>, - top: bool, - ) -> Self { - Self { - world, - scratch, - doc: top.then(|| DocBuilder::default()), - flow: FlowBuilder::default(), - par: ParBuilder::default(), - list: ListBuilder::default(), - } - } - - pub fn into_doc( - mut self, - styles: StyleChain<'a>, - ) -> SourceResult<(DocNode, StyleChain<'a>)> { - self.interrupt(Interruption::Page, styles, true)?; - let (pages, shared) = self.doc.unwrap().pages.finish(); - Ok((DocNode(pages), shared)) - } - - pub fn into_flow( - mut self, - styles: StyleChain<'a>, - ) -> SourceResult<(FlowNode, StyleChain<'a>)> { - self.interrupt(Interruption::Par, styles, false)?; - let (children, shared) = self.flow.0.finish(); - Ok((FlowNode(children), shared)) - } - - pub fn accept( - &mut self, - content: &'a Content, - styles: StyleChain<'a>, - ) -> SourceResult<()> { - if let Some(text) = content.downcast::() { - if let Some(realized) = styles.apply(self.world, Target::Text(&text.0))? { - let stored = self.scratch.templates.alloc(realized); - return self.accept(stored, styles); - } - } else if let Some(styled) = content.downcast::() { - return self.styled(styled, styles); - } else if let Some(seq) = content.downcast::() { - return self.sequence(seq, styles); - } else if content.has::() { - if self.show(&content, styles)? { - return Ok(()); - } - } - - if self.list.accept(content, styles) { - return Ok(()); - } - - self.interrupt(Interruption::List, styles, false)?; - - if content.is::() { - self.list.accept(content, styles); - return Ok(()); - } - - if self.par.accept(content, styles) { - return Ok(()); - } - - self.interrupt(Interruption::Par, styles, false)?; - - if self.flow.accept(content, styles) { - return Ok(()); - } - - let keep = content - .downcast::() - .map_or(false, |pagebreak| !pagebreak.weak); - self.interrupt(Interruption::Page, styles, keep)?; - - if let Some(doc) = &mut self.doc { - doc.accept(content, styles); - } - - // We might want to issue a warning or error for content that wasn't - // handled (e.g. a pagebreak in a flow building process). However, we - // don't have the spans here at the moment. - Ok(()) - } - - fn show( - &mut self, - content: &'a Content, - styles: StyleChain<'a>, - ) -> SourceResult { - if let Some(mut realized) = styles.apply(self.world, Target::Node(content))? { - let mut map = StyleMap::new(); - let barrier = Barrier::new(content.id()); - map.push(StyleEntry::Barrier(barrier)); - map.push(StyleEntry::Barrier(barrier)); - realized = realized.styled_with_map(map); - let stored = self.scratch.templates.alloc(realized); - self.accept(stored, styles)?; - Ok(true) - } else { - Ok(false) - } - } - - fn styled( - &mut self, - styled: &'a StyledNode, - styles: StyleChain<'a>, - ) -> SourceResult<()> { - let stored = self.scratch.styles.alloc(styles); - let styles = styled.map.chain(stored); - let intr = styled.map.interruption(); - - if let Some(intr) = intr { - self.interrupt(intr, styles, false)?; - } - - self.accept(&styled.sub, styles)?; - - if let Some(intr) = intr { - self.interrupt(intr, styles, true)?; - } - - Ok(()) - } - - fn interrupt( - &mut self, - intr: Interruption, - styles: StyleChain<'a>, - keep: bool, - ) -> SourceResult<()> { - if intr >= Interruption::List && !self.list.is_empty() { - mem::take(&mut self.list).finish(self)?; - } - - if intr >= Interruption::Par { - if !self.par.is_empty() { - mem::take(&mut self.par).finish(self); - } - } - - if intr >= Interruption::Page { - if let Some(doc) = &mut self.doc { - if !self.flow.is_empty() || (doc.keep_next && keep) { - mem::take(&mut self.flow).finish(doc, styles); - } - doc.keep_next = !keep; - } - } - - Ok(()) - } - - fn sequence( - &mut self, - seq: &'a SequenceNode, - styles: StyleChain<'a>, - ) -> SourceResult<()> { - for content in &seq.0 { - self.accept(content, styles)?; - } - Ok(()) - } -} - -/// Accepts pagebreaks and pages. -struct DocBuilder<'a> { - /// The page runs built so far. - pages: StyleVecBuilder<'a, PageNode>, - /// Whether to keep a following page even if it is empty. - keep_next: bool, -} - -impl<'a> DocBuilder<'a> { - fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) { - if let Some(pagebreak) = content.downcast::() { - self.keep_next = !pagebreak.weak; - } - - if let Some(page) = content.downcast::() { - self.pages.push(page.clone(), styles); - self.keep_next = false; - } - } -} - -impl Default for DocBuilder<'_> { - fn default() -> Self { - Self { - pages: StyleVecBuilder::new(), - keep_next: true, - } - } -} - -/// Accepts flow content. -#[derive(Default)] -struct FlowBuilder<'a>(CollapsingBuilder<'a, FlowChild>); - -impl<'a> FlowBuilder<'a> { - fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { - // Weak flow elements: - // Weakness | Element - // 0 | weak colbreak - // 1 | weak fractional spacing - // 2 | weak spacing - // 3 | generated weak spacing - // 4 | generated weak fractional spacing - // 5 | par spacing - - if let Some(_) = content.downcast::() { - /* Nothing to do */ - } else if let Some(colbreak) = content.downcast::() { - if colbreak.weak { - self.0.weak(FlowChild::Colbreak, styles, 0); - } else { - self.0.destructive(FlowChild::Colbreak, styles); - } - } else if let Some(vertical) = content.downcast::() { - let child = FlowChild::Spacing(vertical.amount); - let frac = vertical.amount.is_fractional(); - if vertical.weak { - let weakness = 1 + u8::from(frac) + 2 * u8::from(vertical.generated); - self.0.weak(child, styles, weakness); - } else if frac { - self.0.destructive(child, styles); - } else { - self.0.ignorant(child, styles); - } - } else if content.has::() { - let child = FlowChild::Block(content.clone()); - if content.is::() { - self.0.ignorant(child, styles); - } else { - self.0.supportive(child, styles); - } - } else { - return false; - } - - true - } - - fn par(&mut self, par: ParNode, styles: StyleChain<'a>, indent: bool) { - let amount = if indent && !styles.get(ParNode::SPACING_AND_INDENT) { - styles.get(ParNode::LEADING).into() - } else { - styles.get(ParNode::SPACING).into() - }; - - self.0.weak(FlowChild::Spacing(amount), styles, 5); - self.0.supportive(FlowChild::Block(par.pack()), styles); - self.0.weak(FlowChild::Spacing(amount), styles, 5); - } - - fn finish(self, doc: &mut DocBuilder<'a>, styles: StyleChain<'a>) { - let (flow, shared) = self.0.finish(); - let styles = if flow.is_empty() { styles } else { shared }; - let node = PageNode(FlowNode(flow).pack()); - doc.pages.push(node, styles); - } - - fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - -/// Accepts paragraph content. -#[derive(Default)] -struct ParBuilder<'a>(CollapsingBuilder<'a, ParChild>); - -impl<'a> ParBuilder<'a> { - fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { - // Weak par elements: - // Weakness | Element - // 0 | weak fractional spacing - // 1 | weak spacing - // 2 | space - - if content.is::() { - self.0.weak(ParChild::Text(' '.into()), styles, 2); - } else if let Some(linebreak) = content.downcast::() { - let c = if linebreak.justify { '\u{2028}' } else { '\n' }; - self.0.destructive(ParChild::Text(c.into()), styles); - } else if let Some(horizontal) = content.downcast::() { - let child = ParChild::Spacing(horizontal.amount); - let frac = horizontal.amount.is_fractional(); - if horizontal.weak { - let weakness = u8::from(!frac); - self.0.weak(child, styles, weakness); - } else if frac { - self.0.destructive(child, styles); - } else { - self.0.ignorant(child, styles); - } - } else if let Some(quote) = content.downcast::() { - self.0.supportive(ParChild::Quote { double: quote.double }, styles); - } else if let Some(text) = content.downcast::() { - self.0.supportive(ParChild::Text(text.0.clone()), styles); - } else if content.has::() { - self.0.supportive(ParChild::Inline(content.clone()), styles); - } else { - return false; - } - - true - } - - fn finish(self, parent: &mut Builder<'a>) { - let (mut children, shared) = self.0.finish(); - if children.is_empty() { - return; - } - - // Paragraph indent should only apply if the paragraph starts with - // text and follows directly after another paragraph. - let indent = shared.get(ParNode::INDENT); - if !indent.is_zero() - && children - .items() - .find_map(|child| match child { - ParChild::Spacing(_) => None, - ParChild::Text(_) | ParChild::Quote { .. } => Some(true), - ParChild::Inline(_) => Some(false), - }) - .unwrap_or_default() - && parent - .flow - .0 - .items() - .rev() - .find_map(|child| match child { - FlowChild::Spacing(_) => None, - FlowChild::Block(content) => Some(content.is::()), - FlowChild::Colbreak => Some(false), - }) - .unwrap_or_default() - { - children.push_front(ParChild::Spacing(indent.into())); - } - - parent.flow.par(ParNode(children), shared, !indent.is_zero()); - } - - fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - -/// Accepts list / enum items, spaces, paragraph breaks. -struct ListBuilder<'a> { - /// The list items collected so far. - items: StyleVecBuilder<'a, ListItem>, - /// Whether the list contains no paragraph breaks. - tight: bool, - /// Whether the list can be attached. - attachable: 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() { - if content.is::() { - self.attachable = false; - } else if !content.is::() && !content.is::() { - self.attachable = true; - } - } - - if let Some(item) = content.downcast::() { - if self - .items - .items() - .next() - .map_or(true, |first| item.kind() == first.kind()) - { - self.items.push(item.clone(), styles); - self.tight &= self.staged.drain(..).all(|(t, _)| !t.is::()); - } else { - return false; - } - } else if !self.items.is_empty() - && (content.is::() || content.is::()) - { - self.staged.push((content, styles)); - } else { - return false; - } - - true - } - - fn finish(self, parent: &mut Builder<'a>) -> SourceResult<()> { - let (items, shared) = self.items.finish(); - let kind = match items.items().next() { - Some(item) => item.kind(), - None => return Ok(()), - }; - - let tight = self.tight; - let attached = tight && self.attachable; - let content = match kind { - LIST => ListNode:: { tight, attached, items }.pack(), - ENUM => ListNode:: { tight, attached, items }.pack(), - DESC | _ => ListNode:: { tight, attached, items }.pack(), - }; - - let stored = parent.scratch.templates.alloc(content); - parent.accept(stored, shared)?; - - for (content, styles) in self.staged { - parent.accept(content, styles)?; - } - - parent.list.attachable = true; - - Ok(()) - } - - fn is_empty(&self) -> bool { - self.items.is_empty() - } -} - -impl Default for ListBuilder<'_> { - fn default() -> Self { - Self { - items: StyleVecBuilder::default(), - tight: true, - attachable: true, - staged: vec![], - } - } -} - -/// A wrapper around a [`StyleVecBuilder`] that allows to collapse items. -struct CollapsingBuilder<'a, T> { - /// The internal builder. - builder: StyleVecBuilder<'a, T>, - /// Staged weak and ignorant items that we can't yet commit to the builder. - /// The option is `Some(_)` for weak items and `None` for ignorant items. - staged: Vec<(T, StyleChain<'a>, Option)>, - /// What the last non-ignorant item was. - last: Last, -} - -/// What the last non-ignorant item was. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -enum Last { - Weak, - Destructive, - Supportive, -} - -impl<'a, T> CollapsingBuilder<'a, T> { - /// Create a new style-vec builder. - pub fn new() -> Self { - Self { - builder: StyleVecBuilder::new(), - staged: vec![], - last: Last::Destructive, - } - } - - /// Whether the builder is empty. - pub fn is_empty(&self) -> bool { - self.builder.is_empty() && self.staged.is_empty() - } - - /// Can only exist when there is at least one supportive item to its left - /// and to its right, with no destructive items in between. There may be - /// ignorant items in between in both directions. - /// - /// Between weak items, there may be at least one per layer and among the - /// candidates the strongest one (smallest `weakness`) wins. When tied, - /// the one that compares larger through `PartialOrd` wins. - pub fn weak(&mut self, item: T, styles: StyleChain<'a>, weakness: u8) - where - T: PartialOrd, - { - if self.last == Last::Destructive { - return; - } - - if self.last == Last::Weak { - if let Some(i) = - self.staged.iter().position(|(prev_item, _, prev_weakness)| { - prev_weakness.map_or(false, |prev_weakness| { - weakness < prev_weakness - || (weakness == prev_weakness && item > *prev_item) - }) - }) - { - self.staged.remove(i); - } else { - return; - } - } - - self.staged.push((item, styles, Some(weakness))); - self.last = Last::Weak; - } - - /// Forces nearby weak items to collapse. - pub fn destructive(&mut self, item: T, styles: StyleChain<'a>) { - self.flush(false); - self.builder.push(item, styles); - self.last = Last::Destructive; - } - - /// Allows nearby weak items to exist. - pub fn supportive(&mut self, item: T, styles: StyleChain<'a>) { - self.flush(true); - self.builder.push(item, styles); - self.last = Last::Supportive; - } - - /// Has no influence on other items. - pub fn ignorant(&mut self, item: T, styles: StyleChain<'a>) { - self.staged.push((item, styles, None)); - } - - /// Iterate over the contained items. - pub fn items(&self) -> impl DoubleEndedIterator { - self.builder.items().chain(self.staged.iter().map(|(item, ..)| item)) - } - - /// Return the finish style vec and the common prefix chain. - pub fn finish(mut self) -> (StyleVec, StyleChain<'a>) { - self.flush(false); - self.builder.finish() - } - - /// Push the staged items, filtering out weak items if `supportive` is - /// false. - fn flush(&mut self, supportive: bool) { - for (item, styles, meta) in self.staged.drain(..) { - if supportive || meta.is_none() { - self.builder.push(item, styles); - } - } - } -} - -impl<'a, T> Default for CollapsingBuilder<'a, T> { - fn default() -> Self { - Self::new() - } -} diff --git a/src/library/layout/pad.rs b/src/library/layout/pad.rs deleted file mode 100644 index 920660d6..00000000 --- a/src/library/layout/pad.rs +++ /dev/null @@ -1,83 +0,0 @@ -use crate::library::prelude::*; - -/// Pad content at the sides. -#[derive(Debug, Hash)] -pub struct PadNode { - /// The amount of padding. - pub padding: Sides>, - /// The content whose sides to pad. - pub child: Content, -} - -#[node(LayoutBlock)] -impl PadNode { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - let all = args.named("rest")?.or(args.find()?); - let x = args.named("x")?; - let y = args.named("y")?; - let left = args.named("left")?.or(x).or(all).unwrap_or_default(); - let top = args.named("top")?.or(y).or(all).unwrap_or_default(); - let right = args.named("right")?.or(x).or(all).unwrap_or_default(); - let bottom = args.named("bottom")?.or(y).or(all).unwrap_or_default(); - let body = args.expect::("body")?; - let padding = Sides::new(left, top, right, bottom); - Ok(body.padded(padding)) - } -} - -impl LayoutBlock for PadNode { - fn layout_block( - &self, - world: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult> { - // Layout child into padded regions. - let padding = self.padding.resolve(styles); - let pod = regions.map(|size| shrink(size, padding)); - let mut frames = self.child.layout_block(world, &pod, styles)?; - - for frame in &mut frames { - // 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(frames) - } -} - -/// Shrink a size by padding relative to the size itself. -fn shrink(size: Size, padding: Sides>) -> 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>) -> Size { - size.zip(padding.sum_by_axis()) - .map(|(s, p)| (s + p.abs).safe_div(1.0 - p.rel.get())) -} diff --git a/src/library/layout/page.rs b/src/library/layout/page.rs deleted file mode 100644 index 8d081749..00000000 --- a/src/library/layout/page.rs +++ /dev/null @@ -1,421 +0,0 @@ -use std::str::FromStr; - -use super::ColumnsNode; -use crate::library::prelude::*; - -/// Layouts its child onto one or multiple pages. -#[derive(PartialEq, Clone, Hash)] -pub struct PageNode(pub Content); - -#[node] -impl PageNode { - /// The unflipped width of the page. - #[property(resolve)] - pub const WIDTH: Smart = Smart::Custom(Paper::A4.width().into()); - /// The unflipped height of the page. - #[property(resolve)] - pub const HEIGHT: Smart = Smart::Custom(Paper::A4.height().into()); - /// Whether the page is flipped into landscape orientation. - pub const FLIPPED: bool = false; - - /// The page's margins. - #[property(fold)] - pub const MARGINS: Sides>>> = Sides::splat(Smart::Auto); - - /// How many columns the page has. - pub const COLUMNS: NonZeroUsize = NonZeroUsize::new(1).unwrap(); - /// The page's background color. - pub const FILL: Option = None; - - /// The page's header. - #[property(referenced)] - pub const HEADER: Marginal = Marginal::None; - /// The page's footer. - #[property(referenced)] - pub const FOOTER: Marginal = Marginal::None; - /// Content in the page's background. - #[property(referenced)] - pub const BACKGROUND: Marginal = Marginal::None; - /// Content in the page's foreground. - #[property(referenced)] - pub const FOREGROUND: Marginal = Marginal::None; - - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - Ok(Self(args.expect("body")?).pack()) - } - - fn set(...) { - if let Some(paper) = args.named_or_find::("paper")? { - styles.set(Self::WIDTH, Smart::Custom(paper.width().into())); - styles.set(Self::HEIGHT, Smart::Custom(paper.height().into())); - } - } -} - -impl PageNode { - /// Layout the page run into a sequence of frames, one per page. - pub fn layout( - &self, - world: Tracked, - mut page: usize, - styles: StyleChain, - ) -> SourceResult> { - // When one of the lengths is infinite the page fits its content along - // that axis. - let width = styles.get(Self::WIDTH).unwrap_or(Abs::inf()); - let height = styles.get(Self::HEIGHT).unwrap_or(Abs::inf()); - let mut size = Size::new(width, height); - if styles.get(Self::FLIPPED) { - 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::from(0.1190 * min); - let padding = styles.get(Self::MARGINS).map(|side| side.unwrap_or(default)); - - let mut child = self.0.clone(); - - // Realize columns. - let columns = styles.get(Self::COLUMNS); - if columns.get() > 1 { - child = ColumnsNode { columns, child: self.0.clone() }.pack(); - } - - // Realize margins. - child = child.padded(padding); - - // Realize background fill. - if let Some(fill) = styles.get(Self::FILL) { - child = child.filled(fill); - } - - // Layout the child. - let regions = Regions::repeat(size, size, size.map(Abs::is_finite)); - let mut frames = child.layout_block(world, ®ions, styles)?; - - let header = styles.get(Self::HEADER); - let footer = styles.get(Self::FOOTER); - let foreground = styles.get(Self::FOREGROUND); - let background = styles.get(Self::BACKGROUND); - - // Realize overlays. - for frame in &mut frames { - let size = frame.size(); - let pad = padding.resolve(styles).relative_to(size); - let pw = size.x - pad.left - pad.right; - let py = size.y - pad.bottom; - for (role, marginal, pos, area) in [ - ( - Role::Header, - header, - Point::with_x(pad.left), - Size::new(pw, pad.top), - ), - ( - Role::Footer, - footer, - Point::new(pad.left, py), - Size::new(pw, pad.bottom), - ), - (Role::Foreground, foreground, Point::zero(), size), - (Role::Background, background, Point::zero(), size), - ] { - if let Some(content) = marginal.resolve(world, page)? { - let pod = Regions::one(area, area, Axes::splat(true)); - let mut sub = content.layout_block(world, &pod, styles)?.remove(0); - sub.apply_role(role); - - if role == Role::Background { - frame.prepend_frame(pos, sub); - } else { - frame.push_frame(pos, sub); - } - } - } - - page += 1; - } - - Ok(frames) - } -} - -impl Debug for PageNode { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_str("Page(")?; - self.0.fmt(f)?; - f.write_str(")") - } -} - -/// A page break. -#[derive(Debug, Copy, Clone, Hash)] -pub struct PagebreakNode { - pub weak: bool, -} - -#[node] -impl PagebreakNode { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - let weak = args.named("weak")?.unwrap_or(false); - Ok(Self { weak }.pack()) - } -} - -/// A header, footer, foreground or background definition. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum Marginal { - /// Nothing, - None, - /// Bare content. - Content(Content), - /// A closure mapping from a page number to content. - Func(Func, Span), -} - -impl Marginal { - /// Resolve the marginal based on the page number. - pub fn resolve( - &self, - world: Tracked, - page: usize, - ) -> SourceResult> { - Ok(match self { - Self::None => None, - Self::Content(content) => Some(content.clone()), - Self::Func(func, span) => { - let args = Args::new(*span, [Value::Int(page as i64)]); - Some(func.call_detached(world, args)?.display(world)) - } - }) - } -} - -impl Cast> for Marginal { - fn is(value: &Spanned) -> bool { - matches!(&value.v, Value::Content(_) | Value::Func(_)) - } - - fn cast(value: Spanned) -> StrResult { - match value.v { - Value::None => Ok(Self::None), - Value::Str(v) => Ok(Self::Content(TextNode(v.into()).pack())), - Value::Content(v) => Ok(Self::Content(v)), - Value::Func(v) => Ok(Self::Func(v, value.span)), - v => Err(format!( - "expected none, content or function, found {}", - v.type_name(), - )), - } - } -} - -/// Specification of a paper. -#[derive(Debug, Copy, Clone)] -pub struct Paper { - /// The width of the paper in millimeters. - width: f64, - /// The height of the paper in millimeters. - height: f64, -} - -impl Paper { - /// The width of the paper. - pub fn width(self) -> Abs { - Abs::mm(self.width) - } - - /// The height of the paper. - pub fn height(self) -> Abs { - Abs::mm(self.height) - } -} - -/// Defines paper constants and a paper parsing implementation. -macro_rules! papers { - ($(($var:ident: $width:expr, $height: expr, $($pats:tt)*))*) => { - /// Predefined papers. - /// - /// Each paper is parsable from its name in kebab-case. - impl Paper { - $(pub const $var: Self = Self { width: $width, height: $height };)* - } - - impl FromStr for Paper { - type Err = &'static str; - - fn from_str(name: &str) -> Result { - match name.to_lowercase().as_str() { - $($($pats)* => Ok(Self::$var),)* - _ => Err("invalid paper name"), - } - } - } - }; -} - -castable! { - Paper, - Expected: "string", - Value::Str(string) => Self::from_str(&string)?, -} - -// 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/src/library/layout/place.rs b/src/library/layout/place.rs deleted file mode 100644 index ee38ebe6..00000000 --- a/src/library/layout/place.rs +++ /dev/null @@ -1,56 +0,0 @@ -use super::AlignNode; -use crate::library::prelude::*; - -/// Place content at an absolute position. -#[derive(Debug, Hash)] -pub struct PlaceNode(pub Content); - -#[node(LayoutBlock)] -impl PlaceNode { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - let aligns = args.find()?.unwrap_or(Axes::with_x(Some(RawAlign::Start))); - let dx = args.named("dx")?.unwrap_or_default(); - let dy = args.named("dy")?.unwrap_or_default(); - let body = args.expect::("body")?; - Ok(Self(body.moved(Axes::new(dx, dy)).aligned(aligns)).pack()) - } -} - -impl LayoutBlock for PlaceNode { - fn layout_block( - &self, - world: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult> { - let out_of_flow = self.out_of_flow(); - - // 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, regions.base, expand) - }; - - let mut frames = self.0.layout_block(world, &pod, styles)?; - - // 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.first, Size::zero()); - frames[0].resize(target, Align::LEFT_TOP); - - Ok(frames) - } -} - -impl PlaceNode { - /// Whether this node 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) -> bool { - self.0 - .downcast::() - .map_or(false, |node| node.aligns.y.is_some()) - } -} diff --git a/src/library/layout/spacing.rs b/src/library/layout/spacing.rs deleted file mode 100644 index c410eee7..00000000 --- a/src/library/layout/spacing.rs +++ /dev/null @@ -1,100 +0,0 @@ -use std::cmp::Ordering; - -use crate::library::prelude::*; -use crate::library::text::ParNode; - -/// Horizontal spacing. -#[derive(Debug, Clone, Hash)] -pub struct HNode { - pub amount: Spacing, - pub weak: bool, -} - -#[node] -impl HNode { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - let amount = args.expect("spacing")?; - let weak = args.named("weak")?.unwrap_or(false); - Ok(Self { amount, weak }.pack()) - } -} - -/// Vertical spacing. -#[derive(Debug, Clone, Hash)] -pub struct VNode { - pub amount: Spacing, - pub weak: bool, - pub generated: bool, -} - -#[node] -impl VNode { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - let amount = args.expect("spacing")?; - let weak = args.named("weak")?.unwrap_or(false); - Ok(Self { amount, weak, generated: false }.pack()) - } -} - -/// 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. - Relative(Rel), - /// Spacing specified as a fraction of the remaining free space in the - /// parent. - Fractional(Fr), -} - -impl Spacing { - /// Whether this is fractional spacing. - pub fn is_fractional(self) -> bool { - matches!(self, Self::Fractional(_)) - } -} - -impl From for Spacing { - fn from(abs: Abs) -> Self { - Self::Relative(abs.into()) - } -} - -impl PartialOrd for Spacing { - fn partial_cmp(&self, other: &Self) -> Option { - match (self, other) { - (Self::Relative(a), Self::Relative(b)) => a.partial_cmp(b), - (Self::Fractional(a), Self::Fractional(b)) => a.partial_cmp(b), - _ => None, - } - } -} - -castable! { - Spacing, - Expected: "relative length or fraction", - Value::Length(v) => Self::Relative(v.into()), - Value::Ratio(v) => Self::Relative(v.into()), - Value::Relative(v) => Self::Relative(v), - Value::Fraction(v) => Self::Fractional(v), -} - -/// Spacing around and between blocks, relative to paragraph spacing. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct BlockSpacing(Rel); - -castable!(BlockSpacing: Rel); - -impl Resolve for BlockSpacing { - type Output = Abs; - - fn resolve(self, styles: StyleChain) -> Self::Output { - let whole = styles.get(ParNode::SPACING); - self.0.resolve(styles).relative_to(whole) - } -} - -impl From for BlockSpacing { - fn from(ratio: Ratio) -> Self { - Self(ratio.into()) - } -} diff --git a/src/library/layout/stack.rs b/src/library/layout/stack.rs deleted file mode 100644 index e1e70de9..00000000 --- a/src/library/layout/stack.rs +++ /dev/null @@ -1,321 +0,0 @@ -use super::{AlignNode, Spacing}; -use crate::library::prelude::*; -use crate::library::text::ParNode; -use crate::model::StyledNode; - -/// Arrange content and spacing along an axis. -#[derive(Debug, Hash)] -pub struct StackNode { - /// The stacking direction. - pub dir: Dir, - /// The spacing between non-spacing children. - pub spacing: Option, - /// The children to be stacked. - pub children: Vec, -} - -#[node(LayoutBlock)] -impl StackNode { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - Ok(Self { - dir: args.named("dir")?.unwrap_or(Dir::TTB), - spacing: args.named("spacing")?, - children: args.all()?, - } - .pack()) - } -} - -impl LayoutBlock for StackNode { - fn layout_block( - &self, - world: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult> { - let mut layouter = StackLayouter::new(self.dir, regions, styles); - - // Spacing to insert before the next block. - 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(world, block, styles)?; - deferred = self.spacing; - } - } - } - - Ok(layouter.finish()) - } -} - -/// A child of a stack node. -#[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), - } - } -} - -castable! { - StackChild, - Expected: "relative length, fraction, or content", - Value::Length(v) => Self::Spacing(Spacing::Relative(v.into())), - Value::Ratio(v) => Self::Spacing(Spacing::Relative(v.into())), - Value::Relative(v) => Self::Spacing(Spacing::Relative(v)), - Value::Fraction(v) => Self::Spacing(Spacing::Fractional(v)), - Value::Content(v) => Self::Block(v), -} - -/// Performs stack layout. -pub struct StackLayouter<'a> { - /// The stacking direction. - dir: Dir, - /// The axis of the stacking direction. - axis: Axis, - /// The regions to layout children into. - regions: Regions, - /// The inherited styles. - styles: StyleChain<'a>, - /// Whether the stack itself should expand to fill the region. - expand: Axes, - /// The full size of the current region that was available at the start. - full: Size, - /// The generic size used by the frames for the current region. - used: Gen, - /// 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, - /// Finished frames for previous regions. - finished: Vec, -} - -/// 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, Align), -} - -impl<'a> StackLayouter<'a> { - /// Create a new stack layouter. - pub fn new(dir: Dir, regions: &Regions, styles: StyleChain<'a>) -> Self { - let axis = dir.axis(); - let expand = regions.expand; - let full = regions.first; - - // Disable expansion along the block axis for children. - let mut regions = regions.clone(); - regions.expand.set(axis, false); - - Self { - dir, - axis, - regions, - styles, - expand, - full, - used: Gen::zero(), - fr: Fr::zero(), - items: vec![], - finished: vec![], - } - } - - /// Add spacing along the spacing direction. - pub fn layout_spacing(&mut self, spacing: Spacing) { - match spacing { - Spacing::Relative(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.first.get_mut(self.axis); - let limited = resolved.min(*remaining); - *remaining -= limited; - self.used.main += limited; - self.items.push(StackItem::Absolute(resolved)); - } - Spacing::Fractional(v) => { - self.fr += v; - self.items.push(StackItem::Fractional(v)); - } - } - } - - /// Layout an arbitrary block. - pub fn layout_block( - &mut self, - world: Tracked, - block: &Content, - styles: StyleChain, - ) -> SourceResult<()> { - if self.regions.is_full() { - self.finish_region(); - } - - // Block-axis alignment of the `AlignNode` is respected - // by the stack node. - let align = block - .downcast::() - .and_then(|node| node.aligns.get(self.axis)) - .map(|align| align.resolve(styles)) - .unwrap_or_else(|| { - if let Some(styled) = block.downcast::() { - let map = &styled.map; - if map.contains(ParNode::ALIGN) { - return StyleChain::with_root(map).get(ParNode::ALIGN); - } - } - - self.dir.start().into() - }); - - let frames = block.layout_block(world, &self.regions, styles)?; - let len = frames.len(); - for (i, mut frame) in frames.into_iter().enumerate() { - // Set the generic block role. - frame.apply_role(Role::GenericBlock); - - // Grow our size, shrink the region and save the frame for later. - let size = frame.size(); - let size = match self.axis { - Axis::X => Gen::new(size.y, size.x), - Axis::Y => Gen::new(size.x, size.y), - }; - - self.used.main += size.main; - self.used.cross.set_max(size.cross); - *self.regions.first.get_mut(self.axis) -= size.main; - self.items.push(StackItem::Frame(frame, align)); - - if i + 1 < len { - self.finish_region(); - } - } - - Ok(()) - } - - /// Advance to the next region. - pub fn finish_region(&mut self) { - // Determine the size of the stack in this region dependening on whether - // the region expands. - let used = self.used.to_axes(self.axis); - let mut size = self.expand.select(self.full, used); - - // Expand fully if there are fr spacings. - let full = self.full.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, align) => { - if self.dir.is_positive() { - ruler = ruler.max(align); - } else { - ruler = ruler.min(align); - } - - // Align along the block axis. - let parent = size.get(self.axis); - let child = frame.size().get(self.axis); - let block = ruler.position(parent - self.used.main) - + if self.dir.is_positive() { - cursor - } else { - self.used.main - child - cursor - }; - - let pos = Gen::new(Abs::zero(), block).to_point(self.axis); - cursor += child; - output.push_frame(pos, frame); - } - } - } - - // Advance to the next region. - self.regions.next(); - self.full = self.regions.first; - self.used = Gen::zero(); - self.fr = Fr::zero(); - self.finished.push(output); - } - - /// Finish layouting and return the resulting frames. - pub fn finish(mut self) -> Vec { - self.finish_region(); - self.finished - } -} - -/// A container with a main and cross component. -#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Gen { - /// The main component. - pub cross: T, - /// The cross component. - pub main: T, -} - -impl Gen { - /// Create a new instance from the two components. - pub const fn new(cross: T, main: T) -> Self { - Self { cross, main } - } - - /// Convert to the specific representation, given the current main axis. - pub fn to_axes(self, main: Axis) -> Axes { - match main { - Axis::X => Axes::new(self.main, self.cross), - Axis::Y => Axes::new(self.cross, self.main), - } - } -} - -impl Gen { - /// The zero value. - pub fn zero() -> Self { - Self { cross: Abs::zero(), main: Abs::zero() } - } - - /// Convert to a point. - pub fn to_point(self, main: Axis) -> Point { - self.to_axes(main).to_point() - } -} diff --git a/src/library/layout/transform.rs b/src/library/layout/transform.rs deleted file mode 100644 index a73a1827..00000000 --- a/src/library/layout/transform.rs +++ /dev/null @@ -1,116 +0,0 @@ -use crate::geom::Transform; -use crate::library::prelude::*; - -/// Move content without affecting layout. -#[derive(Debug, Hash)] -pub struct MoveNode { - /// The offset by which to move the content. - pub delta: Axes>, - /// The content that should be moved. - pub child: Content, -} - -#[node(LayoutInline)] -impl MoveNode { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - let dx = args.named("dx")?.unwrap_or_default(); - let dy = args.named("dy")?.unwrap_or_default(); - Ok(Self { - delta: Axes::new(dx, dy), - child: args.expect("body")?, - } - .pack()) - } -} - -impl LayoutInline for MoveNode { - fn layout_inline( - &self, - world: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult> { - let mut frames = self.child.layout_inline(world, regions, styles)?; - - let delta = self.delta.resolve(styles); - for frame in &mut frames { - let delta = delta.zip(frame.size()).map(|(d, s)| d.relative_to(s)); - frame.translate(delta.to_point()); - } - - Ok(frames) - } -} - -/// Transform content without affecting layout. -#[derive(Debug, Hash)] -pub struct TransformNode { - /// Transformation to apply to the content. - pub transform: Transform, - /// The content that should be transformed. - pub child: Content, -} - -/// Rotate content without affecting layout. -pub type RotateNode = TransformNode; - -/// Scale content without affecting layout. -pub type ScaleNode = TransformNode; - -#[node(LayoutInline)] -impl TransformNode { - /// The origin of the transformation. - #[property(resolve)] - pub const ORIGIN: Axes> = Axes::default(); - - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - let transform = match T { - ROTATE => { - let angle = args.named_or_find("angle")?.unwrap_or_default(); - Transform::rotate(angle) - } - SCALE | _ => { - let all = args.find()?; - let sx = args.named("x")?.or(all).unwrap_or(Ratio::one()); - let sy = args.named("y")?.or(all).unwrap_or(Ratio::one()); - Transform::scale(sx, sy) - } - }; - - Ok(Self { transform, child: args.expect("body")? }.pack()) - } -} - -impl LayoutInline for TransformNode { - fn layout_inline( - &self, - world: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult> { - let origin = styles.get(Self::ORIGIN).unwrap_or(Align::CENTER_HORIZON); - let mut frames = self.child.layout_inline(world, regions, styles)?; - - for frame in &mut frames { - let Axes { x, y } = origin.zip(frame.size()).map(|(o, s)| o.position(s)); - let transform = Transform::translate(x, y) - .pre_concat(self.transform) - .pre_concat(Transform::translate(-x, -y)); - - frame.transform(transform); - } - - Ok(frames) - } -} - -/// Kinds of transformations. -/// -/// The move transformation is handled separately. -pub type TransformKind = usize; - -/// A rotational transformation. -const ROTATE: TransformKind = 1; - -/// A scale transformation. -const SCALE: TransformKind = 2; diff --git a/src/library/math/frac.rs b/src/library/math/frac.rs deleted file mode 100644 index 791fd19a..00000000 --- a/src/library/math/frac.rs +++ /dev/null @@ -1,17 +0,0 @@ -use super::*; -use crate::library::prelude::*; - -/// A fraction in a mathematical formula. -#[derive(Debug, Hash)] -pub struct FracNode { - /// The numerator. - pub num: MathNode, - /// The denominator. - pub denom: MathNode, -} - -impl Texify for FracNode { - fn texify(&self) -> EcoString { - format_eco!("\\frac{{{}}}{{{}}}", self.num.texify(), self.denom.texify()) - } -} diff --git a/src/library/math/mod.rs b/src/library/math/mod.rs deleted file mode 100644 index 5bb5054d..00000000 --- a/src/library/math/mod.rs +++ /dev/null @@ -1,295 +0,0 @@ -//! Mathematical formulas. - -mod frac; -mod script; - -pub use frac::*; -pub use script::*; - -use rex::error::{Error, LayoutError}; -use rex::font::FontContext; -use rex::layout::{LayoutSettings, Style}; -use rex::parser::color::RGBA; -use rex::render::{Backend, Cursor, Renderer}; - -use crate::font::Font; -use crate::library::layout::BlockSpacing; -use crate::library::prelude::*; -use crate::library::text::{variant, FontFamily, TextNode}; - -/// A piece of a mathematical formula. -#[derive(Debug, Clone, Hash)] -pub enum MathNode { - /// Whitespace. - Space, - /// A forced line break. - Linebreak, - /// An atom in a math formula: `x`, `+`, `12`. - Atom(EcoString), - /// A base with optional sub and superscripts: `a_1^2`. - Script(Arc), - /// A fraction: `x/2`. - Frac(Arc), - /// A numbered math alignment indicator: `&`, `&&`. - Align(usize), - /// A row of mathematical material. - Row(Arc>, Span), -} - -#[node(Show, LayoutInline)] -impl MathNode { - /// The math font family. - #[property(referenced)] - pub const FAMILY: FontFamily = FontFamily::new("NewComputerModernMath"); - /// The spacing above display math. - #[property(resolve, shorthand(around))] - pub const ABOVE: Option = Some(Ratio::one().into()); - /// The spacing below display math. - #[property(resolve, shorthand(around))] - pub const BELOW: Option = Some(Ratio::one().into()); - - fn construct(_: &mut Vm, _: &mut Args) -> SourceResult { - todo!() - } -} - -impl MathNode { - /// Strip parentheses from the node. - pub fn unparen(self) -> Self { - if let Self::Row(row, span) = &self { - if let [MathNode::Atom(l), .., MathNode::Atom(r)] = row.as_slice() { - if l == "(" && r == ")" { - let inner = row[1 .. row.len() - 1].to_vec(); - return Self::Row(Arc::new(inner), *span); - } - } - } - - self - } - - /// Whether the formula is display level. - pub fn display(&self) -> bool { - if let Self::Row(row, _) = self { - matches!(row.as_slice(), [MathNode::Space, .., MathNode::Space]) - } else { - false - } - } -} - -impl Show for MathNode { - fn unguard_parts(&self, _: Selector) -> Content { - self.clone().pack() - } - - fn field(&self, _: &str) -> Option { - None - } - - fn realize(&self, _: Tracked, _: StyleChain) -> SourceResult { - Ok(if self.display() { - self.clone().pack().aligned(Axes::with_x(Some(Align::Center.into()))) - } else { - self.clone().pack() - }) - } - - fn finalize( - &self, - _: Tracked, - styles: StyleChain, - realized: Content, - ) -> SourceResult { - Ok(if self.display() { - realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW)) - } else { - realized - }) - } -} - -impl LayoutInline for MathNode { - fn layout_inline( - &self, - world: Tracked, - _: &Regions, - styles: StyleChain, - ) -> SourceResult> { - let style = if self.display() { Style::Display } else { Style::Text }; - let span = match self { - &Self::Row(_, span) => span, - _ => Span::detached(), - }; - - Ok(vec![layout_tex(world, self, span, style, styles)?]) - } -} - -/// Layout a TeX formula into a frame. -fn layout_tex( - world: Tracked, - node: &dyn Texify, - span: Span, - style: Style, - styles: StyleChain, -) -> SourceResult { - let tex = node.texify(); - - // Load the font. - let font = world - .book() - .select(styles.get(MathNode::FAMILY).as_str(), variant(styles)) - .and_then(|id| world.font(id)) - .ok_or("failed to find math font") - .at(span)?; - - // Prepare the font context. - let ctx = font - .math() - .map(|math| FontContext::new(font.ttf(), math)) - .ok_or("font is not suitable for math") - .at(span)?; - - // Layout the formula. - let em = styles.get(TextNode::SIZE); - let settings = LayoutSettings::new(&ctx, em.to_pt(), style); - let renderer = Renderer::new(); - let layout = renderer - .layout(&tex, settings) - .map_err(|err| match err { - Error::Parse(err) => err.to_string(), - Error::Layout(LayoutError::Font(err)) => err.to_string(), - }) - .at(span)?; - - // Determine the metrics. - let (x0, y0, x1, y1) = renderer.size(&layout); - let width = Abs::pt(x1 - x0); - let mut top = Abs::pt(y1); - let mut bottom = Abs::pt(-y0); - if style != Style::Display { - let metrics = font.metrics(); - top = styles.get(TextNode::TOP_EDGE).resolve(styles, metrics); - bottom = -styles.get(TextNode::BOTTOM_EDGE).resolve(styles, metrics); - }; - - // Prepare a frame rendering backend. - let size = Size::new(width, top + bottom); - let mut backend = FrameBackend { - frame: { - let mut frame = Frame::new(size); - frame.set_baseline(top); - frame.apply_role(Role::Formula); - frame - }, - baseline: top, - font: font.clone(), - fill: styles.get(TextNode::FILL), - lang: styles.get(TextNode::LANG), - colors: vec![], - }; - - // Render into the frame. - renderer.render(&layout, &mut backend); - Ok(backend.frame) -} - -/// A ReX rendering backend that renders into a frame. -struct FrameBackend { - frame: Frame, - baseline: Abs, - font: Font, - fill: Paint, - lang: Lang, - colors: Vec, -} - -impl FrameBackend { - /// The currently active fill paint. - fn fill(&self) -> Paint { - self.colors - .last() - .map(|&RGBA(r, g, b, a)| RgbaColor::new(r, g, b, a).into()) - .unwrap_or(self.fill) - } - - /// Convert a cursor to a point. - fn transform(&self, cursor: Cursor) -> Point { - Point::new(Abs::pt(cursor.x), self.baseline + Abs::pt(cursor.y)) - } -} - -impl Backend for FrameBackend { - fn symbol(&mut self, pos: Cursor, gid: u16, scale: f64) { - self.frame.push( - self.transform(pos), - Element::Text(Text { - font: self.font.clone(), - size: Abs::pt(scale), - fill: self.fill(), - lang: self.lang, - glyphs: vec![Glyph { - id: gid, - x_advance: Em::new(0.0), - x_offset: Em::new(0.0), - c: ' ', - }], - }), - ); - } - - fn rule(&mut self, pos: Cursor, width: f64, height: f64) { - self.frame.push( - self.transform(pos), - Element::Shape(Shape { - geometry: Geometry::Rect(Size::new(Abs::pt(width), Abs::pt(height))), - fill: Some(self.fill()), - stroke: None, - }), - ); - } - - fn begin_color(&mut self, color: RGBA) { - self.colors.push(color); - } - - fn end_color(&mut self) { - self.colors.pop(); - } -} - -/// Turn a math node into TeX math code. -trait Texify { - /// Perform the conversion. - fn texify(&self) -> EcoString; -} - -impl Texify for MathNode { - fn texify(&self) -> EcoString { - match self { - Self::Space => "".into(), - Self::Linebreak => r"\\".into(), - Self::Atom(atom) => atom.chars().map(escape_char).collect(), - Self::Script(script) => script.texify(), - Self::Frac(frac) => frac.texify(), - Self::Align(_) => "".into(), - Self::Row(row, _) => row.iter().map(Texify::texify).collect(), - } - } -} - -#[rustfmt::skip] -fn escape_char(c: char) -> EcoString { - match c { - '{' | '}' | '%' | '&' | '$' | '#' => format_eco!(" \\{c} "), - 'a' ..= 'z' | 'A' ..= 'Z' | '0' ..= '9' | 'Α' ..= 'Ω' | 'α' ..= 'ω' | - '*' | '+' | '-' | '[' | '(' | ']' | ')' | '?' | '!' | '=' | '<' | '>' | - ':' | ',' | ';' | '|' | '/' | '@' | '.' | '"' => c.into(), - c => unicode_math::SYMBOLS - .iter() - .find(|sym| sym.codepoint == c) - .map(|sym| format_eco!("\\{} ", sym.name)) - .unwrap_or_default(), - } -} diff --git a/src/library/math/script.rs b/src/library/math/script.rs deleted file mode 100644 index 09f52164..00000000 --- a/src/library/math/script.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::fmt::Write; - -use super::*; -use crate::library::prelude::*; - -/// A sub- and/or superscript in a mathematical formula. -#[derive(Debug, Hash)] -pub struct ScriptNode { - /// The base. - pub base: MathNode, - /// The subscript. - pub sub: Option, - /// The superscript. - pub sup: Option, -} - -impl Texify for ScriptNode { - fn texify(&self) -> EcoString { - let mut tex = self.base.texify(); - - if let Some(sub) = &self.sub { - write!(tex, "_{{{}}}", sub.texify()).unwrap(); - } - - if let Some(sup) = &self.sup { - write!(tex, "^{{{}}}", sup.texify()).unwrap(); - } - - tex - } -} diff --git a/src/library/mod.rs b/src/library/mod.rs deleted file mode 100644 index 184c515e..00000000 --- a/src/library/mod.rs +++ /dev/null @@ -1,183 +0,0 @@ -//! The standard library. -//! -//! Call [`scope`] to obtain a [`Scope`] containing all standard library -//! definitions. - -pub mod graphics; -pub mod layout; -pub mod math; -pub mod prelude; -pub mod structure; -pub mod text; -pub mod utility; - -mod ext; -mod raw; - -pub use raw::*; - -use crate::geom::{Align, Color, Dir}; -use crate::model::{Node, Scope}; -use crate::LangItems; - -/// Construct a scope containing all standard library definitions. -pub fn scope() -> Scope { - let mut std = Scope::new(); - - // Text. - std.def_node::("space"); - std.def_node::("linebreak"); - std.def_node::("smartquote"); - std.def_node::("text"); - std.def_node::("par"); - std.def_node::("parbreak"); - std.def_node::("strong"); - std.def_node::("emph"); - std.def_node::("raw"); - std.def_node::("underline"); - std.def_node::("strike"); - std.def_node::("overline"); - std.def_node::("super"); - std.def_node::("sub"); - std.def_node::("link"); - std.def_node::("repeat"); - std.def_fn("lower", text::lower); - std.def_fn("upper", text::upper); - std.def_fn("smallcaps", text::smallcaps); - - // Structure. - std.def_node::("ref"); - std.def_node::("heading"); - std.def_node::("list"); - std.def_node::("enum"); - std.def_node::("desc"); - std.def_node::("table"); - - // Layout. - std.def_node::("page"); - std.def_node::("pagebreak"); - std.def_node::("h"); - std.def_node::("v"); - std.def_node::("box"); - std.def_node::("block"); - std.def_node::("align"); - std.def_node::("pad"); - std.def_node::("stack"); - std.def_node::("grid"); - std.def_node::("columns"); - std.def_node::("colbreak"); - std.def_node::("place"); - std.def_node::("move"); - std.def_node::("scale"); - std.def_node::("rotate"); - - // Graphics. - std.def_node::("image"); - std.def_node::("line"); - std.def_node::("rect"); - std.def_node::("square"); - std.def_node::("ellipse"); - std.def_node::("circle"); - std.def_node::("hide"); - - // Math. - std.def_node::("math"); - std.define("sum", "∑"); - std.define("in", "∈"); - std.define("arrow", "→"); - std.define("NN", "ℕ"); - std.define("RR", "ℝ"); - - // Utility. - std.def_fn("type", utility::type_); - std.def_fn("assert", utility::assert); - std.def_fn("eval", utility::eval); - std.def_fn("int", utility::int); - std.def_fn("float", utility::float); - std.def_fn("abs", utility::abs); - std.def_fn("min", utility::min); - std.def_fn("max", utility::max); - std.def_fn("even", utility::even); - std.def_fn("odd", utility::odd); - std.def_fn("mod", utility::mod_); - std.def_fn("range", utility::range); - std.def_fn("luma", utility::luma); - std.def_fn("rgb", utility::rgb); - std.def_fn("cmyk", utility::cmyk); - std.def_fn("repr", utility::repr); - std.def_fn("str", utility::str); - std.def_fn("regex", utility::regex); - std.def_fn("letter", utility::letter); - std.def_fn("roman", utility::roman); - std.def_fn("symbol", utility::symbol); - std.def_fn("lorem", utility::lorem); - std.def_fn("csv", utility::csv); - std.def_fn("json", utility::json); - std.def_fn("xml", utility::xml); - - // Predefined colors. - std.define("black", Color::BLACK); - std.define("gray", Color::GRAY); - std.define("silver", Color::SILVER); - std.define("white", Color::WHITE); - std.define("navy", Color::NAVY); - std.define("blue", Color::BLUE); - std.define("aqua", Color::AQUA); - std.define("teal", Color::TEAL); - std.define("eastern", Color::EASTERN); - std.define("purple", Color::PURPLE); - std.define("fuchsia", Color::FUCHSIA); - std.define("maroon", Color::MAROON); - std.define("red", Color::RED); - std.define("orange", Color::ORANGE); - std.define("yellow", Color::YELLOW); - std.define("olive", Color::OLIVE); - std.define("green", Color::GREEN); - std.define("lime", Color::LIME); - - // Other constants. - std.define("ltr", Dir::LTR); - std.define("rtl", Dir::RTL); - std.define("ttb", Dir::TTB); - std.define("btt", Dir::BTT); - std.define("start", RawAlign::Start); - std.define("end", RawAlign::End); - std.define("left", RawAlign::Specific(Align::Left)); - std.define("center", RawAlign::Specific(Align::Center)); - std.define("right", RawAlign::Specific(Align::Right)); - std.define("top", RawAlign::Specific(Align::Top)); - std.define("horizon", RawAlign::Specific(Align::Horizon)); - std.define("bottom", RawAlign::Specific(Align::Bottom)); - - std -} - -/// Construct the language map. -pub fn items() -> LangItems { - LangItems { - space: || text::SpaceNode.pack(), - linebreak: |justify| text::LinebreakNode { justify }.pack(), - text: |text| text::TextNode(text).pack(), - smart_quote: |double| text::SmartQuoteNode { double }.pack(), - parbreak: || text::ParbreakNode.pack(), - strong: |body| text::StrongNode(body).pack(), - emph: |body| text::EmphNode(body).pack(), - raw: |text, lang, block| { - let content = text::RawNode { text, block }.pack(); - match lang { - Some(_) => content.styled(text::RawNode::LANG, lang), - None => content, - } - }, - link: |url| text::LinkNode::from_url(url).pack(), - ref_: |target| structure::RefNode(target).pack(), - heading: |level, body| structure::HeadingNode { level, body }.pack(), - list_item: |body| structure::ListItem::List(Box::new(body)).pack(), - enum_item: |number, body| { - structure::ListItem::Enum(number, Box::new(body)).pack() - }, - desc_item: |term, body| { - structure::ListItem::Desc(Box::new(structure::DescItem { term, body })).pack() - }, - } -} diff --git a/src/library/prelude.rs b/src/library/prelude.rs deleted file mode 100644 index 66e35e68..00000000 --- a/src/library/prelude.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! Helpful imports for creating library functionality. - -pub use std::fmt::{self, Debug, Formatter}; -pub use std::hash::Hash; -pub use std::io; -pub use std::num::NonZeroUsize; -pub use std::sync::Arc; - -pub use comemo::Tracked; - -pub use super::ext::{ContentExt, StyleMapExt}; -pub use super::layout::{Layout, LayoutBlock, LayoutInline, Regions}; -pub use super::text::TextNode; -pub use super::{RawAlign, RawStroke}; -pub use crate::diag::{ - with_alternative, At, FileError, FileResult, SourceError, SourceResult, StrResult, -}; -pub use crate::frame::*; -pub use crate::geom::*; -pub use crate::model::{ - capability, node, Arg, Args, Array, Capability, Cast, Content, Dict, Dynamic, Fold, - Func, Key, Node, Resolve, Scope, Selector, Show, Smart, Str, StyleChain, StyleMap, - StyleVec, Value, Vm, -}; -pub use crate::syntax::{Span, Spanned}; -pub use crate::util::EcoString; -pub use crate::{LangItems, World}; diff --git a/src/library/raw.rs b/src/library/raw.rs deleted file mode 100644 index 67aa651d..00000000 --- a/src/library/raw.rs +++ /dev/null @@ -1,149 +0,0 @@ -use std::fmt::{self, Debug, Formatter}; - -use crate::geom::{Abs, Align, Axes, Axis, Get, Length, Paint, Stroke}; -use crate::library::text::TextNode; -use crate::model::{Fold, Resolve, Smart, StyleChain, Value}; - -/// The unresolved alignment representation. -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum RawAlign { - /// 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 Resolve for RawAlign { - type Output = Align; - - fn resolve(self, styles: StyleChain) -> Self::Output { - let dir = styles.get(TextNode::DIR); - match self { - Self::Start => dir.start().into(), - Self::End => dir.end().into(), - Self::Specific(align) => align, - } - } -} - -impl RawAlign { - /// The axis this alignment belongs to. - pub const fn axis(self) -> Axis { - match self { - Self::Start | Self::End => Axis::X, - Self::Specific(align) => align.axis(), - } - } -} - -impl From for RawAlign { - fn from(align: Align) -> Self { - Self::Specific(align) - } -} - -impl Debug for RawAlign { - 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), - } - } -} - -dynamic! { - RawAlign: "alignment", -} - -dynamic! { - Axes: "2d alignment", -} - -castable! { - Axes>, - Expected: "1d or 2d alignment", - @align: RawAlign => { - let mut aligns = Axes::default(); - aligns.set(align.axis(), Some(*align)); - aligns - }, - @aligns: Axes => aligns.map(Some), -} - -/// The unresolved 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, Copy, Clone, Eq, PartialEq, Hash)] -pub struct RawStroke { - /// The stroke's paint. - pub paint: Smart, - /// The stroke's thickness. - pub thickness: Smart, -} - -impl RawStroke { - /// Unpack the stroke, filling missing fields from the `default`. - pub fn unwrap_or(self, default: Stroke) -> Stroke { - Stroke { - paint: self.paint.unwrap_or(default.paint), - thickness: self.thickness.unwrap_or(default.thickness), - } - } - - /// Unpack the stroke, filling missing fields with the default values. - pub fn unwrap_or_default(self) -> Stroke { - self.unwrap_or(Stroke::default()) - } -} - -impl Resolve for RawStroke { - type Output = RawStroke; - - fn resolve(self, styles: StyleChain) -> Self::Output { - RawStroke { - paint: self.paint, - thickness: self.thickness.resolve(styles), - } - } -} - -impl Fold for RawStroke { - type Output = Self; - - fn fold(self, outer: Self::Output) -> Self::Output { - Self { - paint: self.paint.or(outer.paint), - thickness: self.thickness.or(outer.thickness), - } - } -} - -impl Debug for RawStroke { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - 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(""), - } - } -} - -dynamic! { - RawStroke: "stroke", - Value::Length(thickness) => Self { - paint: Smart::Auto, - thickness: Smart::Custom(thickness), - }, - Value::Color(color) => Self { - paint: Smart::Custom(color.into()), - thickness: Smart::Auto, - }, -} diff --git a/src/library/structure/doc.rs b/src/library/structure/doc.rs deleted file mode 100644 index c3af3f1c..00000000 --- a/src/library/structure/doc.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::library::layout::PageNode; -use crate::library::prelude::*; - -/// A sequence of page runs. -#[derive(Hash)] -pub struct DocNode(pub StyleVec); - -impl DocNode { - /// Layout the document into a sequence of frames, one per page. - pub fn layout( - &self, - world: Tracked, - styles: StyleChain, - ) -> SourceResult> { - let mut frames = vec![]; - for (page, map) in self.0.iter() { - let number = 1 + frames.len(); - frames.extend(page.layout(world, number, map.chain(&styles))?); - } - Ok(frames) - } -} - -impl Debug for DocNode { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_str("Doc ")?; - self.0.fmt(f) - } -} diff --git a/src/library/structure/heading.rs b/src/library/structure/heading.rs deleted file mode 100644 index 5b056c30..00000000 --- a/src/library/structure/heading.rs +++ /dev/null @@ -1,176 +0,0 @@ -use crate::library::layout::{BlockNode, BlockSpacing}; -use crate::library::prelude::*; -use crate::library::text::{FontFamily, TextNode, TextSize}; - -/// A section heading. -#[derive(Debug, Hash)] -pub struct HeadingNode { - /// The logical nesting depth of the section, starting from one. In the - /// default style, this controls the text size of the heading. - pub level: NonZeroUsize, - /// The heading's contents. - pub body: Content, -} - -#[node(Show)] -impl HeadingNode { - /// The heading's font family. Just the normal text family if `auto`. - #[property(referenced)] - pub const FAMILY: Leveled> = Leveled::Value(Smart::Auto); - /// The color of text in the heading. Just the normal text color if `auto`. - #[property(referenced)] - pub const FILL: Leveled> = Leveled::Value(Smart::Auto); - /// The size of text in the heading. - #[property(referenced)] - pub const SIZE: Leveled = Leveled::Mapping(|level| { - let size = match level.get() { - 1 => 1.4, - 2 => 1.2, - _ => 1.0, - }; - TextSize(Em::new(size).into()) - }); - - /// Whether text in the heading is strengthend. - #[property(referenced)] - pub const STRONG: Leveled = Leveled::Value(true); - /// Whether text in the heading is emphasized. - #[property(referenced)] - pub const EMPH: Leveled = Leveled::Value(false); - /// Whether the heading is underlined. - #[property(referenced)] - pub const UNDERLINE: Leveled = Leveled::Value(false); - - /// The spacing above the heading. - #[property(referenced, shorthand(around))] - pub const ABOVE: Leveled> = Leveled::Mapping(|level| { - let ratio = match level.get() { - 1 => 1.5, - _ => 1.2, - }; - Some(Ratio::new(ratio).into()) - }); - /// The spacing below the heading. - #[property(referenced, shorthand(around))] - pub const BELOW: Leveled> = - Leveled::Value(Some(Ratio::new(0.55).into())); - - /// Whether the heading appears in the outline. - pub const OUTLINED: bool = true; - /// Whether the heading is numbered. - pub const NUMBERED: bool = true; - - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - Ok(Self { - body: args.expect("body")?, - level: args.named("level")?.unwrap_or(NonZeroUsize::new(1).unwrap()), - } - .pack()) - } -} - -impl Show for HeadingNode { - fn unguard_parts(&self, sel: Selector) -> Content { - Self { body: self.body.unguard(sel), ..*self }.pack() - } - - fn field(&self, name: &str) -> Option { - match name { - "level" => Some(Value::Int(self.level.get() as i64)), - "body" => Some(Value::Content(self.body.clone())), - _ => None, - } - } - - fn realize(&self, _: Tracked, _: StyleChain) -> SourceResult { - Ok(BlockNode(self.body.clone()).pack()) - } - - fn finalize( - &self, - world: Tracked, - styles: StyleChain, - mut realized: Content, - ) -> SourceResult { - macro_rules! resolve { - ($key:expr) => { - styles.get($key).resolve(world, self.level)? - }; - } - - let mut map = StyleMap::new(); - map.set(TextNode::SIZE, resolve!(Self::SIZE)); - - if let Smart::Custom(family) = resolve!(Self::FAMILY) { - map.set_family(family, styles); - } - - if let Smart::Custom(fill) = resolve!(Self::FILL) { - map.set(TextNode::FILL, fill); - } - - if resolve!(Self::STRONG) { - realized = realized.strong(); - } - - if resolve!(Self::EMPH) { - realized = realized.emph(); - } - - if resolve!(Self::UNDERLINE) { - realized = realized.underlined(); - } - - realized = realized.styled_with_map(map); - realized = realized.spaced( - resolve!(Self::ABOVE).resolve(styles), - resolve!(Self::BELOW).resolve(styles), - ); - - Ok(realized) - } -} - -/// Either the value or a closure mapping to the value. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum Leveled { - /// A bare value. - Value(T), - /// A simple mapping from a heading level to a value. - Mapping(fn(NonZeroUsize) -> T), - /// A closure mapping from a heading level to a value. - Func(Func, Span), -} - -impl Leveled { - /// Resolve the value based on the level. - pub fn resolve( - &self, - world: Tracked, - level: NonZeroUsize, - ) -> SourceResult { - Ok(match self { - Self::Value(value) => value.clone(), - Self::Mapping(mapping) => mapping(level), - Self::Func(func, span) => { - let args = Args::new(*span, [Value::Int(level.get() as i64)]); - func.call_detached(world, args)?.cast().at(*span)? - } - }) - } -} - -impl Cast> for Leveled { - fn is(value: &Spanned) -> bool { - matches!(&value.v, Value::Func(_)) || T::is(&value.v) - } - - fn cast(value: Spanned) -> StrResult { - match value.v { - Value::Func(v) => Ok(Self::Func(v, value.span)), - v => T::cast(v) - .map(Self::Value) - .map_err(|msg| with_alternative(msg, "function")), - } - } -} diff --git a/src/library/structure/list.rs b/src/library/structure/list.rs deleted file mode 100644 index f061c5f8..00000000 --- a/src/library/structure/list.rs +++ /dev/null @@ -1,371 +0,0 @@ -use unscanny::Scanner; - -use crate::library::layout::{BlockSpacing, GridNode, HNode, TrackSizing}; -use crate::library::prelude::*; -use crate::library::text::{ParNode, SpaceNode}; -use crate::library::utility::Numbering; - -/// An unordered (bulleted) or ordered (numbered) list. -#[derive(Debug, Hash)] -pub struct ListNode { - /// If true, the items are separated by leading instead of list spacing. - pub tight: bool, - /// If true, the spacing above the list is leading instead of above spacing. - pub attached: bool, - /// The individual bulleted or numbered items. - pub items: StyleVec, -} - -/// An ordered list. -pub type EnumNode = ListNode; - -/// A description list. -pub type DescNode = ListNode; - -#[node(Show)] -impl ListNode { - /// How the list is labelled. - #[property(referenced)] - pub const LABEL: Label = Label::Default; - /// The indentation of each item's label. - #[property(resolve)] - pub const INDENT: Length = Length::zero(); - /// The space between the label and the body of each item. - #[property(resolve)] - pub const BODY_INDENT: Length = Em::new(match L { - LIST | ENUM => 0.5, - DESC | _ => 1.0, - }) - .into(); - - /// The spacing above the list. - #[property(resolve, shorthand(around))] - pub const ABOVE: Option = Some(Ratio::one().into()); - /// The spacing below the list. - #[property(resolve, shorthand(around))] - pub const BELOW: Option = Some(Ratio::one().into()); - /// The spacing between the items of a wide (non-tight) list. - #[property(resolve)] - pub const SPACING: BlockSpacing = Ratio::one().into(); - - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - let items = match L { - LIST => args - .all()? - .into_iter() - .map(|body| ListItem::List(Box::new(body))) - .collect(), - ENUM => { - let mut number: usize = args.named("start")?.unwrap_or(1); - args.all()? - .into_iter() - .map(|body| { - let item = ListItem::Enum(Some(number), Box::new(body)); - number += 1; - item - }) - .collect() - } - DESC | _ => args - .all()? - .into_iter() - .map(|item| ListItem::Desc(Box::new(item))) - .collect(), - }; - - Ok(Self { - tight: args.named("tight")?.unwrap_or(true), - attached: args.named("attached")?.unwrap_or(false), - items, - } - .pack()) - } -} - -impl Show for ListNode { - fn unguard_parts(&self, sel: Selector) -> Content { - Self { - items: self.items.map(|item| item.unguard(sel)), - ..*self - } - .pack() - } - - fn field(&self, name: &str) -> Option { - match name { - "tight" => Some(Value::Bool(self.tight)), - "attached" => Some(Value::Bool(self.attached)), - "items" => Some(Value::Array( - self.items.items().map(|item| item.encode()).collect(), - )), - _ => None, - } - } - - fn realize( - &self, - world: Tracked, - styles: StyleChain, - ) -> SourceResult { - let mut cells = vec![]; - let mut number = 1; - - let label = styles.get(Self::LABEL); - let indent = styles.get(Self::INDENT); - let body_indent = styles.get(Self::BODY_INDENT); - let gutter = if self.tight { - styles.get(ParNode::LEADING) - } else { - styles.get(Self::SPACING) - }; - - for (item, map) in self.items.iter() { - if let &ListItem::Enum(Some(n), _) = item { - number = n; - } - - cells.push(Content::empty()); - - let label = if L == LIST || L == ENUM { - label.resolve(world, L, number)?.styled_with_map(map.clone()) - } else { - Content::empty() - }; - - cells.push(label); - cells.push(Content::empty()); - - let body = match &item { - ListItem::List(body) => body.as_ref().clone(), - ListItem::Enum(_, body) => body.as_ref().clone(), - ListItem::Desc(item) => Content::sequence(vec![ - HNode { - amount: (-body_indent).into(), - weak: false, - } - .pack(), - (item.term.clone() + TextNode(':'.into()).pack()).strong(), - SpaceNode.pack(), - item.body.clone(), - ]), - }; - - cells.push(body.styled_with_map(map.clone())); - number += 1; - } - - Ok(GridNode { - tracks: Axes::with_x(vec![ - TrackSizing::Relative(indent.into()), - TrackSizing::Auto, - TrackSizing::Relative(body_indent.into()), - TrackSizing::Auto, - ]), - gutter: Axes::with_y(vec![TrackSizing::Relative(gutter.into())]), - cells, - } - .pack()) - } - - fn finalize( - &self, - _: Tracked, - styles: StyleChain, - realized: Content, - ) -> SourceResult { - let mut above = styles.get(Self::ABOVE); - let mut below = styles.get(Self::BELOW); - - if self.attached { - if above.is_some() { - above = Some(styles.get(ParNode::LEADING)); - } - if below.is_some() { - below = Some(styles.get(ParNode::SPACING)); - } - } - - Ok(realized.spaced(above, below)) - } -} - -/// An item in a list. -#[derive(Clone, PartialEq, Hash)] -pub enum ListItem { - /// An item of an unordered list. - List(Box), - /// An item of an ordered list. - Enum(Option, Box), - /// An item of a description list. - Desc(Box), -} - -impl ListItem { - /// What kind of item this is. - pub fn kind(&self) -> ListKind { - match self { - Self::List(_) => LIST, - Self::Enum { .. } => ENUM, - Self::Desc { .. } => DESC, - } - } - - fn unguard(&self, sel: Selector) -> Self { - match self { - Self::List(body) => Self::List(Box::new(body.unguard(sel))), - Self::Enum(number, body) => Self::Enum(*number, Box::new(body.unguard(sel))), - Self::Desc(item) => Self::Desc(Box::new(DescItem { - term: item.term.unguard(sel), - body: item.body.unguard(sel), - })), - } - } - - /// Encode the item into a value. - fn encode(&self) -> Value { - match self { - Self::List(body) => Value::Content(body.as_ref().clone()), - Self::Enum(number, body) => Value::Dict(dict! { - "number" => match *number { - Some(n) => Value::Int(n as i64), - None => Value::None, - }, - "body" => Value::Content(body.as_ref().clone()), - }), - Self::Desc(item) => Value::Dict(dict! { - "term" => Value::Content(item.term.clone()), - "body" => Value::Content(item.body.clone()), - }), - } - } -} - -impl Debug for ListItem { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::List(body) => write!(f, "- {body:?}"), - Self::Enum(number, body) => match number { - Some(n) => write!(f, "{n}. {body:?}"), - None => write!(f, "+ {body:?}"), - }, - Self::Desc(item) => item.fmt(f), - } - } -} - -#[node] -impl ListItem {} - -/// A description list item. -#[derive(Clone, PartialEq, Hash)] -pub struct DescItem { - /// The term described by the list item. - pub term: Content, - /// The description of the term. - pub body: Content, -} - -castable! { - DescItem, - Expected: "dictionary with `term` and `body` keys", - Value::Dict(dict) => { - let term: Content = dict.get("term")?.clone().cast()?; - let body: Content = dict.get("body")?.clone().cast()?; - Self { term, body } - }, -} - -impl Debug for DescItem { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "/ {:?}: {:?}", self.term, self.body) - } -} - -/// How to label a list. -pub type ListKind = usize; - -/// An unordered list. -pub const LIST: ListKind = 0; - -/// An ordered list. -pub const ENUM: ListKind = 1; - -/// A description list. -pub const DESC: ListKind = 2; - -/// How to label a list or enumeration. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum Label { - /// The default labelling. - Default, - /// A pattern with prefix, numbering, lower / upper case and suffix. - Pattern(EcoString, Numbering, bool, EcoString), - /// Bare content. - Content(Content), - /// A closure mapping from an item number to a value. - Func(Func, Span), -} - -impl Label { - /// Resolve the value based on the level. - pub fn resolve( - &self, - world: Tracked, - kind: ListKind, - number: usize, - ) -> SourceResult { - Ok(match self { - Self::Default => match kind { - LIST => TextNode('•'.into()).pack(), - ENUM => TextNode(format_eco!("{}.", number)).pack(), - DESC | _ => panic!("description lists don't have a label"), - }, - Self::Pattern(prefix, numbering, upper, suffix) => { - let fmt = numbering.apply(number); - let mid = if *upper { fmt.to_uppercase() } else { fmt.to_lowercase() }; - TextNode(format_eco!("{}{}{}", prefix, mid, suffix)).pack() - } - Self::Content(content) => content.clone(), - Self::Func(func, span) => { - let args = Args::new(*span, [Value::Int(number as i64)]); - func.call_detached(world, args)?.display(world) - } - }) - } -} - -impl Cast> for Label { - fn is(value: &Spanned) -> bool { - matches!(&value.v, Value::Content(_) | Value::Func(_)) - } - - fn cast(value: Spanned) -> StrResult { - match value.v { - Value::None => Ok(Self::Content(Content::empty())), - Value::Str(pattern) => { - let mut s = Scanner::new(&pattern); - let mut prefix; - let numbering = loop { - prefix = s.before(); - match s.eat().map(|c| c.to_ascii_lowercase()) { - Some('1') => break Numbering::Arabic, - Some('a') => break Numbering::Letter, - Some('i') => break Numbering::Roman, - Some('*') => break Numbering::Symbol, - Some(_) => {} - None => Err("invalid pattern")?, - } - }; - let upper = s.scout(-1).map_or(false, char::is_uppercase); - let suffix = s.after().into(); - Ok(Self::Pattern(prefix.into(), numbering, upper, suffix)) - } - Value::Content(v) => Ok(Self::Content(v)), - Value::Func(v) => Ok(Self::Func(v, value.span)), - v => Err(format!( - "expected string, content or function, found {}", - v.type_name(), - )), - } - } -} diff --git a/src/library/structure/mod.rs b/src/library/structure/mod.rs deleted file mode 100644 index 088d1e6c..00000000 --- a/src/library/structure/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Document structuring. - -mod doc; -mod heading; -mod list; -mod reference; -mod table; - -pub use doc::*; -pub use heading::*; -pub use list::*; -pub use reference::*; -pub use table::*; diff --git a/src/library/structure/reference.rs b/src/library/structure/reference.rs deleted file mode 100644 index b4e8b047..00000000 --- a/src/library/structure/reference.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::library::prelude::*; - -/// A reference to a label. -#[derive(Debug, Hash)] -pub struct RefNode(pub EcoString); - -#[node(Show)] -impl RefNode { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - Ok(Self(args.expect("label")?).pack()) - } -} - -impl Show for RefNode { - fn unguard_parts(&self, _: Selector) -> Content { - Self(self.0.clone()).pack() - } - - fn field(&self, name: &str) -> Option { - match name { - "label" => Some(Value::Str(self.0.clone().into())), - _ => None, - } - } - - fn realize(&self, _: Tracked, _: StyleChain) -> SourceResult { - Ok(TextNode(format_eco!("@{}", self.0)).pack()) - } -} diff --git a/src/library/structure/table.rs b/src/library/structure/table.rs deleted file mode 100644 index 8a4eb302..00000000 --- a/src/library/structure/table.rs +++ /dev/null @@ -1,160 +0,0 @@ -use crate::library::layout::{BlockSpacing, GridNode, TrackSizing}; -use crate::library::prelude::*; - -/// A table of items. -#[derive(Debug, Hash)] -pub struct TableNode { - /// Defines sizing for content rows and columns. - pub tracks: Axes>, - /// Defines sizing of gutter rows and columns between content. - pub gutter: Axes>, - /// The content to be arranged in the table. - pub cells: Vec, -} - -#[node(Show)] -impl TableNode { - /// How to fill the cells. - #[property(referenced)] - pub const FILL: Celled> = Celled::Value(None); - /// How to stroke the cells. - #[property(resolve, fold)] - pub const STROKE: Option = Some(RawStroke::default()); - /// How much to pad the cells's content. - pub const PADDING: Rel = Abs::pt(5.0).into(); - - /// The spacing above the table. - #[property(resolve, shorthand(around))] - pub const ABOVE: Option = Some(Ratio::one().into()); - /// The spacing below the table. - #[property(resolve, shorthand(around))] - pub const BELOW: Option = Some(Ratio::one().into()); - - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - let columns = args.named("columns")?.unwrap_or_default(); - let rows = args.named("rows")?.unwrap_or_default(); - let base_gutter: Vec = args.named("gutter")?.unwrap_or_default(); - let column_gutter = args.named("column-gutter")?; - let row_gutter = args.named("row-gutter")?; - Ok(Self { - tracks: Axes::new(columns, rows), - gutter: Axes::new( - column_gutter.unwrap_or_else(|| base_gutter.clone()), - row_gutter.unwrap_or(base_gutter), - ), - cells: args.all()?, - } - .pack()) - } -} - -impl Show for TableNode { - fn unguard_parts(&self, sel: Selector) -> Content { - Self { - tracks: self.tracks.clone(), - gutter: self.gutter.clone(), - cells: self.cells.iter().map(|cell| cell.unguard(sel)).collect(), - } - .pack() - } - - fn field(&self, name: &str) -> Option { - match name { - "cells" => Some(Value::Array( - self.cells.iter().cloned().map(Value::Content).collect(), - )), - _ => None, - } - } - - fn realize( - &self, - world: Tracked, - styles: StyleChain, - ) -> SourceResult { - let fill = styles.get(Self::FILL); - let stroke = styles.get(Self::STROKE).map(RawStroke::unwrap_or_default); - let padding = styles.get(Self::PADDING); - - let cols = self.tracks.x.len().max(1); - let cells = self - .cells - .iter() - .cloned() - .enumerate() - .map(|(i, child)| { - let mut child = child.padded(Sides::splat(padding)); - - if let Some(stroke) = stroke { - child = child.stroked(stroke); - } - - let x = i % cols; - let y = i / cols; - if let Some(fill) = fill.resolve(world, x, y)? { - child = child.filled(fill); - } - - Ok(child) - }) - .collect::>()?; - - Ok(GridNode { - tracks: self.tracks.clone(), - gutter: self.gutter.clone(), - cells, - } - .pack()) - } - - fn finalize( - &self, - _: Tracked, - styles: StyleChain, - realized: Content, - ) -> SourceResult { - Ok(realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW))) - } -} - -/// A value that can be configured per cell. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum Celled { - /// A bare value, the same for all cells. - Value(T), - /// A closure mapping from cell coordinates to a value. - Func(Func, Span), -} - -impl Celled { - /// Resolve the value based on the cell position. - pub fn resolve( - &self, - world: Tracked, - x: usize, - y: usize, - ) -> SourceResult { - Ok(match self { - Self::Value(value) => value.clone(), - Self::Func(func, span) => { - let args = Args::new(*span, [Value::Int(x as i64), Value::Int(y as i64)]); - func.call_detached(world, args)?.cast().at(*span)? - } - }) - } -} - -impl Cast> for Celled { - fn is(value: &Spanned) -> bool { - matches!(&value.v, Value::Func(_)) || T::is(&value.v) - } - - fn cast(value: Spanned) -> StrResult { - match value.v { - Value::Func(v) => Ok(Self::Func(v, value.span)), - v => T::cast(v) - .map(Self::Value) - .map_err(|msg| with_alternative(msg, "function")), - } - } -} diff --git a/src/library/text/deco.rs b/src/library/text/deco.rs deleted file mode 100644 index 158647f2..00000000 --- a/src/library/text/deco.rs +++ /dev/null @@ -1,249 +0,0 @@ -use kurbo::{BezPath, Line, ParamCurve}; -use ttf_parser::{GlyphId, OutlineBuilder}; - -use super::TextNode; -use crate::library::prelude::*; - -/// Typeset underline, stricken-through or overlined text. -#[derive(Debug, Hash)] -pub struct DecoNode(pub Content); - -/// Typeset underlined text. -pub type UnderlineNode = DecoNode; - -/// Typeset stricken-through text. -pub type StrikethroughNode = DecoNode; - -/// Typeset overlined text. -pub type OverlineNode = DecoNode; - -#[node(Show)] -impl DecoNode { - /// How to stroke the line. The text color and thickness are read from the - /// font tables if `auto`. - #[property(shorthand, resolve, fold)] - pub const STROKE: Smart = Smart::Auto; - /// Position of the line relative to the baseline, read from the font tables - /// if `auto`. - #[property(resolve)] - pub const OFFSET: Smart = Smart::Auto; - /// Amount that the line will be longer or shorter than its associated text. - #[property(resolve)] - pub const EXTENT: Length = Length::zero(); - /// Whether the line skips sections in which it would collide - /// with the glyphs. Does not apply to strikethrough. - pub const EVADE: bool = true; - - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - Ok(Self(args.expect("body")?).pack()) - } -} - -impl Show for DecoNode { - fn unguard_parts(&self, sel: Selector) -> Content { - Self(self.0.unguard(sel)).pack() - } - - fn field(&self, name: &str) -> Option { - match name { - "body" => Some(Value::Content(self.0.clone())), - _ => None, - } - } - - fn realize( - &self, - _: Tracked, - styles: StyleChain, - ) -> SourceResult { - Ok(self.0.clone().styled(TextNode::DECO, Decoration { - line: L, - stroke: styles.get(Self::STROKE).unwrap_or_default(), - offset: styles.get(Self::OFFSET), - extent: styles.get(Self::EXTENT), - evade: styles.get(Self::EVADE), - })) - } -} - -/// Defines a line that is positioned over, under or on top of text. -/// -/// For more details, see [`DecoNode`]. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct Decoration { - pub line: DecoLine, - pub stroke: RawStroke, - pub offset: Smart, - pub extent: Abs, - pub evade: bool, -} - -/// A kind of decorative line. -pub type DecoLine = usize; - -/// A line under text. -pub const UNDERLINE: DecoLine = 0; - -/// A line through text. -pub const STRIKETHROUGH: DecoLine = 1; - -/// A line over text. -pub const OVERLINE: DecoLine = 2; - -/// Add line decorations to a single run of shaped text. -pub fn decorate( - frame: &mut Frame, - deco: &Decoration, - text: &Text, - shift: Abs, - pos: Point, - width: Abs, -) { - let font_metrics = text.font.metrics(); - let metrics = match deco.line { - STRIKETHROUGH => font_metrics.strikethrough, - OVERLINE => font_metrics.overline, - UNDERLINE | _ => font_metrics.underline, - }; - - let evade = deco.evade && deco.line != STRIKETHROUGH; - let offset = deco.offset.unwrap_or(-metrics.position.at(text.size)) - shift; - let stroke = deco.stroke.unwrap_or(Stroke { - paint: text.fill, - thickness: metrics.thickness.at(text.size), - }); - - let gap_padding = 0.08 * text.size; - let min_width = 0.162 * text.size; - - let mut 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 || !evade { - let shape = Geometry::Line(target).stroked(stroke); - frame.push(origin, Element::Shape(shape)); - } - }; - - if !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. - if 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 - }) { - // 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)), - ); - } - } - - // When emitting the decorative line segments, we move from left to - // right. The intersections are not necessarily in this order, yet. - intersections.sort(); - - for gap in intersections.chunks_exact(2) { - let l = gap[0] - gap_padding; - let r = gap[1] + gap_padding; - - if start >= end { - break; - } - - if start >= l { - start = r; - continue; - } - - push_segment(start, l); - start = r; - } - - if start < end { - push_segment(start, end); - } -} - -/// 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/src/library/text/link.rs b/src/library/text/link.rs deleted file mode 100644 index 1e9adc3e..00000000 --- a/src/library/text/link.rs +++ /dev/null @@ -1,114 +0,0 @@ -use super::TextNode; -use crate::library::prelude::*; - -/// Link text and other elements to a destination. -#[derive(Debug, Hash)] -pub struct LinkNode { - /// The destination the link points to. - pub dest: Destination, - /// How the link is represented. - pub body: Option, -} - -impl LinkNode { - /// Create a link node from a URL with its bare text. - pub fn from_url(url: EcoString) -> Self { - Self { dest: Destination::Url(url), body: None } - } -} - -#[node(Show)] -impl LinkNode { - /// The fill color of text in the link. Just the surrounding text color - /// if `auto`. - pub const FILL: Smart = Smart::Auto; - /// Whether to underline the link. - pub const UNDERLINE: Smart = Smart::Auto; - - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - let dest = args.expect::("destination")?; - let body = match dest { - Destination::Url(_) => args.eat()?, - Destination::Internal(_) => Some(args.expect("body")?), - }; - Ok(Self { dest, body }.pack()) - } -} - -castable! { - Destination, - Expected: "string or dictionary with `page`, `x`, and `y` keys", - Value::Str(string) => Self::Url(string.into()), - Value::Dict(dict) => { - let page = dict.get("page")?.clone().cast()?; - let x: Length = dict.get("x")?.clone().cast()?; - let y: Length = dict.get("y")?.clone().cast()?; - Self::Internal(Location { page, pos: Point::new(x.abs, y.abs) }) - }, -} - -impl Show for LinkNode { - fn unguard_parts(&self, sel: Selector) -> Content { - Self { - dest: self.dest.clone(), - body: self.body.as_ref().map(|body| body.unguard(sel)), - } - .pack() - } - - fn field(&self, name: &str) -> Option { - match name { - "url" => Some(match &self.dest { - Destination::Url(url) => Value::Str(url.clone().into()), - Destination::Internal(loc) => Value::Dict(loc.encode()), - }), - "body" => Some(match &self.body { - Some(body) => Value::Content(body.clone()), - None => Value::None, - }), - _ => None, - } - } - - fn realize(&self, _: Tracked, _: StyleChain) -> SourceResult { - Ok(self - .body - .clone() - .unwrap_or_else(|| match &self.dest { - Destination::Url(url) => { - let mut text = url.as_str(); - for prefix in ["mailto:", "tel:"] { - text = text.trim_start_matches(prefix); - } - let shorter = text.len() < url.len(); - TextNode(if shorter { text.into() } else { url.clone() }).pack() - } - Destination::Internal(_) => Content::empty(), - }) - .styled(TextNode::LINK, Some(self.dest.clone()))) - } - - fn finalize( - &self, - _: Tracked, - styles: StyleChain, - mut realized: Content, - ) -> SourceResult { - let mut map = StyleMap::new(); - if let Smart::Custom(fill) = styles.get(Self::FILL) { - map.set(TextNode::FILL, fill); - } - - if match styles.get(Self::UNDERLINE) { - Smart::Auto => match &self.dest { - Destination::Url(_) => true, - Destination::Internal(_) => false, - }, - Smart::Custom(underline) => underline, - } { - realized = realized.underlined(); - } - - Ok(realized.styled_with_map(map)) - } -} diff --git a/src/library/text/mod.rs b/src/library/text/mod.rs deleted file mode 100644 index 18e747d0..00000000 --- a/src/library/text/mod.rs +++ /dev/null @@ -1,593 +0,0 @@ -//! Text handling and paragraph layout. - -mod deco; -mod link; -mod par; -mod quotes; -mod raw; -mod shaping; -mod shift; - -pub use deco::*; -pub use link::*; -pub use par::*; -pub use quotes::*; -pub use raw::*; -pub use shaping::*; -pub use shift::*; - -use std::borrow::Cow; - -use rustybuzz::Tag; - -use crate::font::{FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontMetric}; -use crate::library::prelude::*; -use crate::util::EcoString; - -/// A single run of text with the same style. -#[derive(Debug, Clone, Hash)] -pub struct TextNode(pub EcoString); - -#[node] -impl TextNode { - /// A prioritized sequence of font families. - #[property(skip, referenced)] - pub const FAMILY: Vec = vec![FontFamily::new("IBM Plex Sans")]; - /// Whether to allow font fallback when the primary font list contains no - /// match. - pub const FALLBACK: bool = true; - - /// How the font is styled. - pub const STYLE: FontStyle = FontStyle::Normal; - /// The boldness / thickness of the font's glyphs. - pub const WEIGHT: FontWeight = FontWeight::REGULAR; - /// The width of the glyphs. - pub const STRETCH: FontStretch = FontStretch::NORMAL; - - /// The size of the glyphs. - #[property(shorthand, fold)] - pub const SIZE: TextSize = Abs::pt(11.0); - /// The glyph fill color. - #[property(shorthand)] - pub const FILL: Paint = Color::BLACK.into(); - /// The amount of space that should be added between characters. - #[property(resolve)] - pub const TRACKING: Length = Length::zero(); - /// The width of spaces relative to the font's space width. - #[property(resolve)] - pub const SPACING: Rel = Rel::one(); - /// The offset of the baseline. - #[property(resolve)] - pub const BASELINE: Length = Length::zero(); - /// Whether certain glyphs can hang over into the margin. - pub const OVERHANG: bool = true; - /// The top end of the text bounding box. - pub const TOP_EDGE: TextEdge = TextEdge::Metric(VerticalFontMetric::CapHeight); - /// The bottom end of the text bounding box. - pub const BOTTOM_EDGE: TextEdge = TextEdge::Metric(VerticalFontMetric::Baseline); - - /// An ISO 639-1/2/3 language code. - pub const LANG: Lang = Lang::ENGLISH; - /// An ISO 3166-1 alpha-2 region code. - pub const REGION: Option = None; - /// The direction for text and inline objects. When `auto`, the direction is - /// automatically inferred from the language. - #[property(resolve)] - pub const DIR: Smart = Smart::Auto; - /// Whether to hyphenate text to improve line breaking. When `auto`, words - /// will will be hyphenated if and only if justification is enabled. - #[property(resolve)] - pub const HYPHENATE: Smart = Smart::Auto; - /// Whether to apply smart quotes. - pub const SMART_QUOTES: bool = true; - - /// Whether to apply kerning ("kern"). - pub const KERNING: bool = true; - /// Whether to apply stylistic alternates. ("salt") - pub const ALTERNATES: bool = false; - /// Which stylistic set to apply. ("ss01" - "ss20") - pub const STYLISTIC_SET: Option = None; - /// Whether standard ligatures are active. ("liga", "clig") - pub const LIGATURES: bool = true; - /// Whether ligatures that should be used sparingly are active. ("dlig") - pub const DISCRETIONARY_LIGATURES: bool = false; - /// Whether historical ligatures are active. ("hlig") - pub const HISTORICAL_LIGATURES: bool = false; - /// Which kind of numbers / figures to select. - pub const NUMBER_TYPE: Smart = Smart::Auto; - /// The width of numbers / figures. - pub const NUMBER_WIDTH: Smart = Smart::Auto; - /// Whether to have a slash through the zero glyph. ("zero") - pub const SLASHED_ZERO: bool = false; - /// Whether to convert fractions. ("frac") - pub const FRACTIONS: bool = false; - /// Raw OpenType features to apply. - #[property(fold)] - pub const FEATURES: Vec<(Tag, u32)> = vec![]; - - /// Whether the font weight should be increased by 300. - #[property(skip, fold)] - pub const BOLD: Toggle = false; - /// Whether the font style should be inverted. - #[property(skip, fold)] - pub const ITALIC: Toggle = false; - /// A case transformation that should be applied to the text. - #[property(skip)] - pub const CASE: Option = None; - /// Whether small capital glyphs should be used. ("smcp") - #[property(skip)] - pub const SMALLCAPS: bool = false; - /// A destination the text should be linked to. - #[property(skip, referenced)] - pub const LINK: Option = None; - /// Decorative lines. - #[property(skip, fold)] - pub const DECO: Decoration = vec![]; - - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - // The text constructor is special: It doesn't create a text node. - // Instead, it leaves the passed argument structurally unchanged, but - // styles all text in it. - args.expect("body") - } - - fn set(...) { - if let Some(family) = args.named("family")? { - styles.set(Self::FAMILY, family); - } else { - let mut count = 0; - let mut content = false; - for item in args.items.iter().filter(|item| item.name.is_none()) { - if EcoString::is(&item.value) { - count += 1; - } else if >>::is(&item.value) { - content = true; - } - } - - // Skip the final string if it's needed as the body. - if constructor && !content && count > 0 { - count -= 1; - } - - if count > 0 { - let mut list = Vec::with_capacity(count); - for _ in 0 .. count { - list.push(args.find()?.unwrap()); - } - - styles.set(Self::FAMILY, list); - } - } - } -} - -/// 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) - } -} - -castable! { - FontFamily, - Expected: "string", - Value::Str(string) => Self::new(&string), -} - -castable! { - Vec, - Expected: "string or array of strings", - Value::Str(string) => vec![FontFamily::new(&string)], - Value::Array(values) => values - .into_iter() - .filter_map(|v| v.cast().ok()) - .map(|string: EcoString| FontFamily::new(&string)) - .collect(), -} - -castable! { - FontStyle, - Expected: "string", - Value::Str(string) => match string.as_str() { - "normal" => Self::Normal, - "italic" => Self::Italic, - "oblique" => Self::Oblique, - _ => Err(r#"expected "normal", "italic" or "oblique""#)?, - }, -} - -castable! { - FontWeight, - Expected: "integer or string", - Value::Int(v) => Value::Int(v) - .cast::()? - .try_into() - .map_or(Self::BLACK, Self::from_number), - Value::Str(string) => match string.as_str() { - "thin" => Self::THIN, - "extralight" => Self::EXTRALIGHT, - "light" => Self::LIGHT, - "regular" => Self::REGULAR, - "medium" => Self::MEDIUM, - "semibold" => Self::SEMIBOLD, - "bold" => Self::BOLD, - "extrabold" => Self::EXTRABOLD, - "black" => Self::BLACK, - _ => Err("unknown font weight")?, - }, -} - -castable! { - FontStretch, - Expected: "ratio", - Value::Ratio(v) => Self::from_ratio(v.get() as f32), -} - -/// 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 - } -} - -castable!(TextSize: Length); - -/// 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), - } - } -} - -castable! { - TextEdge, - Expected: "string or length", - Value::Length(v) => Self::Length(v), - Value::Str(string) => Self::Metric(match string.as_str() { - "ascender" => VerticalFontMetric::Ascender, - "cap-height" => VerticalFontMetric::CapHeight, - "x-height" => VerticalFontMetric::XHeight, - "baseline" => VerticalFontMetric::Baseline, - "descender" => VerticalFontMetric::Descender, - _ => Err("unknown font metric")?, - }), -} - -castable! { - Lang, - Expected: "string", - Value::Str(string) => Self::from_str(&string) - .ok_or("expected two or three letter language code (ISO 639-1/2/3)")?, -} - -castable! { - Region, - Expected: "string", - Value::Str(string) => Self::from_str(&string) - .ok_or("expected two letter region code (ISO 3166-1 alpha-2)")?, -} - -/// The direction of text and inline objects in their line. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct HorizontalDir(pub Dir); - -castable! { - HorizontalDir, - Expected: "direction", - @dir: Dir => match dir.axis() { - Axis::X => Self(*dir), - Axis::Y => Err("must be horizontal")?, - }, -} - -impl Resolve for Smart { - type Output = Dir; - - fn resolve(self, styles: StyleChain) -> Self::Output { - match self { - Smart::Auto => styles.get(TextNode::LANG).dir(), - Smart::Custom(dir) => dir.0, - } - } -} - -/// Whether to hyphenate text. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Hyphenate(pub bool); - -castable!(Hyphenate: bool); - -impl Resolve for Smart { - type Output = bool; - - fn resolve(self, styles: StyleChain) -> Self::Output { - match self { - Smart::Auto => styles.get(ParNode::JUSTIFY), - Smart::Custom(v) => v.0, - } - } -} - -/// 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 - } -} - -castable! { - StylisticSet, - Expected: "integer", - Value::Int(v) => match v { - 1 ..= 20 => Self::new(v as u8), - _ => Err("must be between 1 and 20")?, - }, -} - -/// Which kind of numbers / figures to select. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum NumberType { - /// Numbers that fit well with capital text. ("lnum") - Lining, - /// Numbers that fit well into a flow of upper- and lowercase text. ("onum") - OldStyle, -} - -castable! { - NumberType, - Expected: "string", - Value::Str(string) => match string.as_str() { - "lining" => Self::Lining, - "old-style" => Self::OldStyle, - _ => Err(r#"expected "lining" or "old-style""#)?, - }, -} - -/// The width of numbers / figures. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum NumberWidth { - /// Number widths are glyph specific. ("pnum") - Proportional, - /// All numbers are of equal width / monospaced. ("tnum") - Tabular, -} - -castable! { - NumberWidth, - Expected: "string", - Value::Str(string) => match string.as_str() { - "proportional" => Self::Proportional, - "tabular" => Self::Tabular, - _ => Err(r#"expected "proportional" or "tabular""#)?, - }, -} - -castable! { - Vec<(Tag, u32)>, - Expected: "array of strings or dictionary mapping tags to integers", - Value::Array(values) => values - .into_iter() - .filter_map(|v| v.cast().ok()) - .map(|string: EcoString| (Tag::from_bytes_lossy(string.as_bytes()), 1)) - .collect(), - Value::Dict(values) => values - .into_iter() - .filter_map(|(k, v)| { - let tag = Tag::from_bytes_lossy(k.as_bytes()); - let num = v.cast::().ok()?.try_into().ok()?; - Some((tag, num)) - }) - .collect(), -} - -impl Fold for Vec<(Tag, u32)> { - type Output = Self; - - fn fold(mut self, outer: Self::Output) -> Self::Output { - self.extend(outer); - self - } -} - -/// A text space. -#[derive(Debug, Clone, Hash)] -pub struct SpaceNode; - -#[node] -impl SpaceNode { - fn construct(_: &mut Vm, _: &mut Args) -> SourceResult { - Ok(Self.pack()) - } -} - -/// A line break. -#[derive(Debug, Clone, Hash)] -pub struct LinebreakNode { - pub justify: bool, -} - -#[node] -impl LinebreakNode { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - let justify = args.named("justify")?.unwrap_or(false); - Ok(Self { justify }.pack()) - } -} - -/// A smart quote. -#[derive(Debug, Clone, Hash)] -pub struct SmartQuoteNode { - pub double: bool, -} - -#[node] -impl SmartQuoteNode { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - let double = args.named("double")?.unwrap_or(true); - Ok(Self { double }.pack()) - } -} - -/// Convert a string or content to lowercase. -pub fn lower(_: &mut Vm, args: &mut Args) -> SourceResult { - case(Case::Lower, args) -} - -/// Convert a string or content to uppercase. -pub fn upper(_: &mut Vm, args: &mut Args) -> SourceResult { - case(Case::Upper, args) -} - -/// Change the case of text. -fn case(case: Case, args: &mut Args) -> SourceResult { - let Spanned { v, span } = args.expect("string or content")?; - Ok(match v { - Value::Str(v) => Value::Str(case.apply(&v).into()), - Value::Content(v) => Value::Content(v.styled(TextNode::CASE, Some(case))), - v => bail!(span, "expected string or content, found {}", v.type_name()), - }) -} - -/// A case transformation on text. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Case { - /// Everything is uppercased. - Upper, - /// Everything is lowercased. - Lower, -} - -impl Case { - /// Apply the case to a string. - pub fn apply(self, text: &str) -> String { - match self { - Self::Upper => text.to_uppercase(), - Self::Lower => text.to_lowercase(), - } - } -} - -/// Display text in small capitals. -pub fn smallcaps(_: &mut Vm, args: &mut Args) -> SourceResult { - let body: Content = args.expect("content")?; - Ok(Value::Content(body.styled(TextNode::SMALLCAPS, true))) -} - -/// Strong text, rendered in boldface by default. -#[derive(Debug, Hash)] -pub struct StrongNode(pub Content); - -#[node(Show)] -impl StrongNode { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - Ok(Self(args.expect("body")?).pack()) - } -} - -impl Show for StrongNode { - fn unguard_parts(&self, sel: Selector) -> Content { - Self(self.0.unguard(sel)).pack() - } - - fn field(&self, name: &str) -> Option { - match name { - "body" => Some(Value::Content(self.0.clone())), - _ => None, - } - } - - fn realize(&self, _: Tracked, _: StyleChain) -> SourceResult { - Ok(self.0.clone().styled(TextNode::BOLD, Toggle)) - } -} - -/// Emphasized text, rendered with an italic font by default. -#[derive(Debug, Hash)] -pub struct EmphNode(pub Content); - -#[node(Show)] -impl EmphNode { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - Ok(Self(args.expect("body")?).pack()) - } -} - -impl Show for EmphNode { - fn unguard_parts(&self, sel: Selector) -> Content { - Self(self.0.unguard(sel)).pack() - } - - fn field(&self, name: &str) -> Option { - match name { - "body" => Some(Value::Content(self.0.clone())), - _ => None, - } - } - - fn realize(&self, _: Tracked, _: StyleChain) -> SourceResult { - Ok(self.0.clone().styled(TextNode::ITALIC, Toggle)) - } -} - -/// A toggle that turns on and off alternatingly if folded. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Toggle; - -impl Fold for Toggle { - type Output = bool; - - fn fold(self, outer: Self::Output) -> Self::Output { - !outer - } -} - -impl Fold for Decoration { - type Output = Vec; - - fn fold(self, mut outer: Self::Output) -> Self::Output { - outer.insert(0, self); - outer - } -} diff --git a/src/library/text/par.rs b/src/library/text/par.rs deleted file mode 100644 index 50089b20..00000000 --- a/src/library/text/par.rs +++ /dev/null @@ -1,1266 +0,0 @@ -use std::cmp::Ordering; - -use unicode_bidi::{BidiInfo, Level as BidiLevel}; -use unicode_script::{Script, UnicodeScript}; -use xi_unicode::LineBreakIterator; - -use super::{shape, Lang, Quoter, Quotes, ShapedText, TextNode}; -use crate::library::layout::Spacing; -use crate::library::prelude::*; -use crate::util::EcoString; - -/// Arrange text, spacing and inline-level nodes into a paragraph. -#[derive(Hash)] -pub struct ParNode(pub StyleVec); - -/// A uniformly styled atomic piece of a paragraph. -#[derive(Hash, PartialEq)] -pub enum ParChild { - /// A chunk of text. - Text(EcoString), - /// A single or double smart quote. - Quote { double: bool }, - /// Horizontal spacing between other children. - Spacing(Spacing), - /// Arbitrary inline-level content. - Inline(Content), -} - -#[node(LayoutBlock)] -impl ParNode { - /// The spacing between lines. - #[property(resolve)] - pub const LEADING: Length = Em::new(0.65).into(); - /// The extra spacing between paragraphs. - #[property(resolve)] - pub const SPACING: Length = Em::new(1.2).into(); - /// The indent the first line of a consecutive paragraph should have. - #[property(resolve)] - pub const INDENT: Length = Length::zero(); - /// Whether to allow paragraph spacing when there is paragraph indent. - pub const SPACING_AND_INDENT: bool = false; - - /// How to align text and inline objects in their line. - #[property(resolve)] - pub const ALIGN: HorizontalAlign = HorizontalAlign(RawAlign::Start); - /// Whether to justify text in its line. - pub const JUSTIFY: bool = false; - /// How to determine line breaks. - #[property(resolve)] - pub const LINEBREAKS: Smart = Smart::Auto; - - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - // The paragraph constructor is special: It doesn't create a paragraph - // node. Instead, it just ensures that the passed content lives is in a - // separate paragraph and styles it. - Ok(Content::sequence(vec![ - ParbreakNode.pack(), - args.expect("body")?, - ParbreakNode.pack(), - ])) - } -} - -impl LayoutBlock for ParNode { - fn layout_block( - &self, - world: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult> { - // Collect all text into one string for BiDi analysis. - let (text, segments) = collect(self, &styles); - - // 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(world, self, &text, segments, regions, styles)?; - - // Break the paragraph into lines. - let lines = linebreak(&p, world, regions.first.x); - - // Stack the lines into one frame per region. - stack(&p, world, &lines, regions) - } -} - -impl Debug for ParNode { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_str("Par ")?; - self.0.fmt(f) - } -} - -impl Debug for ParChild { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Text(text) => write!(f, "Text({:?})", text), - Self::Quote { double } => write!(f, "Quote({double})"), - Self::Spacing(kind) => write!(f, "{:?}", kind), - Self::Inline(inline) => inline.fmt(f), - } - } -} - -impl PartialOrd for ParChild { - fn partial_cmp(&self, other: &Self) -> Option { - match (self, other) { - (Self::Spacing(a), Self::Spacing(b)) => a.partial_cmp(b), - _ => None, - } - } -} - -/// A horizontal alignment. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct HorizontalAlign(pub RawAlign); - -castable! { - HorizontalAlign, - Expected: "alignment", - @align: RawAlign => match align.axis() { - Axis::X => Self(*align), - Axis::Y => Err("must be horizontal")?, - }, -} - -impl Resolve for HorizontalAlign { - type Output = Align; - - fn resolve(self, styles: StyleChain) -> Self::Output { - self.0.resolve(styles) - } -} - -/// How to determine line breaks in a paragraph. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Linebreaks { - /// Determine the linebreaks in a simple first-fit style. - Simple, - /// Optimize the linebreaks for the whole paragraph. - Optimized, -} - -castable! { - Linebreaks, - Expected: "string", - Value::Str(string) => match string.as_str() { - "simple" => Self::Simple, - "optimized" => Self::Optimized, - _ => Err(r#"expected "simple" or "optimized""#)?, - }, -} - -impl Resolve for Smart { - type Output = Linebreaks; - - fn resolve(self, styles: StyleChain) -> Self::Output { - self.unwrap_or_else(|| { - if styles.get(ParNode::JUSTIFY) { - Linebreaks::Optimized - } else { - Linebreaks::Simple - } - }) - } -} - -/// A paragraph break. -#[derive(Debug, Clone, Hash)] -pub struct ParbreakNode; - -#[node] -impl ParbreakNode { - fn construct(_: &mut Vm, _: &mut Args) -> SourceResult { - Ok(Self.pack()) - } -} - -/// Repeats content to fill a line. -#[derive(Debug, Hash)] -pub struct RepeatNode(pub Content); - -#[node(LayoutInline)] -impl RepeatNode { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - Ok(Self(args.expect("body")?).pack()) - } -} - -impl LayoutInline for RepeatNode { - fn layout_inline( - &self, - world: Tracked, - regions: &Regions, - styles: StyleChain, - ) -> SourceResult> { - self.0.layout_inline(world, regions, styles) - } -} - -/// Range of a substring of text. -type Range = std::ops::Range; - -// The characters by which spacing, inline content and pins are replaced in the -// paragraph's full text. -const SPACING_REPLACE: char = ' '; // Space -const NODE_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 nodes. - items: Vec>, - /// The styles shared by all children. - styles: StyleChain<'a>, - /// Whether to hyphenate if it's the same for all children. - hyphenate: Option, - /// The text language if it's the same for all children. - lang: Option, - /// The resolved leading between lines. - leading: Abs, - /// The paragraph's resolved alignment. - align: Align, - /// Whether to justify the paragraph. - justify: bool, -} - -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), - /// Arbitrary inline-level content. - Inline(&'a Content), -} - -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::Inline(_) => NODE_REPLACE.len_utf8(), - } - } -} - -/// 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), - /// Layouted inline-level content. - Frame(Frame), - /// A repeating node that fills the remaining space in a line. - Repeat(&'a RepeatNode, StyleChain<'a>), -} - -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, - } - } - - /// 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(_) | Self::Repeat(_, _) => NODE_REPLACE.len_utf8(), - } - } - - /// 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::Repeat(_, _) => Abs::zero(), - } - } -} - -/// 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 comitting 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>, - /// 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>, - /// 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> { - 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> { - 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 justifiable glyphs the line contains. - fn justifiables(&self) -> usize { - let mut count = 0; - for shaped in self.items().filter_map(Item::text) { - count += shaped.justifiables(); - } - count - } - - /// How much of the line is stretchable spaces. - fn stretch(&self) -> Abs { - let mut stretch = Abs::zero(); - for shaped in self.items().filter_map(Item::text) { - stretch += shaped.stretch(); - } - stretch - } - - /// The sum of fractions in the line. - fn fr(&self) -> Fr { - self.items() - .filter_map(|item| match item { - Item::Fractional(fr) => Some(*fr), - Item::Repeat(_, _) => Some(Fr::one()), - _ => None, - }) - .sum() - } -} - -/// Collect all text of the paragraph into one string. This also performs -/// string-level preprocessing like case transformations. -fn collect<'a>( - par: &'a ParNode, - styles: &'a StyleChain<'a>, -) -> (String, Vec<(Segment<'a>, StyleChain<'a>)>) { - let mut full = String::new(); - let mut quoter = Quoter::new(); - let mut segments = vec![]; - let mut iter = par.0.iter().peekable(); - - while let Some((child, map)) = iter.next() { - let styles = map.chain(&styles); - let segment = match child { - ParChild::Text(text) => { - let prev = full.len(); - if let Some(case) = styles.get(TextNode::CASE) { - full.push_str(&case.apply(text)); - } else { - full.push_str(text); - } - Segment::Text(full.len() - prev) - } - &ParChild::Quote { double } => { - let prev = full.len(); - if styles.get(TextNode::SMART_QUOTES) { - let lang = styles.get(TextNode::LANG); - let region = styles.get(TextNode::REGION); - let quotes = Quotes::from_lang(lang, region); - let peeked = iter.peek().and_then(|(child, _)| match child { - ParChild::Text(text) => text.chars().next(), - ParChild::Quote { .. } => Some('"'), - ParChild::Spacing(_) => Some(SPACING_REPLACE), - ParChild::Inline(_) => Some(NODE_REPLACE), - }); - - full.push_str(quoter.quote("es, double, peeked)); - } else { - full.push(if double { '"' } else { '\'' }); - } - Segment::Text(full.len() - prev) - } - &ParChild::Spacing(spacing) => { - full.push(SPACING_REPLACE); - Segment::Spacing(spacing) - } - ParChild::Inline(inline) => { - full.push(NODE_REPLACE); - Segment::Inline(inline) - } - }; - - if let Some(last) = full.chars().last() { - quoter.last(last); - } - - 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)); - } - - (full, segments) -} - -/// Prepare paragraph layout by shaping the whole paragraph and layouting all -/// contained inline-level content. -fn prepare<'a>( - world: Tracked, - par: &'a ParNode, - text: &'a str, - segments: Vec<(Segment<'a>, StyleChain<'a>)>, - regions: &Regions, - styles: StyleChain<'a>, -) -> SourceResult> { - let bidi = BidiInfo::new(&text, match styles.get(TextNode::DIR) { - 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, world, &bidi, cursor .. end, styles); - } - Segment::Spacing(spacing) => match spacing { - Spacing::Relative(v) => { - let resolved = v.resolve(styles).relative_to(regions.base.x); - items.push(Item::Absolute(resolved)); - } - Spacing::Fractional(v) => { - items.push(Item::Fractional(v)); - } - }, - Segment::Inline(inline) => { - if let Some(repeat) = inline.downcast::() { - items.push(Item::Repeat(repeat, styles)); - } else { - let size = Size::new(regions.first.x, regions.base.y); - let pod = Regions::one(size, regions.base, Axes::splat(false)); - let mut frame = inline.layout_inline(world, &pod, styles)?.remove(0); - frame.translate(Point::with_y(styles.get(TextNode::BASELINE))); - frame.apply_role(Role::GenericInline); - items.push(Item::Frame(frame)); - } - } - } - - cursor = end; - } - - Ok(Preparation { - bidi, - items, - styles, - hyphenate: shared_get(styles, &par.0, TextNode::HYPHENATE), - lang: shared_get(styles, &par.0, TextNode::LANG), - leading: styles.get(ParNode::LEADING), - align: styles.get(ParNode::ALIGN), - justify: styles.get(ParNode::JUSTIFY), - }) -} - -/// 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>, - world: Tracked, - bidi: &BidiInfo<'a>, - range: Range, - styles: StyleChain<'a>, -) { - let mut process = |text, level: BidiLevel| { - let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL }; - let shaped = shape(world, text, styles, dir); - 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 cursor .. range.end { - 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(&bidi.text[cursor .. i], prev_level); - } - cursor = i; - prev_level = level; - prev_script = script; - } else if is_generic_script(prev_script) { - prev_script = script; - } - } - - process(&bidi.text[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<'a, K: Key<'a>>( - styles: StyleChain<'a>, - children: &StyleVec, - key: K, -) -> Option { - children - .styles() - .all(|map| !map.contains(key)) - .then(|| styles.get(key)) -} - -/// Find suitable linebreaks. -fn linebreak<'a>( - p: &'a Preparation<'a>, - world: Tracked, - width: Abs, -) -> Vec> { - match p.styles.get(ParNode::LINEBREAKS) { - Linebreaks::Simple => linebreak_simple(p, world, width), - Linebreaks::Optimized => linebreak_optimized(p, world, 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>( - p: &'a Preparation<'a>, - world: Tracked, - width: Abs, -) -> Vec> { - 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(p, world, 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(p, world, 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>( - p: &'a Preparation<'a>, - world: Tracked, - width: Abs, -) -> Vec> { - /// 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 = 30.0; - const MAX_COST: Cost = 1_000_000.0; - const MIN_COST: Cost = -MAX_COST; - const MIN_RATIO: f64 = -0.15; - - // Dynamic programming table. - let mut active = 0; - let mut table = vec![Entry { - pred: 0, - total: 0.0, - line: line(p, world, 0 .. 0, false, false), - }]; - - let em = p.styles.get(TextNode::SIZE); - - for (end, mandatory, hyphen) in breakpoints(p) { - let k = table.len(); - let eof = end == p.bidi.text.len(); - let mut best: Option = 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(p, world, 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; - let mut ratio = delta / attempt.stretch(); - if ratio.is_infinite() { - ratio = delta / (em / 2.0); - } - - // At some point, it doesn't matter any more. - ratio = ratio.min(10.0); - - // Determine the cost of the line. - let min_ratio = if attempt.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 deactive - // this breakpoint. - active = i + 1; - MAX_COST - } else if mandatory || eof { - // This is a mandatory break and the line is not overfull, so it - // has minimum cost. All breakpoints before this one become - // inactive since no line can span above the mandatory break. - active = k; - MIN_COST + if attempt.justify { 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; - } - - // 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 -} - -/// 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) -> Breakpoints<'a> { - Breakpoints { - p, - linebreaks: LineBreakIterator::new(p.bidi.text), - 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: LineBreakIterator<'a>, - /// Iterator over syllables of the current word. - syllables: Option>, - /// 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 { - // 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)); - } - - // Get the next "word". - (self.end, self.mandatory) = self.linebreaks.next()?; - - // 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(shaped.styles.get(TextNode::HYPHENATE)) - }) - .unwrap_or(false) - } - - /// The text language at the given offset. - fn lang(&self, offset: usize) -> Option { - let lang = self.p.lang.or_else(|| { - let shaped = self.p.find(offset)?.text()?; - Some(shaped.styles.get(TextNode::LANG)) - })?; - - 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>( - p: &'a Preparation, - world: Tracked, - 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]; - let trimmed = text.trim_end(); - 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}'); - - // 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 { - if hyphen || start < range.end || before.is_empty() { - let shifted = start - base .. range.end - base; - let mut reshaped = shaped.reshape(world, shifted); - if hyphen || shy { - reshaped.push_hyphen(world); - } - width += reshaped.width; - last = Some(Item::Text(reshaped)); - } - - inner = before; - } - } - - // 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 { - if range.start < end { - let shifted = range.start - base .. end - base; - let reshaped = shaped.reshape(world, shifted); - width += reshaped.width; - first = Some(Item::Text(reshaped)); - } - - inner = after; - } - } - - // 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 stack( - p: &Preparation, - world: Tracked, - lines: &[Line], - regions: &Regions, -) -> SourceResult> { - // Determine the paragraph's width: Full width of the region if we - // should expand or there's fractional spacing, fit-to-width otherwise. - let mut width = regions.first.x; - if !regions.expand.x && lines.iter().all(|line| line.fr().is_zero()) { - width = lines.iter().map(|line| line.width).max().unwrap_or_default(); - } - - // State for final frame building. - let mut regions = regions.clone(); - let mut finished = vec![]; - let mut first = true; - let mut output = Frame::new(Size::with_x(width)); - output.apply_role(Role::Paragraph); - - // Stack the lines into one frame per region. - for line in lines { - let frame = commit(p, world, line, ®ions, width)?; - let height = frame.size().y; - - while !regions.first.y.fits(height) && !regions.in_last() { - finished.push(output); - output = Frame::new(Size::with_x(width)); - output.apply_role(Role::Paragraph); - regions.next(); - first = true; - } - - if !first { - output.size_mut().y += p.leading; - } - - let pos = Point::with_y(output.height()); - output.size_mut().y += height; - output.push_frame(pos, frame); - - regions.first.y -= height + p.leading; - first = false; - } - - finished.push(output); - Ok(finished) -} - -/// Commit to a line and build its frame. -fn commit( - p: &Preparation, - world: Tracked, - line: &Line, - regions: &Regions, - width: Abs, -) -> SourceResult { - let mut remaining = width - line.width; - let mut offset = Abs::zero(); - - // Reorder the line from logical to visual order. - let reordered = reorder(line); - - // 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() - && text.styles.get(TextNode::OVERHANG) - && (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() - && text.styles.get(TextNode::OVERHANG) - && (reordered.len() > 1 || text.glyphs.len() > 1) - { - let amount = overhang(glyph.c) * glyph.x_advance.at(text.size); - remaining += amount; - } - } - } - - // Determine how much to justify each space. - let fr = line.fr(); - let mut justification = Abs::zero(); - if remaining < Abs::zero() || (line.justify && fr.is_zero()) { - let justifiables = line.justifiables(); - if justifiables > 0 { - 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) => { - offset += v.share(fr, remaining); - } - Item::Text(shaped) => { - let frame = shaped.build(world, justification); - push(&mut offset, frame); - } - Item::Frame(frame) => { - push(&mut offset, frame.clone()); - } - Item::Repeat(repeat, styles) => { - let before = offset; - let fill = Fr::one().share(fr, remaining); - let size = Size::new(fill, regions.base.y); - let pod = Regions::one(size, regions.base, Axes::new(false, false)); - let frame = repeat.layout_inline(world, &pod, *styles)?.remove(0); - let width = frame.width(); - let count = (fill / width).floor(); - let remaining = fill % width; - let apart = remaining / (count - 1.0); - if count == 1.0 { - offset += p.align.position(remaining); - } - if width > Abs::zero() { - for _ in 0 .. (count as usize).min(1000) { - push(&mut offset, frame.clone()); - offset += apart; - } - } - offset = before + fill; - } - } - } - - // 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>> { - let mut reordered = vec![]; - - // The bidi crate doesn't like empty lines. - if line.trimmed.is_empty() { - return line.slice(line.trimmed.clone()).collect(); - } - - // 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()); - - // 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 -} - -/// 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 and Ideographic - '\u{60C}' | '\u{6D4}' => 0.4, - '\u{3001}' | '\u{3002}' => 1.0, - - _ => 0.0, - } -} diff --git a/src/library/text/quotes.rs b/src/library/text/quotes.rs deleted file mode 100644 index 0a22646a..00000000 --- a/src/library/text/quotes.rs +++ /dev/null @@ -1,148 +0,0 @@ -use super::{Lang, Region}; -use crate::syntax::is_newline; - -/// State machine for smart quote subtitution. -#[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, - ) -> &'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 subtitute 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, 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) -> 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" => ("’", "’", "”", "”"), - "hu" | "pl" | "ro" => ("’", "’", "„", "”"), - "ru" | "no" | "nn" => ("’", "’", "«", "»"), - _ => 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/src/library/text/raw.rs b/src/library/text/raw.rs deleted file mode 100644 index 0c769636..00000000 --- a/src/library/text/raw.rs +++ /dev/null @@ -1,205 +0,0 @@ -use once_cell::sync::Lazy; -use syntect::easy::HighlightLines; -use syntect::highlighting::{ - Color, FontStyle, Style, StyleModifier, Theme, ThemeItem, ThemeSettings, -}; -use syntect::parsing::SyntaxSet; - -use super::{FontFamily, Hyphenate, LinebreakNode, TextNode}; -use crate::library::layout::{BlockNode, BlockSpacing}; -use crate::library::prelude::*; - -/// Monospaced text with optional syntax highlighting. -#[derive(Debug, Hash)] -pub struct RawNode { - /// The raw text. - pub text: EcoString, - /// Whether the node is block-level. - pub block: bool, -} - -#[node(Show)] -impl RawNode { - /// The language to syntax-highlight in. - #[property(referenced)] - pub const LANG: Option = None; - /// The raw text's font family. - #[property(referenced)] - pub const FAMILY: FontFamily = FontFamily::new("IBM Plex Mono"); - /// The spacing above block-level raw. - #[property(resolve, shorthand(around))] - pub const ABOVE: Option = Some(Ratio::one().into()); - /// The spacing below block-level raw. - #[property(resolve, shorthand(around))] - pub const BELOW: Option = Some(Ratio::one().into()); - - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - Ok(Self { - text: args.expect("text")?, - block: args.named("block")?.unwrap_or(false), - } - .pack()) - } -} - -impl Show for RawNode { - fn unguard_parts(&self, _: Selector) -> Content { - Self { text: self.text.clone(), ..*self }.pack() - } - - fn field(&self, name: &str) -> Option { - match name { - "text" => Some(Value::Str(self.text.clone().into())), - "block" => Some(Value::Bool(self.block)), - _ => None, - } - } - - fn realize( - &self, - _: Tracked, - styles: StyleChain, - ) -> SourceResult { - let lang = styles.get(Self::LANG).as_ref().map(|s| s.to_lowercase()); - let foreground = THEME - .settings - .foreground - .map(Color::from) - .unwrap_or(Color::BLACK) - .into(); - - let mut realized = if matches!(lang.as_deref(), Some("typ" | "typst" | "typc")) { - let root = match lang.as_deref() { - Some("typc") => crate::syntax::parse_code(&self.text), - _ => crate::syntax::parse(&self.text), - }; - - let mut seq = vec![]; - crate::syntax::highlight::highlight_themed(&root, &THEME, |range, style| { - seq.push(styled(&self.text[range], foreground, 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 = HighlightLines::new(syntax, &THEME); - for (i, line) in self.text.lines().enumerate() { - if i != 0 { - seq.push(LinebreakNode { justify: false }.pack()); - } - - for (style, piece) in - highlighter.highlight_line(line, &SYNTAXES).into_iter().flatten() - { - seq.push(styled(piece, foreground, style)); - } - } - - Content::sequence(seq) - } else { - TextNode(self.text.clone()).pack() - }; - - if self.block { - realized = BlockNode(realized).pack(); - } - - let mut map = StyleMap::new(); - map.set(TextNode::OVERHANG, false); - map.set(TextNode::HYPHENATE, Smart::Custom(Hyphenate(false))); - map.set(TextNode::SMART_QUOTES, false); - - Ok(realized.styled_with_map(map)) - } - - fn finalize( - &self, - _: Tracked, - styles: StyleChain, - mut realized: Content, - ) -> SourceResult { - let mut map = StyleMap::new(); - map.set_family(styles.get(Self::FAMILY).clone(), styles); - - if self.block { - realized = realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW)); - } - - Ok(realized.styled_with_map(map)) - } -} - -/// Style a piece of text with a syntect style. -fn styled(piece: &str, foreground: Paint, style: Style) -> Content { - let mut body = TextNode(piece.into()).pack(); - - let paint = style.foreground.into(); - if paint != foreground { - body = body.styled(TextNode::FILL, paint); - } - - if style.font_style.contains(FontStyle::BOLD) { - body = body.strong(); - } - - if style.font_style.contains(FontStyle::ITALIC) { - body = body.emph(); - } - - if style.font_style.contains(FontStyle::UNDERLINE) { - body = body.underlined(); - } - - body -} - -/// The syntect syntax definitions. -static SYNTAXES: Lazy = Lazy::new(|| SyntaxSet::load_defaults_newlines()); - -/// The default theme used for syntax highlighting. -#[rustfmt::skip] -pub static THEME: Lazy = Lazy::new(|| Theme { - name: Some("Typst Light".into()), - author: Some("The Typst Project Developers".into()), - settings: ThemeSettings::default(), - scopes: vec![ - item("comment", Some("#8a8a8a"), None), - item("constant.character.escape", Some("#1d6c76"), None), - item("constant.character.shortcut", Some("#1d6c76"), None), - item("markup.bold", None, Some(FontStyle::BOLD)), - item("markup.italic", None, Some(FontStyle::ITALIC)), - item("markup.underline", None, Some(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(FontStyle::BOLD)), - item("markup.heading.typst", None, Some(FontStyle::BOLD | FontStyle::UNDERLINE)), - item("punctuation.definition.list", Some("#8b41b1"), None), - item("markup.list.term", None, Some(FontStyle::BOLD)), - item("entity.name.label, markup.other.reference", Some("#1d6c76"), None), - item("keyword, constant.language, variable.language", Some("#d73a49"), None), - item("storage.type, storage.modifier", Some("#d73a49"), None), - item("constant", Some("#b60157"), None), - item("string", Some("#298e0d"), None), - item("entity.name, variable.function, support", Some("#4b69c6"), None), - item("support.macro", Some("#16718d"), None), - item("meta.annotation", Some("#301414"), None), - item("entity.other, meta.interpolation", Some("#8b41b1"), None), - item("invalid", Some("#ff0000"), None), - ], -}); - -/// Create a syntect theme item. -fn item(scope: &str, color: Option<&str>, font_style: Option) -> ThemeItem { - ThemeItem { - scope: scope.parse().unwrap(), - style: StyleModifier { - foreground: color.map(|s| s.parse::().unwrap().into()), - background: None, - font_style, - }, - } -} diff --git a/src/library/text/shaping.rs b/src/library/text/shaping.rs deleted file mode 100644 index 9ac3db1c..00000000 --- a/src/library/text/shaping.rs +++ /dev/null @@ -1,654 +0,0 @@ -use std::ops::Range; -use std::str::FromStr; - -use rustybuzz::{Feature, Tag, UnicodeBuffer}; - -use super::*; -use crate::font::{Font, FontVariant}; -use crate::library::prelude::*; -use crate::util::SliceExt; - -/// 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 text that was shaped. - pub text: &'a str, - /// The text direction. - pub dir: Dir, - /// 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 byte index in the source text where this glyph's cluster starts. A - /// cluster is a sequence of one or multiple glyphs that cannot be - /// separated and must always be treated as a union. - pub cluster: 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, -} - -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 { - self.is_space() || matches!(self.c, ',' | '。' | '、') - } -} - -/// 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, world: Tracked, justification: Abs) -> Frame { - let (top, bottom) = self.measure(world); - 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 = self.styles.get(TextNode::BASELINE); - let lang = self.styles.get(TextNode::LANG); - let decos = self.styles.get(TextNode::DECO); - let fill = self.styles.get(TextNode::FILL); - let link = self.styles.get(TextNode::LINK); - - for ((font, y_offset), group) in - self.glyphs.as_ref().group_by_key(|g| (g.font.clone(), g.y_offset)) - { - let pos = Point::new(offset, top + shift + y_offset.at(self.size)); - - let glyphs = group - .iter() - .map(|glyph| Glyph { - id: glyph.glyph_id, - x_advance: glyph.x_advance - + if glyph.is_justifiable() { - frame.size_mut().x += justification; - Em::from_length(justification, self.size) - } else { - Em::zero() - }, - x_offset: glyph.x_offset, - c: glyph.c, - }) - .collect(); - - let text = Text { - font, - size: self.size, - lang, - fill, - glyphs, - }; - - let text_layer = frame.layer(); - let width = text.width(); - - // Apply line decorations. - for deco in &decos { - decorate(&mut frame, &deco, &text, shift, pos, width); - } - - frame.insert(text_layer, pos, Element::Text(text)); - offset += width; - } - - // Apply link if it exists. - if let Some(dest) = link { - frame.link(dest.clone()); - } - - frame - } - - /// Measure the top and bottom extent of this text. - fn measure(&self, world: Tracked) -> (Abs, Abs) { - let mut top = Abs::zero(); - let mut bottom = Abs::zero(); - - let top_edge = self.styles.get(TextNode::TOP_EDGE); - let bottom_edge = self.styles.get(TextNode::BOTTOM_EDGE); - - // 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. - for family in families(self.styles) { - if let Some(font) = world - .book() - .select(family, 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 justifiable glyphs the text contains. - pub fn justifiables(&self) -> usize { - self.glyphs.iter().filter(|g| g.is_justifiable()).count() - } - - /// The width of the spaces in the text. - pub fn stretch(&self) -> Abs { - self.glyphs - .iter() - .filter(|g| g.is_justifiable()) - .map(|g| g.x_advance) - .sum::() - .at(self.size) - } - - /// Reshape a range of the shaped text, reusing information from this - /// shaping process if possible. - pub fn reshape( - &'a self, - world: Tracked, - text_range: Range, - ) -> ShapedText<'a> { - if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { - Self { - text: &self.text[text_range], - dir: self.dir, - styles: self.styles, - size: self.size, - variant: self.variant, - width: glyphs.iter().map(|g| g.x_advance).sum::().at(self.size), - glyphs: Cow::Borrowed(glyphs), - } - } else { - shape(world, &self.text[text_range], self.styles, self.dir) - } - } - - /// Push a hyphen to end of the text. - pub fn push_hyphen(&mut self, world: Tracked) { - families(self.styles).find_map(|family| { - let font = world - .book() - .select(family, 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 cluster = self.glyphs.last().map(|g| g.cluster).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(), - cluster, - safe_to_break: true, - c: '-', - }); - 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) -> 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 { - let ltr = self.dir.is_positive(); - - // Handle edge cases. - let len = self.glyphs.len(); - if text_index == 0 { - return Some(if ltr { 0 } else { len }); - } else if text_index == 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.cluster.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.cluster != 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(|| idx + (!ltr) as usize) - } -} - -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> { - world: Tracked<'a, dyn World>, - glyphs: Vec, - used: Vec, - styles: StyleChain<'a>, - size: Abs, - variant: FontVariant, - tags: Vec, - fallback: bool, - dir: Dir, -} - -/// Shape text into [`ShapedText`]. -pub fn shape<'a>( - world: Tracked, - text: &'a str, - styles: StyleChain<'a>, - dir: Dir, -) -> ShapedText<'a> { - let size = styles.get(TextNode::SIZE); - - let mut ctx = ShapingContext { - world, - size, - glyphs: vec![], - used: vec![], - styles, - variant: variant(styles), - tags: tags(styles), - fallback: styles.get(TextNode::FALLBACK), - dir, - }; - - if !text.is_empty() { - shape_segment(&mut ctx, 0, &text, families(styles)); - } - - track_and_space(&mut ctx); - - ShapedText { - text, - dir, - styles, - variant: ctx.variant, - size, - width: ctx.glyphs.iter().map(|g| g.x_advance).sum::().at(size), - glyphs: Cow::Owned(ctx.glyphs), - } -} - -/// Shape text with font fallback using the `families` iterator. -fn shape_segment<'a>( - ctx: &mut ShapingContext, - base: usize, - text: &str, - mut families: impl Iterator + Clone, -) { - // Fonts dont have newlines and tabs. - if text.chars().all(|c| c == '\n' || c == '\t') { - return; - } - - // Find the next available family. - let book = ctx.world.book(); - let mut selection = families.find_map(|family| { - book.select(family, ctx.variant) - .and_then(|id| ctx.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| ctx.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 font = if let Some(font) = selection { - font - } 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(); - - // 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; - - if info.glyph_id != 0 { - // Add the glyph to the shaped output. - // TODO: Don't ignore y_advance. - ctx.glyphs.push(ShapedGlyph { - font: font.clone(), - glyph_id: info.glyph_id as u16, - 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), - cluster: base + cluster, - safe_to_break: !info.unsafe_to_break(), - c: text[cluster ..].chars().next().unwrap(), - }); - } else { - // Determine the source text range for the tofu sequence. - let range = { - // 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. - // - // 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 ltr = ctx.dir.is_positive(); - let first = if ltr { k } else { i }; - let start = infos[first].cluster as usize; - let last = if ltr { i.checked_add(1) } else { k.checked_sub(1) }; - let end = last - .and_then(|last| infos.get(last)) - .map_or(text.len(), |info| info.cluster as usize); - - start .. end - }; - - // Trim half-baked cluster. - let remove = base + range.start .. base + range.end; - while ctx.glyphs.last().map_or(false, |g| remove.contains(&g.cluster)) { - ctx.glyphs.pop(); - } - - // Recursively shape the tofu sequence with the next family. - shape_segment(ctx, base + range.start, &text[range], 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(); - for (cluster, c) in text.char_indices() { - ctx.glyphs.push(ShapedGlyph { - font: font.clone(), - glyph_id: 0, - x_advance, - x_offset: Em::zero(), - y_offset: Em::zero(), - cluster: base + cluster, - safe_to_break: true, - c, - }); - } -} - -/// Apply tracking and spacing to the shaped glyphs. -fn track_and_space(ctx: &mut ShapingContext) { - let tracking = Em::from_length(ctx.styles.get(TextNode::TRACKING), ctx.size); - let spacing = ctx - .styles - .get(TextNode::SPACING) - .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.cluster != next.cluster) { - glyph.x_advance += tracking; - } - } -} - -/// Difference between non-breaking and normal space. -fn nbsp_delta(font: &Font) -> Option { - 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 with `BOLD` and `ITALIC` factored in. -pub fn variant(styles: StyleChain) -> FontVariant { - let mut variant = FontVariant::new( - styles.get(TextNode::STYLE), - styles.get(TextNode::WEIGHT), - styles.get(TextNode::STRETCH), - ); - - if styles.get(TextNode::BOLD) { - variant.weight = variant.weight.thicken(300); - } - - if styles.get(TextNode::ITALIC) { - 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. -fn families(styles: StyleChain) -> impl Iterator + Clone { - const FALLBACKS: &[&str] = &[ - "ibm plex sans", - "twitter color emoji", - "noto color emoji", - "apple color emoji", - "segoe ui emoji", - ]; - - let tail = if styles.get(TextNode::FALLBACK) { FALLBACKS } else { &[] }; - styles - .get(TextNode::FAMILY) - .iter() - .map(|family| family.as_str()) - .chain(tail.iter().copied()) -} - -/// Collect the tags of the OpenType features to apply. -fn tags(styles: StyleChain) -> Vec { - 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 !styles.get(TextNode::KERNING) { - feat(b"kern", 0); - } - - // Features that are off by default in Harfbuzz are only added if enabled. - if styles.get(TextNode::SMALLCAPS) { - feat(b"smcp", 1); - } - - if styles.get(TextNode::ALTERNATES) { - feat(b"salt", 1); - } - - let storage; - if let Some(set) = styles.get(TextNode::STYLISTIC_SET) { - storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10]; - feat(&storage, 1); - } - - if !styles.get(TextNode::LIGATURES) { - feat(b"liga", 0); - feat(b"clig", 0); - } - - if styles.get(TextNode::DISCRETIONARY_LIGATURES) { - feat(b"dlig", 1); - } - - if styles.get(TextNode::HISTORICAL_LIGATURES) { - feat(b"hilg", 1); - } - - match styles.get(TextNode::NUMBER_TYPE) { - Smart::Auto => {} - Smart::Custom(NumberType::Lining) => feat(b"lnum", 1), - Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1), - } - - match styles.get(TextNode::NUMBER_WIDTH) { - Smart::Auto => {} - Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1), - Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1), - } - - if styles.get(TextNode::SLASHED_ZERO) { - feat(b"zero", 1); - } - - if styles.get(TextNode::FRACTIONS) { - feat(b"frac", 1); - } - - for (tag, value) in styles.get(TextNode::FEATURES) { - 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 = styles.get(TextNode::LANG).as_str().into(); - if let Some(region) = styles.get(TextNode::REGION) { - bcp.push('-'); - bcp.push_str(region.as_str()); - } - rustybuzz::Language::from_str(&bcp).unwrap() -} diff --git a/src/library/text/shift.rs b/src/library/text/shift.rs deleted file mode 100644 index c3cf8b03..00000000 --- a/src/library/text/shift.rs +++ /dev/null @@ -1,187 +0,0 @@ -use super::{variant, SpaceNode, TextNode, TextSize}; -use crate::library::prelude::*; -use crate::model::SequenceNode; -use crate::util::EcoString; - -/// Sub or superscript text. -/// -/// The text is rendered smaller and its baseline is raised. To provide the best -/// typography possible, we first try to transform the text to superscript -/// codepoints. If that fails, we fall back to rendering shrunk normal letters -/// in a raised way. -#[derive(Debug, Hash)] -pub struct ShiftNode(pub Content); - -/// Shift the text into superscript. -pub type SuperNode = ShiftNode; - -/// Shift the text into subscript. -pub type SubNode = ShiftNode; - -#[node(Show)] -impl ShiftNode { - /// Whether to prefer the dedicated sub- and superscript characters of the - /// font. - pub const TYPOGRAPHIC: bool = true; - /// The baseline shift for synthetic sub- and superscripts. - pub const BASELINE: Length = - Em::new(if S == SUPERSCRIPT { -0.5 } else { 0.2 }).into(); - /// The font size for synthetic sub- and superscripts. - pub const SIZE: TextSize = TextSize(Em::new(0.6).into()); - - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - Ok(Self(args.expect("body")?).pack()) - } -} - -impl Show for ShiftNode { - fn unguard_parts(&self, _: Selector) -> Content { - Self(self.0.clone()).pack() - } - - fn field(&self, name: &str) -> Option { - match name { - "body" => Some(Value::Content(self.0.clone())), - _ => None, - } - } - - fn realize( - &self, - world: Tracked, - styles: StyleChain, - ) -> SourceResult { - let mut transformed = None; - if styles.get(Self::TYPOGRAPHIC) { - if let Some(text) = search_text(&self.0, S) { - if is_shapable(world, &text, styles) { - transformed = Some(TextNode(text).pack()); - } - } - }; - - Ok(transformed.unwrap_or_else(|| { - let mut map = StyleMap::new(); - map.set(TextNode::BASELINE, styles.get(Self::BASELINE)); - map.set(TextNode::SIZE, styles.get(Self::SIZE)); - self.0.clone().styled_with_map(map) - })) - } -} - -/// 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` leaf nodes. -fn search_text(content: &Content, mode: ScriptKind) -> Option { - if content.is_empty() { - Some(EcoString::new()) - } else if content.is::() { - Some(' '.into()) - } else if let Some(text) = content.downcast::() { - if let Some(sup) = convert_script(&text.0, mode) { - return Some(sup); - } - None - } else if let Some(seq) = content.downcast::() { - let mut full = EcoString::new(); - for item in seq.0.iter() { - match search_text(item, mode) { - 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(world: Tracked, text: &str, styles: StyleChain) -> bool { - for family in styles.get(TextNode::FAMILY).iter() { - 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, mode: ScriptKind) -> Option { - let mut result = EcoString::with_capacity(text.len()); - let converter = match mode { - SUPERSCRIPT => to_superscript_codepoint, - SUBSCRIPT | _ => to_subscript_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::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::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, - }) -} - -/// A category of script. -pub type ScriptKind = usize; - -/// Text that is rendered smaller and raised, also known as superior. -const SUPERSCRIPT: ScriptKind = 0; - -/// Text that is rendered smaller and lowered, also known as inferior. -const SUBSCRIPT: ScriptKind = 1; diff --git a/src/library/utility/color.rs b/src/library/utility/color.rs deleted file mode 100644 index a5a5704d..00000000 --- a/src/library/utility/color.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::str::FromStr; - -use crate::library::prelude::*; - -/// Create a grayscale color. -pub fn luma(_: &mut Vm, args: &mut Args) -> SourceResult { - let Component(luma) = args.expect("gray component")?; - Ok(Value::Color(LumaColor::new(luma).into())) -} - -/// Create an RGB(A) color. -pub fn rgb(_: &mut Vm, args: &mut Args) -> SourceResult { - Ok(Value::Color( - if let Some(string) = args.find::>()? { - 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() - }, - )) -} - -/// Create a CMYK color. -pub fn cmyk(_: &mut Vm, args: &mut Args) -> SourceResult { - let RatioComponent(c) = args.expect("cyan component")?; - let RatioComponent(m) = args.expect("magenta component")?; - let RatioComponent(y) = args.expect("yellow component")?; - let RatioComponent(k) = args.expect("key component")?; - Ok(Value::Color(CmykColor::new(c, m, y, k).into())) -} - -/// An integer or ratio component. -struct Component(u8); - -castable! { - Component, - Expected: "integer or ratio", - Value::Int(v) => match v { - 0 ..= 255 => Self(v as u8), - _ => Err("must be between 0 and 255")?, - }, - Value::Ratio(v) => if (0.0 ..= 1.0).contains(&v.get()) { - Self((v.get() * 255.0).round() as u8) - } else { - Err("must be between 0% and 100%")? - }, -} - -/// A component that must be a ratio. -struct RatioComponent(u8); - -castable! { - RatioComponent, - Expected: "ratio", - Value::Ratio(v) => if (0.0 ..= 1.0).contains(&v.get()) { - Self((v.get() * 255.0).round() as u8) - } else { - Err("must be between 0% and 100%")? - }, -} diff --git a/src/library/utility/data.rs b/src/library/utility/data.rs deleted file mode 100644 index 0cff42c1..00000000 --- a/src/library/utility/data.rs +++ /dev/null @@ -1,131 +0,0 @@ -use std::fmt::Write; - -use crate::diag::format_xml_like_error; -use crate::library::prelude::*; - -/// Read structured data from a CSV file. -pub fn csv(vm: &mut Vm, args: &mut Args) -> SourceResult { - let Spanned { v: path, span } = - args.expect::>("path to csv file")?; - - let path = vm.locate(&path).at(span)?; - let data = vm.world.file(&path).at(span)?; - - let mut builder = csv::ReaderBuilder::new(); - builder.has_headers(false); - - let mut reader = builder.from_reader(data.as_slice()); - let mut vec = vec![]; - - for result in reader.records() { - let row = result.map_err(format_csv_error).at(span)?; - let array = row.iter().map(|field| Value::Str(field.into())).collect(); - vec.push(Value::Array(array)) - } - - Ok(Value::Array(Array::from_vec(vec))) -} - -/// Format the user-facing CSV error message. -fn format_csv_error(error: csv::Error) -> String { - match error.kind() { - csv::ErrorKind::Utf8 { .. } => "file is not valid utf-8".into(), - csv::ErrorKind::UnequalLengths { pos, expected_len, len } => { - let mut msg = format!( - "failed to parse csv file: found {len} instead of {expected_len} fields" - ); - if let Some(pos) = pos { - write!(msg, " in line {}", pos.line()).unwrap(); - } - msg - } - _ => "failed to parse csv file".into(), - } -} - -/// Read structured data from a JSON file. -pub fn json(vm: &mut Vm, args: &mut Args) -> SourceResult { - let Spanned { v: path, span } = - args.expect::>("path to json file")?; - - let path = vm.locate(&path).at(span)?; - let data = vm.world.file(&path).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) => Value::Bool(v), - serde_json::Value::Number(v) => match v.as_i64() { - Some(int) => Value::Int(int), - None => Value::Float(v.as_f64().unwrap_or(f64::NAN)), - }, - serde_json::Value::String(v) => Value::Str(v.into()), - serde_json::Value::Array(v) => { - Value::Array(v.into_iter().map(convert_json).collect()) - } - serde_json::Value::Object(v) => Value::Dict( - v.into_iter() - .map(|(key, value)| (key.into(), convert_json(value))) - .collect(), - ), - } -} - -/// Format the user-facing JSON error message. -fn format_json_error(error: serde_json::Error) -> String { - assert!(error.is_syntax() || error.is_eof()); - format!( - "failed to parse json file: syntax error in line {}", - error.line() - ) -} - -/// Read structured data from an XML file. -pub fn xml(vm: &mut Vm, args: &mut Args) -> SourceResult { - let Spanned { v: path, span } = - args.expect::>("path to xml file")?; - - let path = vm.locate(&path).at(span)?; - let data = vm.world.file(&path).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 Value::Str(node.text().unwrap_or_default().into()); - } - - 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() - .iter() - .map(|attr| (attr.name().into(), attr.value().into())) - .collect(); - - Value::Dict(dict! { - "tag" => tag, - "attrs" => attrs, - "children" => children, - }) -} - -/// Format the user-facing XML error message. -fn format_xml_error(error: roxmltree::Error) -> String { - format_xml_like_error("xml file", error) -} diff --git a/src/library/utility/math.rs b/src/library/utility/math.rs deleted file mode 100644 index 7c3af490..00000000 --- a/src/library/utility/math.rs +++ /dev/null @@ -1,144 +0,0 @@ -use std::cmp::Ordering; - -use crate::library::prelude::*; - -/// Convert a value to an integer. -pub fn int(_: &mut Vm, args: &mut Args) -> SourceResult { - let Spanned { v, span } = args.expect("value")?; - Ok(Value::Int(match v { - Value::Bool(v) => v as i64, - Value::Int(v) => v, - Value::Float(v) => v as i64, - Value::Str(v) => match v.parse() { - Ok(v) => v, - Err(_) => bail!(span, "invalid integer"), - }, - v => bail!(span, "cannot convert {} to integer", v.type_name()), - })) -} - -/// Convert a value to a float. -pub fn float(_: &mut Vm, args: &mut Args) -> SourceResult { - let Spanned { v, span } = args.expect("value")?; - Ok(Value::Float(match v { - Value::Int(v) => v as f64, - Value::Float(v) => v, - Value::Str(v) => match v.parse() { - Ok(v) => v, - Err(_) => bail!(span, "invalid float"), - }, - v => bail!(span, "cannot convert {} to float", v.type_name()), - })) -} - -/// The absolute value of a numeric value. -pub fn abs(_: &mut Vm, args: &mut Args) -> SourceResult { - let Spanned { v, span } = args.expect("numeric value")?; - Ok(match v { - Value::Int(v) => Value::Int(v.abs()), - Value::Float(v) => Value::Float(v.abs()), - Value::Angle(v) => Value::Angle(v.abs()), - Value::Ratio(v) => Value::Ratio(v.abs()), - Value::Fraction(v) => Value::Fraction(v.abs()), - Value::Length(_) | Value::Relative(_) => { - bail!(span, "cannot take absolute value of a length") - } - v => bail!(span, "expected numeric value, found {}", v.type_name()), - }) -} - -/// The minimum of a sequence of values. -pub fn min(_: &mut Vm, args: &mut Args) -> SourceResult { - minmax(args, Ordering::Less) -} - -/// The maximum of a sequence of values. -pub fn max(_: &mut Vm, args: &mut Args) -> SourceResult { - minmax(args, Ordering::Greater) -} - -/// Find the minimum or maximum of a sequence of values. -fn minmax(args: &mut Args, goal: Ordering) -> SourceResult { - let mut extremum = args.expect::("value")?; - for Spanned { v, span } in args.all::>()? { - match v.partial_cmp(&extremum) { - Some(ordering) => { - if ordering == goal { - extremum = v; - } - } - None => bail!( - span, - "cannot compare {} and {}", - extremum.type_name(), - v.type_name(), - ), - } - } - Ok(extremum) -} - -/// Whether an integer is even. -pub fn even(_: &mut Vm, args: &mut Args) -> SourceResult { - Ok(Value::Bool(args.expect::("integer")? % 2 == 0)) -} - -/// Whether an integer is odd. -pub fn odd(_: &mut Vm, args: &mut Args) -> SourceResult { - Ok(Value::Bool(args.expect::("integer")? % 2 != 0)) -} - -/// The modulo of two numbers. -pub fn mod_(_: &mut Vm, args: &mut Args) -> SourceResult { - let Spanned { v: v1, span: span1 } = args.expect("integer or float")?; - let Spanned { v: v2, span: span2 } = args.expect("integer or float")?; - - let (a, b) = match (v1, v2) { - (Value::Int(a), Value::Int(b)) => match a.checked_rem(b) { - Some(res) => return Ok(Value::Int(res)), - None => bail!(span2, "divisor must not be zero"), - }, - (Value::Int(a), Value::Float(b)) => (a as f64, b), - (Value::Float(a), Value::Int(b)) => (a, b as f64), - (Value::Float(a), Value::Float(b)) => (a, b), - (Value::Int(_), b) | (Value::Float(_), b) => bail!( - span2, - format!("expected integer or float, found {}", b.type_name()) - ), - (a, _) => bail!( - span1, - format!("expected integer or float, found {}", a.type_name()) - ), - }; - - if b == 0.0 { - bail!(span2, "divisor must not be zero"); - } - - Ok(Value::Float(a % b)) -} - -/// Create a sequence of numbers. -pub fn range(_: &mut Vm, args: &mut Args) -> SourceResult { - let first = args.expect::("end")?; - let (start, end) = match args.eat::()? { - Some(second) => (first, second), - None => (0, first), - }; - - let step: i64 = match args.named("step")? { - Some(Spanned { v: 0, span }) => bail!(span, "step must not be zero"), - Some(Spanned { v, .. }) => v, - None => 1, - }; - - let mut x = start; - let mut seq = vec![]; - - while x.cmp(&end) == 0.cmp(&step) { - seq.push(Value::Int(x)); - x += step; - } - - Ok(Value::Array(Array::from_vec(seq))) -} diff --git a/src/library/utility/mod.rs b/src/library/utility/mod.rs deleted file mode 100644 index 2d637d29..00000000 --- a/src/library/utility/mod.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Computational utility functions. - -mod color; -mod data; -mod math; -mod string; - -pub use color::*; -pub use data::*; -pub use math::*; -pub use string::*; - -use comemo::Track; - -use crate::library::prelude::*; -use crate::model::{Eval, Route, Scopes, Vm}; -use crate::syntax::Source; - -/// The name of a value's type. -pub fn type_(_: &mut Vm, args: &mut Args) -> SourceResult { - Ok(args.expect::("value")?.type_name().into()) -} - -/// Ensure that a condition is fulfilled. -pub fn assert(_: &mut Vm, args: &mut Args) -> SourceResult { - let Spanned { v, span } = args.expect::>("condition")?; - if !v { - bail!(span, "assertion failed"); - } - Ok(Value::None) -} - -/// Evaluate a string as Typst markup. -pub fn eval(vm: &mut Vm, args: &mut Args) -> SourceResult { - let Spanned { v: text, span } = args.expect::>("source")?; - - // Parse the source and set a synthetic span for all nodes. - let source = Source::synthesized(text, span); - let ast = source.ast()?; - - // Evaluate the source. - let std = &vm.world.config().std; - let scopes = Scopes::new(Some(std)); - let route = Route::default(); - let mut sub = Vm::new(vm.world, route.track(), None, scopes); - let result = ast.eval(&mut sub); - - // Handle control flow. - if let Some(flow) = sub.flow { - bail!(flow.forbidden()); - } - - Ok(Value::Content(result?)) -} diff --git a/src/library/utility/string.rs b/src/library/utility/string.rs deleted file mode 100644 index 66f127d1..00000000 --- a/src/library/utility/string.rs +++ /dev/null @@ -1,140 +0,0 @@ -use crate::library::prelude::*; -use crate::model::Regex; - -/// The string representation of a value. -pub fn repr(_: &mut Vm, args: &mut Args) -> SourceResult { - Ok(args.expect::("value")?.repr().into()) -} - -/// Convert a value to a string. -pub fn str(_: &mut Vm, args: &mut Args) -> SourceResult { - let Spanned { v, span } = args.expect("value")?; - Ok(Value::Str(match v { - Value::Int(v) => format_str!("{}", v), - Value::Float(v) => format_str!("{}", v), - Value::Str(v) => v, - v => bail!(span, "cannot convert {} to string", v.type_name()), - })) -} - -/// Create blind text. -pub fn lorem(_: &mut Vm, args: &mut Args) -> SourceResult { - let words: usize = args.expect("number of words")?; - Ok(Value::Str(lipsum::lipsum(words).into())) -} - -/// Create a regular expression. -pub fn regex(_: &mut Vm, args: &mut Args) -> SourceResult { - let Spanned { v, span } = args.expect::>("regular expression")?; - Ok(Regex::new(&v).at(span)?.into()) -} - -/// Converts an integer into one or multiple letters. -pub fn letter(_: &mut Vm, args: &mut Args) -> SourceResult { - numbered(Numbering::Letter, args) -} - -/// Converts an integer into a roman numeral. -pub fn roman(_: &mut Vm, args: &mut Args) -> SourceResult { - numbered(Numbering::Roman, args) -} - -/// Convert a number into a symbol. -pub fn symbol(_: &mut Vm, args: &mut Args) -> SourceResult { - numbered(Numbering::Symbol, args) -} - -fn numbered(numbering: Numbering, args: &mut Args) -> SourceResult { - let n = args.expect::("non-negative integer")?; - Ok(Value::Str(numbering.apply(n).into())) -} - -/// Allows to convert a number into letters, roman numerals and symbols. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Numbering { - Arabic, - Letter, - Roman, - Symbol, -} - -impl Numbering { - /// Apply the numbering to the given number. - pub fn apply(self, mut n: usize) -> EcoString { - match self { - Self::Arabic => { - format_eco!("{}", n) - } - Self::Letter => { - if n == 0 { - return '-'.into(); - } - - n -= 1; - - let mut letters = vec![]; - loop { - letters.push(b'a' + (n % 26) as u8); - n /= 26; - if n == 0 { - break; - } - } - - letters.reverse(); - String::from_utf8(letters).unwrap().into() - } - 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 ROMANS { - while n >= value { - n -= value; - fmt.push_str(name); - } - } - - fmt - } - Self::Symbol => { - if n == 0 { - return '-'.into(); - } - - let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()]; - let amount = ((n - 1) / SYMBOLS.len()) + 1; - std::iter::repeat(symbol).take(amount).collect() - } - } - } -} - -const ROMANS: &[(&str, usize)] = &[ - ("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), -]; - -const SYMBOLS: &[char] = &['*', '†', '‡', '§', '‖', '¶']; diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index e32bb8c6..00000000 --- a/src/main.rs +++ /dev/null @@ -1,674 +0,0 @@ -use std::cell::{RefCell, RefMut}; -use std::collections::HashMap; -use std::fs::{self, File}; -use std::hash::Hash; -use std::io::{self, Read, Write}; -use std::path::{Path, PathBuf}; -use std::process; - -use codespan_reporting::diagnostic::{Diagnostic, Label}; -use codespan_reporting::term::{self, termcolor}; -use comemo::Prehashed; -use elsa::FrozenVec; -use memmap2::Mmap; -use notify::{RecommendedWatcher, RecursiveMode, Watcher}; -use once_cell::unsync::OnceCell; -use pico_args::Arguments; -use same_file::{is_same_file, Handle}; -use siphasher::sip128::{Hasher128, SipHasher}; -use termcolor::{ColorChoice, StandardStream, WriteColor}; -use walkdir::WalkDir; - -use typst::diag::{FileError, FileResult, SourceError, StrResult}; -use typst::font::{Font, FontBook, FontInfo, FontVariant}; -use typst::syntax::{Source, SourceId}; -use typst::util::{Buffer, PathExt}; -use typst::{Config, World}; - -type CodespanResult = Result; -type CodespanError = codespan_reporting::files::Error; - -/// What to do. -enum Command { - Typeset(TypesetCommand), - Fonts(FontsCommand), -} - -/// Typeset a .typ file into a PDF file. -struct TypesetCommand { - input: PathBuf, - output: PathBuf, - root: Option, - watch: bool, -} - -const HELP: &'static str = "\ -typst creates PDF files from .typ files - -USAGE: - typst [OPTIONS] [output.pdf] - typst [SUBCOMMAND] ... - -ARGS: - Path to input Typst file - [output.pdf] Path to output PDF file - -OPTIONS: - -h, --help Print this help - -w, --watch Watch the inputs and recompile on changes - --root Configure the root for absolute paths - -SUBCOMMANDS: - --fonts List all discovered system fonts -"; - -/// List discovered system fonts. -struct FontsCommand { - variants: bool, -} - -const HELP_FONTS: &'static str = "\ -typst --fonts lists all discovered system fonts - -USAGE: - typst --fonts [OPTIONS] - -OPTIONS: - -h, --help Print this help - --variants Also list style variants of each font family -"; - -/// Entry point. -fn main() { - let command = parse_args(); - let ok = command.is_ok(); - if let Err(msg) = command.and_then(dispatch) { - print_error(&msg).unwrap(); - if !ok { - println!("\nfor more information, try --help"); - } - process::exit(1); - } -} - -/// Parse command line arguments. -fn parse_args() -> StrResult { - let mut args = Arguments::from_env(); - let help = args.contains(["-h", "--help"]); - - let command = if args.contains("--fonts") { - if help { - print_help(HELP_FONTS); - } - - Command::Fonts(FontsCommand { variants: args.contains("--variants") }) - } else { - if help { - print_help(HELP); - } - - let root = args.opt_value_from_str("--root").map_err(|_| "missing root path")?; - let watch = args.contains(["-w", "--watch"]); - let (input, output) = parse_input_output(&mut args, "pdf")?; - Command::Typeset(TypesetCommand { input, output, watch, root }) - }; - - // Don't allow excess arguments. - let rest = args.finish(); - if !rest.is_empty() { - Err(format!( - "unexpected argument{}", - if rest.len() > 1 { "s" } else { "" } - ))?; - } - - Ok(command) -} - -/// Parse two freestanding path arguments, with the output path being optional. -/// If it is omitted, it is determined from the input path's file stem plus the -/// given extension. -fn parse_input_output(args: &mut Arguments, ext: &str) -> StrResult<(PathBuf, PathBuf)> { - let input: PathBuf = args.free_from_str().map_err(|_| "missing input file")?; - let output = match args.opt_free_from_str().ok().flatten() { - Some(output) => output, - None => { - let name = input.file_name().ok_or("source path does not point to a file")?; - Path::new(name).with_extension(ext) - } - }; - - // Ensure that the source file is not overwritten. - if is_same_file(&input, &output).unwrap_or(false) { - Err("source and destination files are the same")?; - } - - Ok((input, output)) -} - -/// Print a help string and quit. -fn print_help(help: &'static str) { - print!("{help}"); - std::process::exit(0); -} - -/// Print an application-level error (independent from a source file). -fn print_error(msg: &str) -> io::Result<()> { - let mut w = StandardStream::stderr(ColorChoice::Always); - let styles = term::Styles::default(); - - w.set_color(&styles.header_error)?; - write!(w, "error")?; - - w.reset()?; - writeln!(w, ": {msg}.") -} - -/// Dispatch a command. -fn dispatch(command: Command) -> StrResult<()> { - match command { - Command::Typeset(command) => typeset(command), - Command::Fonts(command) => fonts(command), - } -} - -/// Execute a typesetting command. -fn typeset(command: TypesetCommand) -> StrResult<()> { - let mut config = Config::default(); - if let Some(root) = &command.root { - config.root = root.clone(); - } else if let Some(dir) = command.input.parent() { - config.root = dir.into(); - } - - - // Create the world that serves sources, fonts and files. - let mut world = SystemWorld::new(config); - - // Typeset. - typeset_once(&mut world, &command)?; - - if !command.watch { - return Ok(()); - } - - // Setup file watching. - let (tx, rx) = std::sync::mpsc::channel(); - let mut watcher = RecommendedWatcher::new(tx, notify::Config::default()) - .map_err(|_| "failed to watch directory")?; - - // Watch this directory recursively. - watcher - .watch(Path::new("."), RecursiveMode::Recursive) - .map_err(|_| "failed to watch directory")?; - - // Handle events. - let timeout = std::time::Duration::from_millis(100); - 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")?; - if event - .paths - .iter() - .all(|path| is_same_file(path, &command.output).unwrap_or(false)) - { - continue; - } - - recompile |= world.relevant(&event); - } - - if recompile { - typeset_once(&mut world, &command)?; - } - } -} - -/// Typeset a single time. -fn typeset_once(world: &mut SystemWorld, command: &TypesetCommand) -> StrResult<()> { - status(command, Status::Compiling).unwrap(); - - world.reset(); - let main = world.resolve(&command.input).map_err(|err| err.to_string())?; - match typst::typeset(world, main) { - // Export the PDF. - Ok(frames) => { - let buffer = typst::export::pdf(&frames); - fs::write(&command.output, buffer).map_err(|_| "failed to write PDF file")?; - status(command, Status::Success).unwrap(); - } - - // Print diagnostics. - Err(errors) => { - status(command, Status::Error).unwrap(); - print_diagnostics(&world, *errors) - .map_err(|_| "failed to print diagnostics")?; - } - } - - Ok(()) -} - -/// Clear the terminal and render the status message. -fn status(command: &TypesetCommand, status: Status) -> io::Result<()> { - if !command.watch { - return Ok(()); - } - - let esc = 27 as char; - let input = command.input.display(); - let output = command.output.display(); - let time = chrono::offset::Local::now(); - let timestamp = time.format("%H:%M:%S"); - let message = status.message(); - let color = status.color(); - - let mut w = StandardStream::stderr(ColorChoice::Always); - write!(w, "{esc}c{esc}[1;1H")?; - - w.set_color(&color)?; - write!(w, "watching")?; - w.reset()?; - writeln!(w, " {input}")?; - - w.set_color(&color)?; - write!(w, "writing to")?; - w.reset()?; - writeln!(w, " {output}")?; - - writeln!(w)?; - writeln!(w, "[{timestamp}] {message}")?; - writeln!(w)?; - - w.flush() -} - -/// The status in which the watcher can be. -enum Status { - Compiling, - Success, - Error, -} - -impl Status { - fn message(&self) -> &str { - match self { - Self::Compiling => "compiling ...", - Self::Success => "compiled successfully", - Self::Error => "compiled with errors", - } - } - - fn color(&self) -> termcolor::ColorSpec { - let styles = term::Styles::default(); - match self { - Self::Error => styles.header_error, - _ => styles.header_note, - } - } -} - -/// Print diagnostic messages to the terminal. -fn print_diagnostics( - world: &SystemWorld, - errors: Vec, -) -> Result<(), codespan_reporting::files::Error> { - let mut w = StandardStream::stderr(ColorChoice::Always); - let config = term::Config { tab_width: 2, ..Default::default() }; - - for error in errors { - // The main diagnostic. - let range = error.range(world); - let diag = Diagnostic::error() - .with_message(error.message) - .with_labels(vec![Label::primary(error.span.source(), range)]); - - 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.source(), - world.source(point.span.source()).range(point.span), - ), - ]); - - term::emit(&mut w, &config, world, &help)?; - } - } - - Ok(()) -} - -/// Execute a font listing command. -fn fonts(command: FontsCommand) -> StrResult<()> { - let mut searcher = FontSearcher::new(); - searcher.search_system(); - 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(()) -} - -/// A world that provides access to the operating system. -struct SystemWorld { - config: Prehashed, - book: Prehashed, - fonts: Vec, - hashes: RefCell>>, - paths: RefCell>, - sources: FrozenVec>, -} - -/// Holds details about the location of a font and lazily the font itself. -struct FontSlot { - path: PathBuf, - index: u32, - font: OnceCell>, -} - -/// Holds canonical data for all paths pointing to the same entity. -#[derive(Default)] -struct PathSlot { - source: OnceCell>, - buffer: OnceCell>, -} - -impl SystemWorld { - fn new(config: Config) -> Self { - let mut searcher = FontSearcher::new(); - searcher.search_system(); - - Self { - config: Prehashed::new(config), - book: Prehashed::new(searcher.book), - fonts: searcher.fonts, - hashes: RefCell::default(), - paths: RefCell::default(), - sources: FrozenVec::new(), - } - } -} - -impl World for SystemWorld { - fn config(&self) -> &Prehashed { - &self.config - } - - fn book(&self) -> &Prehashed { - &self.book - } - - fn font(&self, id: usize) -> Option { - let slot = &self.fonts[id]; - slot.font - .get_or_init(|| { - let data = self.file(&slot.path).ok()?; - Font::new(data, slot.index) - }) - .clone() - } - - fn file(&self, path: &Path) -> FileResult { - self.slot(path)? - .buffer - .get_or_init(|| read(path).map(Buffer::from)) - .clone() - } - - fn resolve(&self, path: &Path) -> FileResult { - self.slot(path)? - .source - .get_or_init(|| { - let buf = read(path)?; - let text = String::from_utf8(buf)?; - Ok(self.insert(path, text)) - }) - .clone() - } - - fn source(&self, id: SourceId) -> &Source { - &self.sources[id.into_u16() as usize] - } -} - -impl SystemWorld { - fn slot(&self, path: &Path) -> FileResult> { - let mut hashes = self.hashes.borrow_mut(); - let hash = match hashes.get(path).cloned() { - Some(hash) => hash, - None => { - let hash = PathHash::new(path); - if let Ok(canon) = path.canonicalize() { - hashes.insert(canon.normalize(), hash.clone()); - } - hashes.insert(path.into(), hash.clone()); - hash - } - }?; - - Ok(std::cell::RefMut::map(self.paths.borrow_mut(), |paths| { - paths.entry(hash).or_default() - })) - } - - fn insert(&self, path: &Path, text: String) -> SourceId { - let id = SourceId::from_u16(self.sources.len() as u16); - let source = Source::new(id, path, text); - self.sources.push(Box::new(source)); - id - } - - fn relevant(&mut self, event: ¬ify::Event) -> bool { - match &event.kind { - notify::EventKind::Any => {} - notify::EventKind::Access(_) => return false, - notify::EventKind::Create(_) => return true, - notify::EventKind::Modify(kind) => match kind { - notify::event::ModifyKind::Any => {} - notify::event::ModifyKind::Data(_) => {} - notify::event::ModifyKind::Metadata(_) => return false, - notify::event::ModifyKind::Name(_) => return true, - notify::event::ModifyKind::Other => return false, - }, - notify::EventKind::Remove(_) => {} - notify::EventKind::Other => return false, - } - - event.paths.iter().any(|path| self.dependant(path)) - } - - fn dependant(&self, path: &Path) -> bool { - self.hashes.borrow().contains_key(&path.normalize()) - || PathHash::new(path) - .map_or(false, |hash| self.paths.borrow().contains_key(&hash)) - } - - fn reset(&mut self) { - self.sources.as_mut().clear(); - self.hashes.borrow_mut().clear(); - self.paths.borrow_mut().clear(); - } -} - -/// 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 { - let f = |e| FileError::from_io(e, path); - let handle = Handle::from_path(path).map_err(f)?; - let mut state = SipHasher::new(); - handle.hash(&mut state); - Ok(Self(state.finish128().as_u128())) - } -} - -/// Read a file. -fn read(path: &Path) -> FileResult> { - let f = |e| FileError::from_io(e, path); - let mut file = File::open(path).map_err(f)?; - if file.metadata().map_err(f)?.is_file() { - let mut data = vec![]; - file.read_to_end(&mut data).map_err(f)?; - Ok(data) - } else { - Err(FileError::IsDirectory) - } -} - -impl<'a> codespan_reporting::files::Files<'a> for SystemWorld { - type FileId = SourceId; - type Name = std::path::Display<'a>; - type Source = &'a str; - - fn name(&'a self, id: SourceId) -> CodespanResult { - Ok(World::source(self, id).path().display()) - } - - fn source(&'a self, id: SourceId) -> CodespanResult { - Ok(World::source(self, id).text()) - } - - fn line_index(&'a self, id: SourceId, given: usize) -> CodespanResult { - let source = World::source(self, id); - source - .byte_to_line(given) - .ok_or_else(|| CodespanError::IndexTooLarge { - given, - max: source.len_bytes(), - }) - } - - fn line_range( - &'a self, - id: SourceId, - given: usize, - ) -> CodespanResult> { - let source = World::source(self, id); - source - .line_to_range(given) - .ok_or_else(|| CodespanError::LineTooLarge { given, max: source.len_lines() }) - } - - fn column_number( - &'a self, - id: SourceId, - _: usize, - given: usize, - ) -> CodespanResult { - let source = World::source(self, 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 } - } - }) - } -} - -/// Searches for fonts. -struct FontSearcher { - book: FontBook, - fonts: Vec, -} - -impl FontSearcher { - /// Create a new, empty system searcher. - fn new() -> Self { - Self { book: FontBook::new(), fonts: vec![] } - } - - /// Search for fonts in the linux system font directories. - #[cfg(all(unix, not(target_os = "macos")))] - fn search_system(&mut self) { - self.search_dir("/usr/share/fonts"); - self.search_dir("/usr/local/share/fonts"); - - if let Some(dir) = dirs::font_dir() { - self.search_dir(dir); - } - } - - /// Search for fonts in the macOS system font directories. - #[cfg(target_os = "macos")] - fn search_system(&mut self) { - self.search_dir("/Library/Fonts"); - self.search_dir("/Network/Library/Fonts"); - self.search_dir("/System/Library/Fonts"); - - if let Some(dir) = dirs::font_dir() { - self.search_dir(dir); - } - } - - /// Search for fonts in the Windows system font directories. - #[cfg(windows)] - fn search_system(&mut self) { - let windir = - std::env::var("WINDIR").unwrap_or_else(|_| "C:\\Windows".to_string()); - - self.search_dir(Path::new(&windir).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")); - } - } - - /// Search for all fonts in a directory. - /// recursively. - fn search_dir(&mut self, path: impl AsRef) { - 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: impl AsRef) { - let path = path.as_ref(); - if let Ok(file) = File::open(path) { - if let Ok(mmap) = unsafe { Mmap::map(&file) } { - for (i, info) in FontInfo::from_data(&mmap).enumerate() { - self.book.push(info); - self.fonts.push(FontSlot { - path: path.into(), - index: i as u32, - font: OnceCell::new(), - }); - } - } - } - } -} diff --git a/src/model/args.rs b/src/model/args.rs index f95fbf08..9fb30b9c 100644 --- a/src/model/args.rs +++ b/src/model/args.rs @@ -1,7 +1,7 @@ use std::fmt::{self, Debug, Formatter, Write}; use super::{Array, Cast, Dict, Str, Value}; -use crate::diag::{At, SourceResult}; +use crate::diag::{bail, At, SourceResult}; use crate::syntax::{Span, Spanned}; /// Evaluated arguments to a function. diff --git a/src/model/array.rs b/src/model/array.rs index 196f02ec..053248ec 100644 --- a/src/model/array.rs +++ b/src/model/array.rs @@ -9,8 +9,9 @@ use crate::syntax::Spanned; use crate::util::ArcExt; /// Create a new [`Array`] from values. -#[allow(unused_macros)] -macro_rules! array { +#[macro_export] +#[doc(hidden)] +macro_rules! __array { ($value:expr; $count:expr) => { $crate::model::Array::from_vec(vec![$value.into(); $count]) }; @@ -20,6 +21,9 @@ macro_rules! array { }; } +#[doc(inline)] +pub use crate::__array as array; + /// A reference counted array with value semantics. #[derive(Default, Clone, PartialEq, Hash)] pub struct Array(Arc>); @@ -97,7 +101,7 @@ impl Array { .ok_or_else(|| out_of_bounds(index, len))?; Arc::make_mut(&mut self.0).remove(i); - return Ok(()); + Ok(()) } /// Extract a contigous subregion of the array. diff --git a/src/model/cast.rs b/src/model/cast.rs index 7356ef70..cbb2952d 100644 --- a/src/model/cast.rs +++ b/src/model/cast.rs @@ -1,8 +1,13 @@ use std::num::NonZeroUsize; +use std::str::FromStr; use super::{Pattern, Regex, Value}; use crate::diag::{with_alternative, StrResult}; -use crate::geom::{Corners, Dir, Paint, Sides}; +use crate::font::{FontStretch, FontStyle, FontWeight}; +use crate::frame::{Destination, Lang, Location, Region}; +use crate::geom::{ + Axes, Corners, Dir, GenAlign, Get, Length, Paint, PartialStroke, Point, Rel, Sides, +}; use crate::syntax::Spanned; use crate::util::EcoString; @@ -16,7 +21,9 @@ pub trait Cast: Sized { } /// Implement traits for dynamic types. -macro_rules! dynamic { +#[macro_export] +#[doc(hidden)] +macro_rules! __dynamic { ($type:ty: $name:literal, $($tts:tt)*) => { impl $crate::model::Type for $type { const TYPE_NAME: &'static str = $name; @@ -37,8 +44,13 @@ macro_rules! dynamic { }; } +#[doc(inline)] +pub use crate::__dynamic as dynamic; + /// Make a type castable from a value. -macro_rules! castable { +#[macro_export] +#[doc(hidden)] +macro_rules! __castable { ($type:ty: $inner:ty) => { impl $crate::model::Cast<$crate::model::Value> for $type { fn is(value: &$crate::model::Value) -> bool { @@ -88,6 +100,9 @@ macro_rules! castable { }; } +#[doc(inline)] +pub use crate::__castable as castable; + impl Cast for Value { fn is(_: &Value) -> bool { true @@ -119,14 +134,6 @@ impl Cast> for Spanned { } } -dynamic! { - Dir: "direction", -} - -dynamic! { - Regex: "regular expression", -} - castable! { usize, Expected: "non-negative integer", @@ -170,6 +177,10 @@ castable! { Value::Str(string) => string.into(), } +dynamic! { + Regex: "regular expression", +} + castable! { Pattern, Expected: "function, string or regular expression", @@ -178,6 +189,115 @@ castable! { @regex: Regex => Self::Regex(regex.clone()), } +dynamic! { + Dir: "direction", +} + +dynamic! { + GenAlign: "alignment", +} + +dynamic! { + Axes: "2d alignment", +} + +castable! { + Axes>, + Expected: "1d or 2d alignment", + @align: GenAlign => { + let mut aligns = Axes::default(); + aligns.set(align.axis(), Some(*align)); + aligns + }, + @aligns: Axes => aligns.map(Some), +} + +dynamic! { + PartialStroke: "stroke", + Value::Length(thickness) => Self { + paint: Smart::Auto, + thickness: Smart::Custom(thickness), + }, + Value::Color(color) => Self { + paint: Smart::Custom(color.into()), + thickness: Smart::Auto, + }, +} + +castable! { + Axes>, + Expected: "array of two relative lengths", + 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()?), + _ => Err("point array must contain exactly two entries")?, + } + }, +} + +castable! { + Destination, + Expected: "string or dictionary with `page`, `x`, and `y` keys", + Value::Str(string) => Self::Url(string.into()), + Value::Dict(dict) => { + let page = dict.get("page")?.clone().cast()?; + let x: Length = dict.get("x")?.clone().cast()?; + let y: Length = dict.get("y")?.clone().cast()?; + Self::Internal(Location { page, pos: Point::new(x.abs, y.abs) }) + }, +} + +castable! { + FontStyle, + Expected: "string", + Value::Str(string) => match string.as_str() { + "normal" => Self::Normal, + "italic" => Self::Italic, + "oblique" => Self::Oblique, + _ => Err(r#"expected "normal", "italic" or "oblique""#)?, + }, +} + +castable! { + FontWeight, + Expected: "integer or string", + Value::Int(v) => Value::Int(v) + .cast::()? + .try_into() + .map_or(Self::BLACK, Self::from_number), + Value::Str(string) => match string.as_str() { + "thin" => Self::THIN, + "extralight" => Self::EXTRALIGHT, + "light" => Self::LIGHT, + "regular" => Self::REGULAR, + "medium" => Self::MEDIUM, + "semibold" => Self::SEMIBOLD, + "bold" => Self::BOLD, + "extrabold" => Self::EXTRABOLD, + "black" => Self::BLACK, + _ => Err("unknown font weight")?, + }, +} + +castable! { + FontStretch, + Expected: "ratio", + Value::Ratio(v) => Self::from_ratio(v.get() as f32), +} + +castable! { + Lang, + Expected: "string", + Value::Str(string) => Self::from_str(&string)?, +} + +castable! { + Region, + Expected: "string", + Value::Str(string) => Self::from_str(&string)?, +} + impl Cast for Option { fn is(value: &Value) -> bool { matches!(value, Value::None) || T::is(value) diff --git a/src/model/content.rs b/src/model/content.rs index 1cffa773..372f6ff6 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -9,8 +9,9 @@ use siphasher::sip128::{Hasher128, SipHasher}; use typst_macros::node; use super::{Args, Key, Property, Selector, StyleEntry, StyleMap, Vm}; +use crate as typst; use crate::diag::{SourceResult, StrResult}; -use crate::util::ReadableTypeId; +use crate::util::{EcoString, ReadableTypeId}; /// Composable representation of styled content. /// @@ -26,6 +27,11 @@ impl Content { SequenceNode(vec![]).pack() } + /// Create content from a string of text. + pub fn text(text: impl Into) -> Self { + item!(text)(text.into()) + } + /// Create a new sequence node from multiples nodes. pub fn sequence(seq: Vec) -> Self { match seq.as_slice() { @@ -146,7 +152,7 @@ impl Add for Content { let mut lhs = self; if let Some(lhs_mut) = lhs.try_downcast_mut::() { if let Some(rhs_mut) = rhs.try_downcast_mut::() { - lhs_mut.0.extend(rhs_mut.0.drain(..)); + lhs_mut.0.append(&mut rhs_mut.0); } else if let Some(rhs) = rhs.downcast::() { lhs_mut.0.extend(rhs.0.iter().cloned()); } else { diff --git a/src/model/dict.rs b/src/model/dict.rs index 3e4fd956..49e50aa0 100644 --- a/src/model/dict.rs +++ b/src/model/dict.rs @@ -10,8 +10,9 @@ use crate::syntax::Spanned; use crate::util::ArcExt; /// Create a new [`Dict`] from key-value pairs. -#[allow(unused_macros)] -macro_rules! dict { +#[macro_export] +#[doc(hidden)] +macro_rules! __dict { ($($key:expr => $value:expr),* $(,)?) => {{ #[allow(unused_mut)] let mut map = std::collections::BTreeMap::new(); @@ -20,6 +21,9 @@ macro_rules! dict { }}; } +#[doc(inline)] +pub use crate::__dict as dict; + /// A reference-counted dictionary with value semantics. #[derive(Default, Clone, PartialEq, Hash)] pub struct Dict(Arc>); diff --git a/src/model/eval.rs b/src/model/eval.rs index 02617ed6..8e287f14 100644 --- a/src/model/eval.rs +++ b/src/model/eval.rs @@ -1,22 +1,19 @@ //! Evaluation of markup into modules. use std::collections::BTreeMap; -use std::sync::Arc; use comemo::{Track, Tracked}; use unicode_segmentation::UnicodeSegmentation; use super::{ methods, ops, Arg, Args, Array, CapturesVisitor, Closure, Content, Dict, Flow, Func, - Node, Pattern, Recipe, Scope, Scopes, Show, StyleEntry, StyleMap, Value, Vm, + Pattern, Recipe, Scope, Scopes, Show, StyleEntry, StyleMap, Value, Vm, }; -use crate::diag::{At, SourceResult, StrResult, Trace, Tracepoint}; +use crate::diag::{bail, error, At, SourceResult, StrResult, Trace, Tracepoint}; use crate::geom::{Abs, Angle, Em, Fr, Ratio}; -use crate::library::math; -use crate::library::text::TextNode; use crate::syntax::ast::TypedNode; use crate::syntax::{ast, SourceId, Span, Spanned, Unit}; -use crate::util::EcoString; +use crate::util::{format_eco, EcoString}; use crate::World; /// Evaluate a source file and return the resulting module. @@ -39,7 +36,7 @@ pub fn eval( // Evaluate the module. let route = unsafe { Route::insert(route, id) }; let ast = world.source(id).ast()?; - let std = &world.config().std; + let std = &world.config().scope; let scopes = Scopes::new(Some(std)); let mut vm = Vm::new(world, route.track(), Some(id), scopes); let result = ast.eval(&mut vm); @@ -136,8 +133,7 @@ fn eval_markup( break; } - eval_markup(vm, nodes)? - .styled_with_entry(StyleEntry::Recipe(recipe).into()) + eval_markup(vm, nodes)?.styled_with_entry(StyleEntry::Recipe(recipe)) } ast::MarkupNode::Expr(ast::Expr::Wrap(wrap)) => { let tail = eval_markup(vm, nodes)?; @@ -165,10 +161,13 @@ impl Eval for ast::MarkupNode { fn eval(&self, vm: &mut Vm) -> SourceResult { match self { - Self::Space(v) => v.eval(vm), + Self::Space(v) => Ok(match v.newlines() { + 0 ..= 1 => (vm.items.space)(), + _ => (vm.items.parbreak)(), + }), Self::Linebreak(v) => v.eval(vm), Self::Text(v) => v.eval(vm), - Self::Escape(v) => v.eval(vm), + Self::Escape(v) => Ok((vm.items.text)(v.get().into())), Self::Shorthand(v) => v.eval(vm), Self::SmartQuote(v) => v.eval(vm), Self::Strong(v) => v.eval(vm), @@ -187,23 +186,11 @@ impl Eval for ast::MarkupNode { } } -impl Eval for ast::Space { - type Output = Content; - - fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok(if self.newlines() < 2 { - (vm.items().space)() - } else { - (vm.items().parbreak)() - }) - } -} - impl Eval for ast::Linebreak { type Output = Content; fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items().linebreak)(false)) + Ok((vm.items.linebreak)(false)) } } @@ -211,15 +198,7 @@ impl Eval for ast::Text { type Output = Content; fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok(vm.text(self.get().clone())) - } -} - -impl Eval for ast::Escape { - type Output = Content; - - fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok(vm.text(self.get())) + Ok((vm.items.text)(self.get().clone())) } } @@ -227,7 +206,7 @@ impl Eval for ast::Shorthand { type Output = Content; fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok(vm.text(self.get())) + Ok((vm.items.text)(self.get().into())) } } @@ -235,7 +214,7 @@ impl Eval for ast::SmartQuote { type Output = Content; fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items().smart_quote)(self.double())) + Ok((vm.items.smart_quote)(self.double())) } } @@ -243,7 +222,7 @@ impl Eval for ast::Strong { type Output = Content; fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items().strong)(self.body().eval(vm)?)) + Ok((vm.items.strong)(self.body().eval(vm)?)) } } @@ -251,7 +230,7 @@ impl Eval for ast::Emph { type Output = Content; fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items().emph)(self.body().eval(vm)?)) + Ok((vm.items.emph)(self.body().eval(vm)?)) } } @@ -262,7 +241,7 @@ impl Eval for ast::Raw { let text = self.text().clone(); let lang = self.lang().cloned(); let block = self.block(); - Ok((vm.items().raw)(text, lang, block)) + Ok((vm.items.raw)(text, lang, block)) } } @@ -270,7 +249,7 @@ impl Eval for ast::Link { type Output = Content; fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items().link)(self.url().clone())) + Ok((vm.items.link)(self.url().clone())) } } @@ -286,7 +265,7 @@ impl Eval for ast::Ref { type Output = Content; fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items().ref_)(self.get().clone())) + Ok((vm.items.ref_)(self.get().clone())) } } @@ -296,7 +275,7 @@ impl Eval for ast::Heading { fn eval(&self, vm: &mut Vm) -> SourceResult { let level = self.level(); let body = self.body().eval(vm)?; - Ok((vm.items().heading)(level, body)) + Ok((vm.items.heading)(level, body)) } } @@ -304,7 +283,7 @@ impl Eval for ast::ListItem { type Output = Content; fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items().list_item)(self.body().eval(vm)?)) + Ok((vm.items.list_item)(self.body().eval(vm)?)) } } @@ -314,7 +293,7 @@ impl Eval for ast::EnumItem { fn eval(&self, vm: &mut Vm) -> SourceResult { let number = self.number(); let body = self.body().eval(vm)?; - Ok((vm.items().enum_item)(number, body)) + Ok((vm.items.enum_item)(number, body)) } } @@ -324,7 +303,7 @@ impl Eval for ast::DescItem { fn eval(&self, vm: &mut Vm) -> SourceResult { let term = self.term().eval(vm)?; let body = self.body().eval(vm)?; - Ok((vm.items().desc_item)(term, body)) + Ok((vm.items.desc_item)(term, body)) } } @@ -332,82 +311,76 @@ impl Eval for ast::Math { type Output = Content; fn eval(&self, vm: &mut Vm) -> SourceResult { - let nodes = self - .children() - .map(|node| node.eval(vm)) - .collect::>()?; - Ok(math::MathNode::Row(Arc::new(nodes), self.span()).pack()) + Ok((vm.items.math)( + self.children() + .map(|node| node.eval(vm)) + .collect::>()?, + self.display(), + )) } } impl Eval for ast::MathNode { - type Output = math::MathNode; + type Output = Content; fn eval(&self, vm: &mut Vm) -> SourceResult { Ok(match self { - Self::Space(_) => math::MathNode::Space, - Self::Linebreak(_) => math::MathNode::Linebreak, - Self::Escape(c) => math::MathNode::Atom(c.get().into()), - Self::Atom(atom) => math::MathNode::Atom(atom.get().clone()), - Self::Script(node) => node.eval(vm)?, - Self::Frac(node) => node.eval(vm)?, - Self::Align(node) => node.eval(vm)?, - Self::Group(node) => math::MathNode::Row( - Arc::new( - node.children() - .map(|node| node.eval(vm)) - .collect::>()?, - ), - node.span(), - ), - Self::Expr(expr) => { - let content = expr.eval(vm)?.display(vm.world); - if let Some(node) = content.downcast::() { - math::MathNode::Atom(node.0.clone()) - } else { - bail!(expr.span(), "expected text") - } - } + Self::Space(_) => (vm.items.space)(), + Self::Linebreak(v) => v.eval(vm)?, + Self::Escape(v) => (vm.items.math_atom)(v.get().into()), + Self::Atom(v) => v.eval(vm)?, + Self::Script(v) => v.eval(vm)?, + Self::Frac(v) => v.eval(vm)?, + Self::Align(v) => v.eval(vm)?, + Self::Group(v) => v.eval(vm)?, + Self::Expr(v) => match v.eval(vm)? { + Value::None => Content::empty(), + Value::Int(v) => (vm.items.math_atom)(format_eco!("{}", v)), + Value::Float(v) => (vm.items.math_atom)(format_eco!("{}", v)), + Value::Str(v) => (vm.items.math_atom)(v.into()), + Value::Content(v) => v, + _ => bail!(v.span(), "unexpected garbage"), + }, }) } } +impl Eval for ast::Atom { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult { + Ok((vm.items.math_atom)(self.get().clone())) + } +} + impl Eval for ast::Script { - type Output = math::MathNode; + type Output = Content; fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok(math::MathNode::Script(Arc::new(math::ScriptNode { - base: self.base().eval(vm)?, - sub: self - .sub() - .map(|node| node.eval(vm)) - .transpose()? - .map(|node| node.unparen()), - sup: self - .sup() - .map(|node| node.eval(vm)) - .transpose()? - .map(|node| node.unparen()), - }))) + Ok((vm.items.math_script)( + self.base().eval(vm)?, + self.sub().map(|node| node.eval(vm)).transpose()?, + self.sup().map(|node| node.eval(vm)).transpose()?, + )) } } impl Eval for ast::Frac { - type Output = math::MathNode; + type Output = Content; fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok(math::MathNode::Frac(Arc::new(math::FracNode { - num: self.num().eval(vm)?.unparen(), - denom: self.denom().eval(vm)?.unparen(), - }))) + Ok((vm.items.math_frac)( + self.num().eval(vm)?, + self.denom().eval(vm)?, + )) } } impl Eval for ast::Align { - type Output = math::MathNode; + type Output = Content; - fn eval(&self, _: &mut Vm) -> SourceResult { - Ok(math::MathNode::Align(self.count())) + fn eval(&self, vm: &mut Vm) -> SourceResult { + Ok((vm.items.math_align)(self.count())) } } @@ -515,7 +488,7 @@ fn eval_code( } ast::Expr::Show(show) => { let recipe = show.eval(vm)?; - let entry = StyleEntry::Recipe(recipe).into(); + let entry = StyleEntry::Recipe(recipe); if vm.flow.is_some() { break; } @@ -627,7 +600,7 @@ impl Eval for ast::Unary { ast::UnOp::Neg => ops::neg(value), ast::UnOp::Not => ops::not(value), }; - Ok(result.at(self.span())?) + result.at(self.span()) } } @@ -676,7 +649,7 @@ impl ast::Binary { } let rhs = self.rhs().eval(vm)?; - Ok(op(lhs, rhs).at(self.span())?) + op(lhs, rhs).at(self.span()) } /// Apply an assignment operation. @@ -708,8 +681,7 @@ impl Eval for ast::FieldAccess { .to::() .and_then(|node| node.field(&field)) .ok_or_else(|| format!("unknown field {field:?}")) - .at(span)? - .clone(), + .at(span)?, v => bail!( self.target().span(), @@ -754,9 +726,8 @@ impl Eval for ast::MethodCall { Ok(if methods::is_mutating(&method) { let args = self.args().eval(vm)?; - let mut value = self.target().access(vm)?; - methods::call_mut(&mut value, &method, args, span) - .trace(vm.world, point, span)?; + let value = self.target().access(vm)?; + methods::call_mut(value, &method, args, span).trace(vm.world, point, span)?; Value::None } else { let value = self.target().eval(vm)?; @@ -882,7 +853,7 @@ impl Eval for ast::SetRule { let target = self.target(); let target = target.eval(vm)?.cast::().at(target.span())?; let args = self.args().eval(vm)?; - Ok(target.set(args)?) + target.set(args) } } @@ -1085,14 +1056,14 @@ impl Eval for ast::ModuleInclude { let span = self.path().span(); let path = self.path().eval(vm)?.cast::().at(span)?; let module = import(vm, &path, span)?; - Ok(module.content.clone()) + Ok(module.content) } } /// Process an import of a module relative to the current location. fn import(vm: &mut Vm, path: &str, span: Span) -> SourceResult { // Load the source file. - let full = vm.locate(&path).at(span)?; + let full = vm.locate(path).at(span)?; let id = vm.world.resolve(&full).at(span)?; // Prevent cyclic importing. diff --git a/src/model/func.rs b/src/model/func.rs index dff58233..5be1aae3 100644 --- a/src/model/func.rs +++ b/src/model/func.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use comemo::{Track, Tracked}; use super::{Args, Eval, Flow, Node, NodeId, Route, Scope, Scopes, StyleMap, Value, Vm}; -use crate::diag::{SourceResult, StrResult}; +use crate::diag::{bail, SourceResult, StrResult}; use crate::syntax::ast::{self, Expr, TypedNode}; use crate::syntax::{SourceId, SyntaxNode}; use crate::util::EcoString; @@ -229,7 +229,7 @@ impl Closure { } /// A visitor that determines which variables to capture for a closure. -pub struct CapturesVisitor<'a> { +pub(super) struct CapturesVisitor<'a> { external: &'a Scopes<'a>, internal: Scopes<'a>, captures: Scope, diff --git a/src/model/items.rs b/src/model/items.rs new file mode 100644 index 00000000..164d9602 --- /dev/null +++ b/src/model/items.rs @@ -0,0 +1,123 @@ +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::num::NonZeroUsize; + +use comemo::Tracked; +use once_cell::sync::OnceCell; + +use super::{Content, StyleChain}; +use crate::diag::SourceResult; +use crate::frame::Frame; +use crate::geom::{Abs, Dir}; +use crate::util::{hash128, EcoString}; +use crate::World; + +/// Global storage for lang items. +#[doc(hidden)] +pub static LANG_ITEMS: OnceCell = OnceCell::new(); + +/// Set the lang items. This is a hack :( +/// +/// Passing the lang items everywhere they are needed (especially the text node +/// 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. +pub fn set_lang_items(items: LangItems) { + if LANG_ITEMS.set(items).is_err() { + 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::model::LANG_ITEMS.get().unwrap().$name + }; +} + +/// Definition of certain standard library items the language is aware of. +#[derive(Copy, Clone)] +pub struct LangItems { + /// The root layout function. + pub root: + fn(world: Tracked, document: &Content) -> SourceResult>, + /// Access the em size. + pub em: fn(StyleChain) -> Abs, + /// Access the text direction. + pub dir: fn(StyleChain) -> Dir, + /// A space. + pub space: fn() -> Content, + /// A forced line break. + pub linebreak: fn(justify: bool) -> Content, + /// Plain text. + pub text: fn(text: EcoString) -> Content, + /// 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, + /// A raw block with optional syntax highlighting: `` `...` ``. + pub raw: fn(text: EcoString, tag: Option, block: bool) -> Content, + /// A hyperlink: `https://typst.org`. + pub link: fn(url: EcoString) -> Content, + /// A reference: `@target`. + pub ref_: fn(target: EcoString) -> Content, + /// A section heading: `= Introduction`. + pub heading: fn(level: NonZeroUsize, body: Content) -> Content, + /// An item in an unordered list: `- ...`. + pub list_item: fn(body: Content) -> Content, + /// An item in an enumeration (ordered list): `1. ...`. + pub enum_item: fn(number: Option, body: Content) -> Content, + /// An item in a description list: `/ Term: Details`. + pub desc_item: fn(term: Content, body: Content) -> Content, + /// A math formula: `$x$`, `$ x^2 $`. + pub math: fn(children: Vec, display: bool) -> Content, + /// A atom in a formula: `x`, `+`, `12`. + pub math_atom: fn(atom: EcoString) -> Content, + /// A base with an optional sub- and superscript in a formula: `a_1^2`. + pub math_script: + fn(base: Content, sub: Option, sup: Option) -> Content, + /// A fraction in a formula: `x/2` + pub math_frac: fn(num: Content, denom: Content) -> Content, + /// An alignment indicator in a formula: `&`, `&&`. + pub math_align: fn(count: usize) -> Content, +} + +impl Debug for LangItems { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("LangItems { .. }") + } +} + +impl Hash for LangItems { + fn hash(&self, state: &mut H) { + (self.root 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.smart_quote.hash(state); + self.parbreak.hash(state); + self.strong.hash(state); + self.emph.hash(state); + self.raw.hash(state); + self.link.hash(state); + self.ref_.hash(state); + self.heading.hash(state); + self.list_item.hash(state); + self.enum_item.hash(state); + self.desc_item.hash(state); + self.math.hash(state); + self.math_atom.hash(state); + self.math_script.hash(state); + self.math_frac.hash(state); + self.math_align.hash(state); + } +} diff --git a/src/model/methods.rs b/src/model/methods.rs index 07cbb822..26d27dfa 100644 --- a/src/model/methods.rs +++ b/src/model/methods.rs @@ -97,7 +97,7 @@ pub fn call( }, Value::Func(func) => match method { - "with" => Value::Func(func.clone().with(args.take())), + "with" => Value::Func(func.with(args.take())), _ => return missing(), }, diff --git a/src/model/mod.rs b/src/model/mod.rs index b4f8f653..fdebce0a 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,5 +1,7 @@ //! Layout and computation model. +#[macro_use] +mod items; #[macro_use] mod cast; #[macro_use] @@ -16,12 +18,11 @@ mod args; mod content; mod eval; mod func; +mod methods; +mod ops; mod scope; mod vm; -pub mod methods; -pub mod ops; - pub use self::str::*; pub use args::*; pub use array::*; @@ -30,6 +31,7 @@ pub use content::*; pub use dict::*; pub use eval::*; pub use func::*; +pub use items::*; pub use scope::*; pub use styles::*; pub use value::*; diff --git a/src/model/ops.rs b/src/model/ops.rs index 7eb814c1..ee126b03 100644 --- a/src/model/ops.rs +++ b/src/model/ops.rs @@ -1,12 +1,9 @@ //! Operations on values. -use std::cmp::Ordering; - -use super::{Node, Regex, Smart, Value}; +use super::{Regex, Smart, Value}; use crate::diag::StrResult; -use crate::geom::{Axes, Axis, Length, Numeric, Rel}; -use crate::library::text::TextNode; -use crate::library::{RawAlign, RawStroke}; +use crate::geom::{Axes, Axis, GenAlign, Length, Numeric, PartialStroke, Rel}; +use std::cmp::Ordering; use Value::*; /// Bail with a type mismatch error. @@ -22,8 +19,8 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult { (a, None) => a, (None, b) => b, (Str(a), Str(b)) => Str(a + b), - (Str(a), Content(b)) => Content(TextNode(a.into()).pack() + b), - (Content(a), Str(b)) => Content(a + TextNode(b.into()).pack()), + (Str(a), Content(b)) => Content(super::Content::text(a) + b), + (Content(a), Str(b)) => Content(a + super::Content::text(b)), (Content(a), Content(b)) => Content(a + b), (Array(a), Array(b)) => Array(a + b), (Dict(a), Dict(b)) => Dict(a + b), @@ -88,14 +85,14 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult { (Str(a), Str(b)) => Str(a + b), (Content(a), Content(b)) => Content(a + b), - (Content(a), Str(b)) => Content(a + TextNode(b.into()).pack()), - (Str(a), Content(b)) => Content(TextNode(a.into()).pack() + b), + (Content(a), Str(b)) => Content(a + super::Content::text(b)), + (Str(a), Content(b)) => Content(super::Content::text(a) + 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(RawStroke { + Value::dynamic(PartialStroke { paint: Smart::Custom(color.into()), thickness: Smart::Custom(thickness), }) @@ -104,7 +101,7 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult { (Dyn(a), Dyn(b)) => { // 1D alignments can be summed into 2D alignments. if let (Some(&a), Some(&b)) = - (a.downcast::(), b.downcast::()) + (a.downcast::(), b.downcast::()) { if a.axis() != b.axis() { Value::dynamic(match a.axis() { diff --git a/src/model/str.rs b/src/model/str.rs index 843da9a8..4aa40c54 100644 --- a/src/model/str.rs +++ b/src/model/str.rs @@ -5,19 +5,23 @@ use std::ops::{Add, AddAssign, Deref}; use unicode_segmentation::UnicodeSegmentation; -use super::{Array, Dict, Value}; +use super::{castable, dict, Array, Dict, Value}; use crate::diag::StrResult; -use crate::library::RawAlign; +use crate::geom::GenAlign; use crate::util::EcoString; /// Create a new [`Str`] from a format string. -#[allow(unused_macros)] -macro_rules! format_str { +#[macro_export] +#[doc(hidden)] +macro_rules! __format_str { ($($tts:tt)*) => {{ $crate::model::Str::from(format_eco!($($tts)*)) }}; } +#[doc(inline)] +pub use crate::__format_str as format_str; + /// An immutable reference counted string. #[derive(Default, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Str(EcoString); @@ -463,9 +467,9 @@ pub enum StrSide { castable! { StrSide, Expected: "start or end", - @align: RawAlign => match align { - RawAlign::Start => Self::Start, - RawAlign::End => Self::End, + @align: GenAlign => match align { + GenAlign::Start => Self::Start, + GenAlign::End => Self::End, _ => Err("expected either `start` or `end`")?, }, } diff --git a/src/model/styles.rs b/src/model/styles.rs index c58a1beb..24566b09 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -7,12 +7,11 @@ use std::sync::Arc; use comemo::{Prehashed, Tracked}; -use super::{capability, Args, Content, Func, Node, NodeId, Regex, Smart, Value}; +use super::{capability, Args, Content, Func, NodeId, Regex, Smart, Value}; use crate::diag::SourceResult; -use crate::geom::{Abs, Axes, Corners, Em, Length, Numeric, Rel, Sides}; -use crate::library::layout::PageNode; -use crate::library::structure::{DescNode, EnumNode, ListNode}; -use crate::library::text::{ParNode, TextNode}; +use crate::geom::{ + Abs, Align, Axes, Corners, Em, GenAlign, Length, Numeric, PartialStroke, Rel, Sides, +}; use crate::syntax::Spanned; use crate::util::ReadableTypeId; use crate::World; @@ -111,9 +110,9 @@ impl StyleMap { self } - /// The highest-level kind of of structure the map interrupts. - pub fn interruption(&self) -> Option { - self.0.iter().filter_map(|entry| entry.interruption()).max() + /// Whether this map contains styles for the given `node.` + pub fn interrupts(&self) -> bool { + self.0.iter().any(|entry| entry.is_of(NodeId::of::())) } } @@ -132,17 +131,6 @@ impl Debug for StyleMap { } } -/// Determines whether a style could interrupt some composable structure. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] -pub enum Interruption { - /// The style forces a list break. - List, - /// The style forces a paragraph break. - Par, - /// The style forces a page break. - Page, -} - /// An entry for a single style property, recipe or barrier. #[derive(Clone, PartialEq, Hash)] pub enum StyleEntry { @@ -193,12 +181,12 @@ impl StyleEntry { } } - /// The highest-level kind of structure the entry interrupts. - pub fn interruption(&self) -> Option { + /// Whether this entry contains styles for the given `node.` + pub fn is_of(&self, node: NodeId) -> bool { match self { - Self::Property(property) => property.interruption(), - Self::Recipe(recipe) => recipe.interruption(), - _ => None, + Self::Property(property) => property.is_of(node), + Self::Recipe(recipe) => recipe.is_of(node), + _ => false, } } } @@ -397,7 +385,7 @@ impl<'a, K: Key<'a>> Iterator for Values<'a, K> { type Item = &'a K::Value; fn next(&mut self) -> Option { - while let Some(entry) = self.entries.next() { + for entry in &mut self.entries { match entry { StyleEntry::Property(property) => { if let Some(value) = property.downcast::() { @@ -662,9 +650,9 @@ impl Property { self.key == KeyId::of::() } - /// Whether this property belongs to the node `T`. - pub fn is_of(&self) -> bool { - self.node == NodeId::of::() + /// Whether this property belongs to the node with the given id. + pub fn is_of(&self, node: NodeId) -> bool { + self.node == node } /// Access the property's value if it is of the given key. @@ -690,22 +678,6 @@ impl Property { pub fn make_scoped(&mut self) { self.scoped = true; } - - /// What kind of structure the property interrupts. - pub fn interruption(&self) -> Option { - if self.is_of::() { - Some(Interruption::Page) - } else if self.is_of::() { - Some(Interruption::Par) - } else if self.is_of::() - || self.is_of::() - || self.is_of::() - { - Some(Interruption::List) - } else { - None - } - } } impl Debug for Property { @@ -826,7 +798,7 @@ impl Resolve for Em { if self.is_zero() { Abs::zero() } else { - self.at(styles.get(TextNode::SIZE)) + self.at(item!(em)(styles)) } } } @@ -891,6 +863,30 @@ where } } +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 Resolve for PartialStroke { + type Output = PartialStroke; + + fn resolve(self, styles: StyleChain) -> Self::Output { + PartialStroke { + paint: self.paint, + thickness: self.thickness.resolve(styles), + } + } +} + /// A property that is folded to determine its final value. pub trait Fold { /// The type of the folded output. @@ -970,6 +966,17 @@ impl Fold for Corners>> { } } +impl Fold for PartialStroke { + type Output = Self; + + fn fold(self, outer: Self::Output) -> Self::Output { + Self { + paint: self.paint.or(outer.paint), + thickness: self.thickness.or(outer.thickness), + } + } +} + /// A show rule recipe. #[derive(Clone, PartialEq, Hash)] pub struct Recipe { @@ -1003,13 +1010,14 @@ impl Recipe { } (Target::Text(text), Pattern::Regex(regex)) => { + let make = world.config().items.text; let mut result = vec![]; let mut cursor = 0; for mat in regex.find_iter(text) { let start = mat.start(); if cursor < start { - result.push(TextNode(text[cursor .. start].into()).pack()); + result.push(make(text[cursor .. start].into())); } result.push(self.call(world, || Value::Str(mat.as_str().into()))?); @@ -1021,7 +1029,7 @@ impl Recipe { } if cursor < text.len() { - result.push(TextNode(text[cursor ..].into()).pack()); + result.push(make(text[cursor ..].into())); } Content::sequence(result) @@ -1047,18 +1055,12 @@ impl Recipe { Ok(self.func.v.call_detached(world, args)?.display(world)) } - /// What kind of structure the property interrupts. - pub fn interruption(&self) -> Option { - if let Pattern::Node(id) = self.pattern { - if id == NodeId::of::() - || id == NodeId::of::() - || id == NodeId::of::() - { - return Some(Interruption::List); - } + /// Whether this recipe is for the given node. + pub fn is_of(&self, node: NodeId) -> bool { + match self.pattern { + Pattern::Node(id) => id == node, + _ => false, } - - None } } diff --git a/src/model/value.rs b/src/model/value.rs index d68f42a0..07719883 100644 --- a/src/model/value.rs +++ b/src/model/value.rs @@ -7,11 +7,10 @@ use std::sync::Arc; use comemo::Tracked; use siphasher::sip128::{Hasher128, SipHasher}; -use super::{ops, Args, Array, Cast, Content, Dict, Func, Node, Str}; +use super::{format_str, ops, Args, Array, Cast, Content, Dict, Func, Str}; use crate::diag::StrResult; use crate::geom::{Abs, Angle, Color, Em, Fr, Length, Ratio, Rel, RgbaColor}; -use crate::library::text::TextNode; -use crate::util::EcoString; +use crate::util::{format_eco, EcoString}; use crate::World; /// A computational value. @@ -385,7 +384,7 @@ primitive! { Str: "string", Str } primitive! { Content: "content", Content, None => Content::empty(), - Str(text) => TextNode(text.into()).pack() + Str(text) => Content::text(text) } primitive! { Array: "array", Array } primitive! { Dict: "dictionary", Dict } @@ -395,6 +394,7 @@ primitive! { Args: "arguments", Args } #[cfg(test)] mod tests { use super::*; + use crate::model::{array, dict}; #[track_caller] fn test(value: impl Into, exp: &str) { diff --git a/src/model/vm.rs b/src/model/vm.rs index 4de57d1c..db0bf77c 100644 --- a/src/model/vm.rs +++ b/src/model/vm.rs @@ -2,11 +2,11 @@ use std::path::PathBuf; use comemo::Tracked; -use super::{Content, Route, Scopes, Value}; -use crate::diag::{SourceError, StrResult}; +use super::{LangItems, Route, Scopes, Value}; +use crate::diag::{error, SourceError, StrResult}; use crate::syntax::{SourceId, Span}; -use crate::util::{EcoString, PathExt}; -use crate::{LangItems, World}; +use crate::util::PathExt; +use crate::World; /// A virtual machine. pub struct Vm<'a> { @@ -20,6 +20,8 @@ pub struct Vm<'a> { pub scopes: Scopes<'a>, /// A control flow event that is currently happening. pub flow: Option, + /// The language items. + pub items: LangItems, } impl<'a> Vm<'a> { @@ -36,6 +38,7 @@ impl<'a> Vm<'a> { location, scopes, flow: None, + items: world.config().items, } } @@ -54,18 +57,6 @@ impl<'a> Vm<'a> { Err("cannot access file system from here".into()) } - - /// The language items. - pub fn items(&self) -> &LangItems { - &self.world.config().items - } - - /// Create text content. - /// - /// This is a shorthand for `(vm.items().text)(..)`. - pub fn text(&self, text: impl Into) -> Content { - (self.items().text)(text.into()) - } } /// A control flow event that occurred during evaluation. diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index ecfa9a5b..06e41fa0 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -55,7 +55,7 @@ node! { impl Markup { /// The children. - pub fn children(&self) -> impl Iterator + '_ { + pub fn children(&self) -> impl DoubleEndedIterator + '_ { self.0.children().filter_map(SyntaxNode::cast) } } @@ -166,7 +166,7 @@ impl Space { } node! { - /// A forced line break. + /// A forced line break: `\`. Linebreak } @@ -414,9 +414,15 @@ node! { impl Math { /// The children. - pub fn children(&self) -> impl Iterator + '_ { + pub fn children(&self) -> impl DoubleEndedIterator + '_ { self.0.children().filter_map(SyntaxNode::cast) } + + /// Whether this is a display-level math formula. + pub fn display(&self) -> bool { + matches!(self.children().next(), Some(MathNode::Space(_))) + && matches!(self.children().last(), Some(MathNode::Space(_))) + } } /// A single piece of a math formula. @@ -424,7 +430,7 @@ impl Math { pub enum MathNode { /// Whitespace. Space(Space), - /// A forced line break. + /// A forced line break: `\`. Linebreak(Linebreak), /// An escape sequence: `\#`, `\u{1F5FA}`. Escape(Escape), @@ -535,7 +541,7 @@ impl Frac { } node! { - /// A math alignment indicator: `&`, `&&`. + /// An alignment indicator in a formula: `&`, `&&`. Align } @@ -736,7 +742,7 @@ node! { impl CodeBlock { /// The list of expressions contained in the block. - pub fn exprs(&self) -> impl Iterator + '_ { + pub fn exprs(&self) -> impl DoubleEndedIterator + '_ { self.0.children().filter_map(SyntaxNode::cast) } } @@ -774,7 +780,7 @@ node! { impl Array { /// The array's items. - pub fn items(&self) -> impl Iterator + '_ { + pub fn items(&self) -> impl DoubleEndedIterator + '_ { self.0.children().filter_map(SyntaxNode::cast) } } @@ -811,7 +817,7 @@ node! { impl Dict { /// The dictionary's items. - pub fn items(&self) -> impl Iterator + '_ { + pub fn items(&self) -> impl DoubleEndedIterator + '_ { self.0.children().filter_map(SyntaxNode::cast) } } @@ -1204,7 +1210,7 @@ node! { impl Args { /// The positional and named arguments. - pub fn items(&self) -> impl Iterator + '_ { + pub fn items(&self) -> impl DoubleEndedIterator + '_ { self.0.children().filter_map(SyntaxNode::cast) } } @@ -1252,7 +1258,7 @@ impl Closure { } /// The parameter bindings. - pub fn params(&self) -> impl Iterator + '_ { + pub fn params(&self) -> impl DoubleEndedIterator + '_ { self.0 .children() .find(|x| x.kind() == &NodeKind::Params) diff --git a/src/syntax/highlight.rs b/src/syntax/highlight.rs index 325b7274..0db45785 100644 --- a/src/syntax/highlight.rs +++ b/src/syntax/highlight.rs @@ -97,7 +97,7 @@ where } } - let highlighter = Highlighter::new(&theme); + let highlighter = Highlighter::new(theme); process(0, root, vec![], &highlighter, &mut f); } diff --git a/src/syntax/incremental.rs b/src/syntax/incremental.rs index 529defd7..15c0df0c 100644 --- a/src/syntax/incremental.rs +++ b/src/syntax/incremental.rs @@ -235,17 +235,17 @@ fn replace( let (newborns, terminated, amount) = match mode { ReparseMode::Code => reparse_code_block( - &prefix, + prefix, &change.text[newborn_span.start ..], newborn_span.len(), ), ReparseMode::Content => reparse_content_block( - &prefix, + prefix, &change.text[newborn_span.start ..], newborn_span.len(), ), ReparseMode::MarkupElements { at_start, min_indent } => reparse_markup_elements( - &prefix, + prefix, &change.text[newborn_span.start ..], newborn_span.len(), differential, @@ -385,17 +385,17 @@ enum ReparseMode { /// Whether changes _inside_ this node are safely encapsulated, so that only /// this node must be reparsed. fn is_bounded(kind: &NodeKind) -> bool { - match kind { + matches!( + kind, NodeKind::CodeBlock - | NodeKind::ContentBlock - | NodeKind::Linebreak - | NodeKind::SmartQuote { .. } - | NodeKind::BlockComment - | NodeKind::Space { .. } - | NodeKind::Escape(_) - | NodeKind::Shorthand(_) => true, - _ => false, - } + | NodeKind::ContentBlock + | NodeKind::Linebreak + | NodeKind::SmartQuote { .. } + | NodeKind::BlockComment + | NodeKind::Space { .. } + | NodeKind::Escape(_) + | NodeKind::Shorthand(_) + ) } /// Whether `at_start` would still be true after this node given the diff --git a/src/syntax/node.rs b/src/syntax/node.rs index 6a7d424a..4ec4abdf 100644 --- a/src/syntax/node.rs +++ b/src/syntax/node.rs @@ -99,8 +99,22 @@ impl SyntaxNode { self.children().rev().find_map(Self::cast) } + /// Returns all leaf descendants of this node (may include itself). + /// + /// This method is slow and only intended for testing. + pub fn leafs(&self) -> Vec { + if match self { + Self::Inner(inner) => inner.children.is_empty(), + Self::Leaf(_) => true, + } { + vec![self.clone()] + } else { + self.children().flat_map(Self::leafs).collect() + } + } + /// Change the type of the node. - pub fn convert(&mut self, kind: NodeKind) { + pub(super) fn convert(&mut self, kind: NodeKind) { match self { Self::Inner(inner) => { let node = Arc::make_mut(inner); @@ -112,7 +126,7 @@ impl SyntaxNode { } /// Set a synthetic span for the node and all its descendants. - pub fn synthesize(&mut self, span: Span) { + pub(super) fn synthesize(&mut self, span: Span) { match self { Self::Inner(inner) => Arc::make_mut(inner).synthesize(span), Self::Leaf(leaf) => leaf.synthesize(span), @@ -120,40 +134,30 @@ impl SyntaxNode { } /// Assign spans to each node. - pub fn numberize(&mut self, id: SourceId, within: Range) -> NumberingResult { + pub(super) fn numberize( + &mut self, + id: SourceId, + within: Range, + ) -> NumberingResult { match self { Self::Inner(inner) => Arc::make_mut(inner).numberize(id, None, within), Self::Leaf(leaf) => leaf.numberize(id, within), } } - /// The upper bound of assigned numbers in this subtree. - pub fn upper(&self) -> u64 { - match self { - Self::Inner(inner) => inner.upper(), - Self::Leaf(leaf) => leaf.span().number() + 1, - } - } - /// If the span points into this node, convert it to a byte range. - pub fn range(&self, span: Span, offset: usize) -> Option> { + pub(super) fn range(&self, span: Span, offset: usize) -> Option> { match self { Self::Inner(inner) => inner.range(span, offset), Self::Leaf(leaf) => leaf.range(span, offset), } } - /// Returns all leaf descendants of this node (may include itself). - /// - /// This method is slow and only intended for testing. - pub fn leafs(&self) -> Vec { - if match self { - Self::Inner(inner) => inner.children.is_empty(), - Self::Leaf(_) => true, - } { - vec![self.clone()] - } else { - self.children().flat_map(Self::leafs).collect() + /// The upper bound of assigned numbers in this subtree. + fn upper(&self) -> u64 { + match self { + Self::Inner(inner) => inner.upper(), + Self::Leaf(leaf) => leaf.span().number() + 1, } } } @@ -246,7 +250,7 @@ impl InnerNode { } /// Set a synthetic span for the node and all its descendants. - pub fn synthesize(&mut self, span: Span) { + fn synthesize(&mut self, span: Span) { self.data.synthesize(span); for child in &mut self.children { child.synthesize(span); @@ -255,7 +259,7 @@ impl InnerNode { /// Assign span numbers `within` an interval to this node's subtree or just /// a `range` of its children. - pub fn numberize( + fn numberize( &mut self, id: SourceId, range: Option>, @@ -304,12 +308,12 @@ impl InnerNode { } /// The upper bound of assigned numbers in this subtree. - pub fn upper(&self) -> u64 { + fn upper(&self) -> u64 { self.upper } /// If the span points into this node, convert it to a byte range. - pub fn range(&self, span: Span, mut offset: usize) -> Option> { + fn range(&self, span: Span, mut offset: usize) -> Option> { // Check whether we found it. if let Some(range) = self.data.range(span, offset) { return Some(range); @@ -343,14 +347,14 @@ impl InnerNode { } /// The node's children, mutably. - pub(crate) fn children_mut(&mut self) -> &mut [SyntaxNode] { + pub(super) fn children_mut(&mut self) -> &mut [SyntaxNode] { &mut self.children } /// Replaces a range of children with a replacement. /// /// May have mutated the children if it returns `Err(_)`. - pub(crate) fn replace_children( + pub(super) fn replace_children( &mut self, mut range: Range, replacement: Vec, @@ -430,7 +434,7 @@ impl InnerNode { } /// Update this node after changes were made to one of its children. - pub(crate) fn update_parent( + pub(super) fn update_parent( &mut self, prev_len: usize, new_len: usize, @@ -509,12 +513,12 @@ impl NodeData { } /// Set a synthetic span for the node. - pub fn synthesize(&mut self, span: Span) { + fn synthesize(&mut self, span: Span) { self.span = span; } /// Assign a span to the node. - pub fn numberize(&mut self, id: SourceId, within: Range) -> NumberingResult { + fn numberize(&mut self, id: SourceId, within: Range) -> NumberingResult { if within.start < within.end { self.span = Span::new(id, (within.start + within.end) / 2); Ok(()) @@ -524,7 +528,7 @@ impl NodeData { } /// If the span points into this node, convert it to a byte range. - pub fn range(&self, span: Span, offset: usize) -> Option> { + fn range(&self, span: Span, offset: usize) -> Option> { (self.span == span).then(|| offset .. offset + self.len()) } } diff --git a/src/syntax/parser.rs b/src/syntax/parser.rs index 83b333f4..4c8e1013 100644 --- a/src/syntax/parser.rs +++ b/src/syntax/parser.rs @@ -3,7 +3,7 @@ use std::mem; use std::ops::Range; use super::{ErrorPos, InnerNode, NodeData, NodeKind, SyntaxNode, TokenMode, Tokens}; -use crate::util::EcoString; +use crate::util::{format_eco, EcoString}; /// A convenient token-based parser. pub struct Parser<'s> { diff --git a/src/syntax/span.rs b/src/syntax/span.rs index d4d9a8f6..e3ff67b8 100644 --- a/src/syntax/span.rs +++ b/src/syntax/span.rs @@ -110,11 +110,11 @@ const fn to_non_zero(v: u64) -> NonZeroU64 { } /// Result of numbering a node within an interval. -pub type NumberingResult = Result<(), Unnumberable>; +pub(super) type NumberingResult = Result<(), Unnumberable>; /// Indicates that a node cannot be numbered within a given interval. #[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct Unnumberable; +pub(super) struct Unnumberable; impl Display for Unnumberable { fn fmt(&self, f: &mut Formatter) -> fmt::Result { diff --git a/src/syntax/tokens.rs b/src/syntax/tokens.rs index 59e6cd3f..c787fa69 100644 --- a/src/syntax/tokens.rs +++ b/src/syntax/tokens.rs @@ -6,7 +6,7 @@ use unscanny::Scanner; use super::resolve::{resolve_hex, resolve_raw, resolve_string}; use super::{ErrorPos, NodeKind, RawKind, Unit}; use crate::geom::{AbsUnit, AngleUnit}; -use crate::util::EcoString; +use crate::util::{format_eco, EcoString}; /// An iterator over the tokens of a string of source code. #[derive(Clone)] diff --git a/src/util/eco.rs b/src/util/eco.rs index 645880c4..800760e2 100644 --- a/src/util/eco.rs +++ b/src/util/eco.rs @@ -8,7 +8,9 @@ use std::sync::Arc; use super::ArcExt; /// Create a new [`EcoString`] from a format string. -macro_rules! format_eco { +#[macro_export] +#[doc(hidden)] +macro_rules! __format_eco { ($($tts:tt)*) => {{ use std::fmt::Write; let mut s = $crate::util::EcoString::new(); @@ -17,6 +19,9 @@ macro_rules! format_eco { }}; } +#[doc(inline)] +pub use crate::__format_eco as format_eco; + /// An economical string with inline storage and clone-on-write semantics. #[derive(Clone)] pub struct EcoString(Repr); @@ -55,7 +60,7 @@ impl EcoString { } /// Create an instance from an existing string-like type. - pub fn from_str(s: S) -> Self + pub fn from_str_like(s: S) -> Self where S: AsRef + Into, { @@ -324,13 +329,13 @@ impl From for EcoString { impl From<&str> for EcoString { fn from(s: &str) -> Self { - Self::from_str(s) + Self::from_str_like(s) } } impl From for EcoString { fn from(s: String) -> Self { - Self::from_str(s) + Self::from_str_like(s) } } diff --git a/src/util/mod.rs b/src/util/mod.rs index bc7aa250..df3c446e 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -3,7 +3,7 @@ pub mod fat; pub use buffer::Buffer; -pub use eco::EcoString; +pub use eco::{format_eco, EcoString}; #[macro_use] mod eco; @@ -11,9 +11,12 @@ mod buffer; use std::any::TypeId; use std::fmt::{self, Debug, Formatter}; +use std::hash::Hash; use std::path::{Component, Path, PathBuf}; use std::sync::Arc; +use siphasher::sip128::{Hasher128, SipHasher}; + /// Turn a closure into a struct implementing [`Debug`]. pub fn debug(f: F) -> impl Debug where @@ -33,6 +36,13 @@ where Wrapper(f) } +/// Calculate a 128-bit siphash of a value. +pub fn hash128(value: &T) -> u128 { + let mut state = SipHasher::new(); + value.hash(&mut state); + state.finish128().as_u128() +} + /// Extra methods for [`str`]. pub trait StrExt { /// The number of code units this string would use if it was encoded in diff --git a/tests/typ/math/basic.typ b/tests/typ/math/basic.typ index a1a9d32f..55a853cf 100644 --- a/tests/typ/math/basic.typ +++ b/tests/typ/math/basic.typ @@ -15,12 +15,6 @@ $ sum_(k=0)^n k = (n(n+1))/2 $ // Test that blackboard style looks nice. $ f: NN arrow RR $ ---- -#set math(family: "IBM Plex Sans") - -// Error: 1-4 font is not suitable for math -$a$ - --- // Error: 1:3 expected dollar sign $a diff --git a/tests/typeset.rs b/tests/typeset.rs index f3f5bfbe..7c0a6e69 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -14,16 +14,16 @@ use tiny_skia as sk; use unscanny::Scanner; use walkdir::WalkDir; -use typst::diag::{FileError, FileResult}; +use typst::diag::{bail, FileError, FileResult}; use typst::font::{Font, FontBook}; use typst::frame::{Element, Frame}; use typst::geom::{Abs, RgbaColor, Sides}; -use typst::library::layout::PageNode; -use typst::library::text::{TextNode, TextSize}; -use typst::model::{Smart, StyleMap, Value}; +use typst::model::{Smart, Value}; use typst::syntax::{Source, SourceId, SyntaxNode}; use typst::util::{Buffer, PathExt}; -use typst::{bail, Config, World}; +use typst::{Config, World}; +use typst_library::layout::PageNode; +use typst_library::text::{TextNode, TextSize}; const TYP_DIR: &str = "./typ"; const REF_DIR: &str = "./ref"; @@ -149,7 +149,7 @@ fn config() -> Config { // Set page width to 120pt with 10pt margins, so that the inner page is // exactly 100pt wide. Page height is unbounded and font size is 10pt so // that it multiplies to nice round numbers. - let mut styles = StyleMap::new(); + let mut styles = typst_library::styles(); styles.set(PageNode::WIDTH, Smart::Custom(Abs::pt(120.0).into())); styles.set(PageNode::HEIGHT, Smart::Auto); styles.set( @@ -159,10 +159,10 @@ fn config() -> Config { styles.set(TextNode::SIZE, TextSize(Abs::pt(10.0).into())); // Hook up helpers into the global scope. - let mut std = typst::library::scope(); - std.define("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF)); - std.define("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF)); - std.def_fn("test", move |_, args| { + let mut scope = typst_library::scope(); + scope.define("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF)); + scope.define("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF)); + scope.def_fn("test", move |_, args| { let lhs = args.expect::("left-hand side")?; let rhs = args.expect::("right-hand side")?; if lhs != rhs { @@ -170,7 +170,7 @@ fn config() -> Config { } Ok(Value::None) }); - std.def_fn("print", move |_, args| { + scope.def_fn("print", move |_, args| { print!("> "); for (i, value) in args.all::()?.into_iter().enumerate() { if i > 0 { @@ -184,9 +184,9 @@ fn config() -> Config { Config { root: PathBuf::new(), - items: typst::library::items(), - std, + scope, styles, + items: typst_library::items(), } } -- cgit v1.2.3