Split out four new crates (#5302)

This commit is contained in:
Laurenz 2024-10-27 19:04:55 +01:00 committed by GitHub
parent b8034a3438
commit be7cfc85d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
325 changed files with 9754 additions and 8803 deletions

207
Cargo.lock generated
View File

@ -2668,68 +2668,16 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
name = "typst"
version = "0.12.0"
dependencies = [
"arrayvec",
"az",
"bitflags 2.6.0",
"bumpalo",
"chinese-number",
"ciborium",
"comemo",
"csv",
"ecow",
"flate2",
"fontdb",
"hayagriva",
"hypher",
"icu_properties",
"icu_provider",
"icu_provider_adapters",
"icu_provider_blob",
"icu_segmenter",
"if_chain",
"image",
"indexmap 2.6.0",
"kamadak-exif",
"kurbo",
"lipsum",
"log",
"once_cell",
"palette",
"phf",
"png",
"portable-atomic",
"qcms",
"rayon",
"regex",
"roxmltree",
"rust_decimal",
"rustybuzz",
"serde",
"serde_json",
"serde_yaml 0.9.34+deprecated",
"siphasher 1.0.1",
"smallvec",
"stacker",
"syntect",
"time",
"toml",
"ttf-parser",
"two-face",
"typed-arena",
"typst-assets",
"typst-dev-assets",
"typst-eval",
"typst-layout",
"typst-library",
"typst-macros",
"typst-realize",
"typst-syntax",
"typst-timing",
"typst-utils",
"unicode-bidi",
"unicode-math-class",
"unicode-script",
"unicode-segmentation",
"unscanny",
"usvg",
"wasmi",
"xmlwriter",
]
[[package]]
@ -2769,7 +2717,7 @@ dependencies = [
"tempfile",
"toml",
"typst",
"typst-assets",
"typst-eval",
"typst-kit",
"typst-macros",
"typst-pdf",
@ -2791,7 +2739,6 @@ name = "typst-docs"
version = "0.12.0"
dependencies = [
"clap",
"comemo",
"ecow",
"heck",
"once_cell",
@ -2809,6 +2756,24 @@ dependencies = [
"yaml-front-matter",
]
[[package]]
name = "typst-eval"
version = "0.12.0"
dependencies = [
"comemo",
"ecow",
"if_chain",
"indexmap 2.6.0",
"stacker",
"toml",
"typst-library",
"typst-macros",
"typst-syntax",
"typst-timing",
"typst-utils",
"unicode-segmentation",
]
[[package]]
name = "typst-fuzz"
version = "0.12.0"
@ -2834,6 +2799,7 @@ dependencies = [
"typst",
"typst-assets",
"typst-dev-assets",
"typst-eval",
"unscanny",
]
@ -2850,13 +2816,103 @@ dependencies = [
"once_cell",
"openssl",
"tar",
"typst",
"typst-assets",
"typst-library",
"typst-syntax",
"typst-timing",
"typst-utils",
"ureq",
]
[[package]]
name = "typst-layout"
version = "0.12.0"
dependencies = [
"az",
"bumpalo",
"comemo",
"ecow",
"hypher",
"icu_properties",
"icu_provider",
"icu_provider_adapters",
"icu_provider_blob",
"icu_segmenter",
"kurbo",
"once_cell",
"rustybuzz",
"smallvec",
"ttf-parser",
"typst-assets",
"typst-library",
"typst-macros",
"typst-syntax",
"typst-timing",
"typst-utils",
"unicode-bidi",
"unicode-math-class",
"unicode-script",
"unicode-segmentation",
]
[[package]]
name = "typst-library"
version = "0.12.0"
dependencies = [
"az",
"bitflags 2.6.0",
"bumpalo",
"chinese-number",
"ciborium",
"comemo",
"csv",
"ecow",
"flate2",
"fontdb",
"hayagriva",
"icu_properties",
"icu_provider",
"icu_provider_blob",
"image",
"indexmap 2.6.0",
"kamadak-exif",
"kurbo",
"lipsum",
"once_cell",
"palette",
"phf",
"png",
"qcms",
"rayon",
"regex",
"roxmltree",
"rust_decimal",
"rustybuzz",
"serde",
"serde_json",
"serde_yaml 0.9.34+deprecated",
"siphasher 1.0.1",
"smallvec",
"syntect",
"time",
"toml",
"ttf-parser",
"two-face",
"typed-arena",
"typst-assets",
"typst-dev-assets",
"typst-macros",
"typst-syntax",
"typst-timing",
"typst-utils",
"unicode-math-class",
"unicode-segmentation",
"unscanny",
"usvg",
"wasmi",
"xmlwriter",
]
[[package]]
name = "typst-macros"
version = "0.12.0"
@ -2885,14 +2941,32 @@ dependencies = [
"subsetter",
"svg2pdf",
"ttf-parser",
"typst",
"typst-assets",
"typst-library",
"typst-macros",
"typst-syntax",
"typst-timing",
"unscanny",
"typst-utils",
"xmp-writer",
]
[[package]]
name = "typst-realize"
version = "0.12.0"
dependencies = [
"arrayvec",
"bumpalo",
"comemo",
"ecow",
"once_cell",
"regex",
"typst-library",
"typst-macros",
"typst-syntax",
"typst-timing",
"typst-utils",
]
[[package]]
name = "typst-render"
version = "0.12.0"
@ -2902,13 +2976,11 @@ dependencies = [
"image",
"pixglyph",
"resvg",
"roxmltree",
"tiny-skia",
"ttf-parser",
"typst",
"typst-library",
"typst-macros",
"typst-timing",
"usvg",
]
[[package]]
@ -2920,9 +2992,10 @@ dependencies = [
"ecow",
"flate2",
"ttf-parser",
"typst",
"typst-library",
"typst-macros",
"typst-timing",
"typst-utils",
"xmlparser",
"xmlwriter",
]
@ -2935,6 +3008,7 @@ dependencies = [
"once_cell",
"serde",
"toml",
"typst-timing",
"typst-utils",
"unicode-ident",
"unicode-math-class",
@ -2956,10 +3030,10 @@ dependencies = [
"rayon",
"regex",
"tiny-skia",
"ttf-parser",
"typst",
"typst-assets",
"typst-dev-assets",
"typst-library",
"typst-pdf",
"typst-render",
"typst-svg",
@ -2974,7 +3048,6 @@ dependencies = [
"parking_lot",
"serde",
"serde_json",
"typst-syntax",
]
[[package]]

View File

@ -18,10 +18,14 @@ readme = "README.md"
[workspace.dependencies]
typst = { path = "crates/typst", version = "0.12.0" }
typst-cli = { path = "crates/typst-cli", version = "0.12.0" }
typst-eval = { path = "crates/typst-eval", version = "0.12.0" }
typst-ide = { path = "crates/typst-ide", version = "0.12.0" }
typst-kit = { path = "crates/typst-kit", version = "0.12.0" }
typst-layout = { path = "crates/typst-layout", version = "0.12.0" }
typst-library = { path = "crates/typst-library", version = "0.12.0" }
typst-macros = { path = "crates/typst-macros", version = "0.12.0" }
typst-pdf = { path = "crates/typst-pdf", version = "0.12.0" }
typst-realize = { path = "crates/typst-realize", version = "0.12.0" }
typst-render = { path = "crates/typst-render", version = "0.12.0" }
typst-svg = { path = "crates/typst-svg", version = "0.12.0" }
typst-syntax = { path = "crates/typst-syntax", version = "0.12.0" }
@ -145,5 +149,8 @@ strip = true
[workspace.lints.clippy]
blocks_in_conditions = "allow"
comparison_chain = "allow"
manual_range_contains = "allow"
mutable_key_type = "allow"
uninlined_format_args = "warn"
wildcard_in_or_patterns = "allow"

View File

@ -19,7 +19,7 @@ doc = false
[dependencies]
typst = { workspace = true }
typst-assets = { workspace = true, features = ["fonts"] }
typst-eval = { workspace = true }
typst-kit = { workspace = true }
typst-macros = { workspace = true }
typst-pdf = { workspace = true }
@ -28,8 +28,8 @@ typst-svg = { workspace = true }
typst-timing = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true }
color-print = { workspace = true }
codespan-reporting = { workspace = true }
color-print = { workspace = true }
comemo = { workspace = true }
dirs = { workspace = true }
ecow = { workspace = true }

View File

@ -94,9 +94,10 @@ fn print_error(msg: &str) -> io::Result<()> {
#[cfg(not(feature = "self-update"))]
mod update {
use crate::args::UpdateCommand;
use typst::diag::{bail, StrResult};
use crate::args::UpdateCommand;
pub fn update(_: &UpdateCommand) -> StrResult<()> {
bail!(
"self-updating is not enabled for this executable, \

View File

@ -2,11 +2,11 @@ use comemo::Track;
use ecow::{eco_format, EcoString};
use serde::Serialize;
use typst::diag::{bail, HintedStrResult, StrResult, Warned};
use typst::eval::{eval_string, EvalMode};
use typst::foundations::{Content, IntoValue, LocatableSelector, Scope};
use typst::model::Document;
use typst::syntax::Span;
use typst::World;
use typst_eval::{eval_string, EvalMode};
use crate::args::{QueryCommand, SerializationFormat};
use crate::compile::print_diagnostics;
@ -56,6 +56,7 @@ fn retrieve(
document: &Document,
) -> HintedStrResult<Vec<Content>> {
let selector = eval_string(
&typst::ROUTINES,
world.track(),
&command.selector,
Span::detached(),

View File

@ -72,7 +72,8 @@ impl Timer {
let writer = BufWriter::with_capacity(1 << 20, file);
typst_timing::export_json(writer, |span| {
resolve_span(world, span).unwrap_or_else(|| ("unknown".to_string(), 0))
resolve_span(world, Span::from_raw(span))
.unwrap_or_else(|| ("unknown".to_string(), 0))
})?;
Ok(output)

View File

@ -16,7 +16,7 @@ use typst::utils::LazyHash;
use typst::{Library, World};
use typst_kit::fonts::{FontSlot, Fonts};
use typst_kit::package::PackageStorage;
use typst_timing::{timed, TimingScope};
use typst_timing::timed;
use crate::args::{Input, SharedArgs};
use crate::compile::ExportCache;
@ -285,8 +285,6 @@ impl FileSlot {
self.source.get_or_init(
|| read(self.id, project_root, package_storage),
|data, prev| {
let name = if prev.is_some() { "reparsing file" } else { "parsing file" };
let _scope = TimingScope::new(name, None);
let text = decode_utf8(&data)?;
if let Some(mut prev) = prev {
prev.replace(text);

View File

@ -0,0 +1,32 @@
[package]
name = "typst-eval"
description = "Typst's code interpreter."
version = { workspace = true }
rust-version = { workspace = true }
authors = { workspace = true }
edition = { workspace = true }
homepage = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
categories = { workspace = true }
keywords = { workspace = true }
readme = { workspace = true }
[dependencies]
typst-library = { workspace = true }
typst-macros = { workspace = true }
typst-syntax = { workspace = true }
typst-timing = { workspace = true }
typst-utils = { workspace = true }
comemo = { workspace = true }
ecow = { workspace = true }
if_chain = { workspace = true }
indexmap = { workspace = true }
toml = { workspace = true }
unicode-segmentation = { workspace = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
stacker = { workspace = true }
[lints]
workspace = true

View File

@ -1,9 +1,9 @@
use ecow::eco_format;
use typst_library::diag::{bail, At, Hint, SourceResult, Trace, Tracepoint};
use typst_library::foundations::{Dict, Value};
use typst_syntax::ast::{self, AstNode};
use crate::diag::{bail, At, Hint, SourceResult, Trace, Tracepoint};
use crate::eval::{Eval, Vm};
use crate::foundations::{call_method_access, is_accessor_method, Dict, Value};
use crate::syntax::ast::{self, AstNode};
use crate::{call_method_access, is_accessor_method, Eval, Vm};
/// Access an expression mutably.
pub(crate) trait Access {
@ -85,7 +85,7 @@ pub(crate) fn access_dict<'a>(
Value::Symbol(_) | Value::Content(_) | Value::Module(_) | Value::Func(_)
) {
bail!(span, "cannot mutate fields on {ty}");
} else if crate::foundations::fields_on(ty).is_empty() {
} else if typst_library::foundations::fields_on(ty).is_empty() {
bail!(span, "{ty} does not have accessible fields");
} else {
// type supports static fields, which don't yet have

View File

@ -1,11 +1,11 @@
use std::collections::HashSet;
use ecow::eco_format;
use typst_library::diag::{bail, error, At, SourceDiagnostic, SourceResult};
use typst_library::foundations::{Array, Dict, Value};
use typst_syntax::ast::{self, AstNode};
use crate::diag::{bail, error, At, SourceDiagnostic, SourceResult};
use crate::eval::{Access, Eval, Vm};
use crate::foundations::{Array, Dict, Value};
use crate::syntax::ast::{self, AstNode};
use crate::{Access, Eval, Vm};
impl Eval for ast::LetBinding<'_> {
type Output = Value;

View File

@ -1,23 +1,24 @@
use comemo::{Tracked, TrackedMut};
use ecow::{eco_format, EcoString, EcoVec};
use crate::diag::{
use typst_library::diag::{
bail, error, At, HintedStrResult, HintedString, SourceDiagnostic, SourceResult,
Trace, Tracepoint,
};
use crate::engine::{Engine, Sink, Traced};
use crate::eval::{Access, Eval, FlowEvent, Route, Vm};
use crate::foundations::{
call_method_mut, is_mutating_method, Arg, Args, Bytes, Capturer, Closure, Content,
Context, Func, IntoValue, NativeElement, Scope, Scopes, Value,
use typst_library::engine::{Engine, Sink, Traced};
use typst_library::foundations::{
Arg, Args, Bytes, Capturer, Closure, Content, Context, Func, IntoValue,
NativeElement, Scope, Scopes, Value,
};
use crate::introspection::Introspector;
use crate::math::LrElem;
use crate::syntax::ast::{self, AstNode, Ident};
use crate::syntax::{Span, Spanned, SyntaxNode};
use crate::text::TextElem;
use crate::utils::LazyHash;
use crate::World;
use typst_library::introspection::Introspector;
use typst_library::math::LrElem;
use typst_library::routines::Routines;
use typst_library::text::TextElem;
use typst_library::World;
use typst_syntax::ast::{self, AstNode, Ident};
use typst_syntax::{Span, Spanned, SyntaxNode};
use typst_utils::LazyHash;
use crate::{call_method_mut, is_mutating_method, Access, Eval, FlowEvent, Route, Vm};
impl Eval for ast::FuncCall<'_> {
type Output = Value;
@ -159,9 +160,10 @@ impl Eval for ast::Closure<'_> {
/// Call the function in the context with the arguments.
#[comemo::memoize]
#[allow(clippy::too_many_arguments)]
pub(crate) fn call_closure(
pub fn eval_closure(
func: &Func,
closure: &LazyHash<Closure>,
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
traced: Tracked<Traced>,
@ -182,6 +184,7 @@ pub(crate) fn call_closure(
// Prepare the engine.
let engine = Engine {
routines,
world,
introspector,
traced,
@ -210,7 +213,7 @@ pub(crate) fn call_closure(
vm.define(ident, args.expect::<Value>(&ident)?)
}
pattern => {
crate::eval::destructure(
crate::destructure(
&mut vm,
pattern,
args.expect::<Value>("pattern parameter")?,
@ -583,8 +586,9 @@ impl<'a> CapturesVisitor<'a> {
#[cfg(test)]
mod tests {
use typst_syntax::parse;
use super::*;
use crate::syntax::parse;
#[track_caller]
fn test(text: &str, result: &[&str]) {

View File

@ -1,11 +1,12 @@
use ecow::{eco_vec, EcoVec};
use crate::diag::{bail, error, At, SourceResult};
use crate::eval::{ops, CapturesVisitor, Eval, Vm};
use crate::foundations::{
Array, Capturer, Closure, Content, ContextElem, Dict, Func, NativeElement, Str, Value,
use typst_library::diag::{bail, error, At, SourceResult};
use typst_library::foundations::{
ops, Array, Capturer, Closure, Content, ContextElem, Dict, Func, NativeElement, Str,
Value,
};
use crate::syntax::ast::{self, AstNode};
use typst_syntax::ast::{self, AstNode};
use crate::{CapturesVisitor, Eval, Vm};
impl Eval for ast::Code<'_> {
type Output = Value;

View File

@ -1,10 +1,10 @@
use typst_library::diag::{bail, error, At, SourceDiagnostic, SourceResult};
use typst_library::foundations::{ops, IntoValue, Value};
use typst_syntax::ast::{self, AstNode};
use typst_syntax::{Span, SyntaxKind, SyntaxNode};
use unicode_segmentation::UnicodeSegmentation;
use crate::diag::{bail, error, At, SourceDiagnostic, SourceResult};
use crate::eval::{destructure, ops, Eval, Vm};
use crate::foundations::{IntoValue, Value};
use crate::syntax::ast::{self, AstNode};
use crate::syntax::{Span, SyntaxKind, SyntaxNode};
use crate::{destructure, Eval, Vm};
/// The maximum number of loop iterations.
const MAX_ITERATIONS: usize = 10_000;

View File

@ -1,13 +1,15 @@
use comemo::TrackedMut;
use ecow::{eco_format, eco_vec, EcoString};
use typst_library::diag::{
bail, error, warning, At, FileError, SourceResult, Trace, Tracepoint,
};
use typst_library::foundations::{Content, Module, Value};
use typst_library::World;
use typst_syntax::ast::{self, AstNode};
use typst_syntax::package::{PackageManifest, PackageSpec};
use typst_syntax::{FileId, Span, VirtualPath};
use crate::diag::{bail, error, warning, At, FileError, SourceResult, Trace, Tracepoint};
use crate::eval::{eval, Eval, Vm};
use crate::foundations::{Content, Module, Value};
use crate::syntax::ast::{self, AstNode};
use crate::syntax::package::{PackageManifest, PackageSpec};
use crate::syntax::{FileId, Span, VirtualPath};
use crate::World;
use crate::{eval, Eval, Vm};
impl Eval for ast::ModuleImport<'_> {
type Output = Value;
@ -171,8 +173,9 @@ pub fn import(
/// Import an external package.
fn import_package(vm: &mut Vm, spec: PackageSpec, span: Span) -> SourceResult<Module> {
// Evaluate the manifest.
let world = vm.world();
let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml"));
let bytes = vm.world().file(manifest_id).at(span)?;
let bytes = world.file(manifest_id).at(span)?;
let string = std::str::from_utf8(&bytes).map_err(FileError::from).at(span)?;
let manifest: PackageManifest = toml::from_str(string)
.map_err(|err| eco_format!("package manifest is malformed ({})", err.message()))
@ -181,7 +184,7 @@ fn import_package(vm: &mut Vm, spec: PackageSpec, span: Span) -> SourceResult<Mo
// Evaluate the entry point.
let entrypoint_id = manifest_id.join(&manifest.package.entrypoint);
let source = vm.world().source(entrypoint_id).at(span)?;
let source = world.source(entrypoint_id).at(span)?;
// Prevent cyclic importing.
if vm.engine.route.contains(source.id()) {
@ -190,13 +193,14 @@ fn import_package(vm: &mut Vm, spec: PackageSpec, span: Span) -> SourceResult<Mo
let point = || Tracepoint::Import;
Ok(eval(
vm.world(),
vm.engine.routines,
vm.engine.world,
vm.engine.traced,
TrackedMut::reborrow_mut(&mut vm.engine.sink),
vm.engine.route.track(),
&source,
)
.trace(vm.world(), point, span)?
.trace(world, point, span)?
.with_name(manifest.package.name))
}
@ -215,7 +219,8 @@ fn import_file(vm: &mut Vm, path: &str, span: Span) -> SourceResult<Module> {
// Evaluate the file.
let point = || Tracepoint::Import;
eval(
world,
vm.engine.routines,
vm.engine.world,
vm.engine.traced,
TrackedMut::reborrow_mut(&mut vm.engine.sink),
vm.engine.route.track(),

View File

@ -1,4 +1,4 @@
//! Evaluation of markup and code.
//! Typst's code interpreter.
pub(crate) mod ops;
@ -10,31 +10,35 @@ mod flow;
mod import;
mod markup;
mod math;
mod methods;
mod rules;
mod vm;
pub use self::call::*;
pub use self::import::*;
pub use self::vm::*;
pub use typst_library::routines::EvalMode;
pub(crate) use self::access::*;
pub(crate) use self::binding::*;
pub(crate) use self::flow::*;
use self::access::*;
use self::binding::*;
use self::flow::*;
use self::methods::*;
use comemo::{Track, Tracked, TrackedMut};
use crate::diag::{bail, SourceResult};
use crate::engine::{Engine, Route, Sink, Traced};
use crate::foundations::{Cast, Context, Module, NativeElement, Scope, Scopes, Value};
use crate::introspection::Introspector;
use crate::math::EquationElem;
use crate::syntax::{ast, parse, parse_code, parse_math, Source, Span};
use crate::World;
use typst_library::diag::{bail, SourceResult};
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Context, Module, NativeElement, Scope, Scopes, Value};
use typst_library::introspection::Introspector;
use typst_library::math::EquationElem;
use typst_library::routines::Routines;
use typst_library::World;
use typst_syntax::{ast, parse, parse_code, parse_math, Source, Span};
/// Evaluate a source file and return the resulting module.
#[comemo::memoize]
#[typst_macros::time(name = "eval", span = source.root().span())]
pub fn eval(
routines: &Routines,
world: Tracked<dyn World + '_>,
traced: Tracked<Traced>,
sink: TrackedMut<Sink>,
@ -50,6 +54,7 @@ pub fn eval(
// Prepare the engine.
let introspector = Introspector::default();
let engine = Engine {
routines,
world,
introspector: introspector.track(),
traced,
@ -94,6 +99,7 @@ pub fn eval(
/// Everything in the output is associated with the given `span`.
#[comemo::memoize]
pub fn eval_string(
routines: &Routines,
world: Tracked<dyn World + '_>,
string: &str,
span: Span,
@ -119,6 +125,7 @@ pub fn eval_string(
let introspector = Introspector::default();
let traced = Traced::default();
let engine = Engine {
routines,
world,
introspector: introspector.track(),
traced: traced.track(),
@ -153,17 +160,6 @@ pub fn eval_string(
Ok(output)
}
/// In which mode to evaluate a string.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum EvalMode {
/// Evaluate as code, as after a hash.
Code,
/// Evaluate as markup, like in a Typst file.
Markup,
/// Evaluate as math, as in an equation.
Math,
}
/// Evaluate an expression.
pub trait Eval {
/// The output of evaluating the expression.

View File

@ -1,18 +1,18 @@
use crate::diag::{warning, At, SourceResult};
use crate::eval::{Eval, Vm};
use crate::foundations::{
Content, Label, NativeElement, Repr, Smart, Unlabellable, Value,
use typst_library::diag::{warning, At, SourceResult};
use typst_library::foundations::{
Content, Label, NativeElement, Repr, Smart, Symbol, Unlabellable, Value,
};
use crate::math::EquationElem;
use crate::model::{
use typst_library::math::EquationElem;
use typst_library::model::{
EmphElem, EnumItem, HeadingElem, LinkElem, ListItem, ParbreakElem, RefElem,
StrongElem, Supplement, TermItem, Url,
};
use crate::symbols::Symbol;
use crate::syntax::ast::{self, AstNode};
use crate::text::{
use typst_library::text::{
LinebreakElem, RawContent, RawElem, SmartQuoteElem, SpaceElem, TextElem,
};
use typst_syntax::ast::{self, AstNode};
use crate::{Eval, Vm};
impl Eval for ast::Markup<'_> {
type Output = Content;

View File

@ -1,12 +1,13 @@
use ecow::eco_format;
use typst_library::diag::{At, SourceResult};
use typst_library::foundations::{Content, NativeElement, Symbol, Value};
use typst_library::math::{
AlignPointElem, AttachElem, FracElem, LrElem, PrimesElem, RootElem,
};
use typst_library::text::TextElem;
use typst_syntax::ast::{self, AstNode};
use crate::diag::{At, SourceResult};
use crate::eval::{Eval, Vm};
use crate::foundations::{Content, NativeElement, Value};
use crate::math::{AlignPointElem, AttachElem, FracElem, LrElem, PrimesElem, RootElem};
use crate::symbols::Symbol;
use crate::syntax::ast::{self, AstNode};
use crate::text::TextElem;
use crate::{Eval, Vm};
impl Eval for ast::Math<'_> {
type Output = Content;

View File

@ -1,8 +1,8 @@
//! Handles special built-in methods on values.
use crate::diag::{At, SourceResult};
use crate::foundations::{Args, Str, Type, Value};
use crate::syntax::Span;
use typst_library::diag::{At, SourceResult};
use typst_library::foundations::{Args, Str, Type, Value};
use typst_syntax::Span;
/// Whether a specific method is mutating.
pub(crate) fn is_mutating_method(method: &str) -> bool {

View File

@ -0,0 +1,91 @@
use typst_library::diag::{At, HintedStrResult, SourceResult};
use typst_library::foundations::{ops, IntoValue, Value};
use typst_syntax::ast::{self, AstNode};
use crate::{access_dict, Access, Eval, Vm};
impl Eval for ast::Unary<'_> {
type Output = Value;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let value = self.expr().eval(vm)?;
let result = match self.op() {
ast::UnOp::Pos => ops::pos(value),
ast::UnOp::Neg => ops::neg(value),
ast::UnOp::Not => ops::not(value),
};
result.at(self.span())
}
}
impl Eval for ast::Binary<'_> {
type Output = Value;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
match self.op() {
ast::BinOp::Add => apply_binary(self, vm, ops::add),
ast::BinOp::Sub => apply_binary(self, vm, ops::sub),
ast::BinOp::Mul => apply_binary(self, vm, ops::mul),
ast::BinOp::Div => apply_binary(self, vm, ops::div),
ast::BinOp::And => apply_binary(self, vm, ops::and),
ast::BinOp::Or => apply_binary(self, vm, ops::or),
ast::BinOp::Eq => apply_binary(self, vm, ops::eq),
ast::BinOp::Neq => apply_binary(self, vm, ops::neq),
ast::BinOp::Lt => apply_binary(self, vm, ops::lt),
ast::BinOp::Leq => apply_binary(self, vm, ops::leq),
ast::BinOp::Gt => apply_binary(self, vm, ops::gt),
ast::BinOp::Geq => apply_binary(self, vm, ops::geq),
ast::BinOp::In => apply_binary(self, vm, ops::in_),
ast::BinOp::NotIn => apply_binary(self, vm, ops::not_in),
ast::BinOp::Assign => apply_assignment(self, vm, |_, b| Ok(b)),
ast::BinOp::AddAssign => apply_assignment(self, vm, ops::add),
ast::BinOp::SubAssign => apply_assignment(self, vm, ops::sub),
ast::BinOp::MulAssign => apply_assignment(self, vm, ops::mul),
ast::BinOp::DivAssign => apply_assignment(self, vm, ops::div),
}
}
}
/// Apply a basic binary operation.
fn apply_binary(
binary: ast::Binary,
vm: &mut Vm,
op: fn(Value, Value) -> HintedStrResult<Value>,
) -> SourceResult<Value> {
let lhs = binary.lhs().eval(vm)?;
// Short-circuit boolean operations.
if (binary.op() == ast::BinOp::And && lhs == false.into_value())
|| (binary.op() == ast::BinOp::Or && lhs == true.into_value())
{
return Ok(lhs);
}
let rhs = binary.rhs().eval(vm)?;
op(lhs, rhs).at(binary.span())
}
/// Apply an assignment operation.
fn apply_assignment(
binary: ast::Binary,
vm: &mut Vm,
op: fn(Value, Value) -> HintedStrResult<Value>,
) -> SourceResult<Value> {
let rhs = binary.rhs().eval(vm)?;
let lhs = binary.lhs();
// An assignment to a dictionary field is different from a normal access
// since it can create the field instead of just modifying it.
if binary.op() == ast::BinOp::Assign {
if let ast::Expr::FieldAccess(access) = lhs {
let dict = access_dict(vm, access)?;
dict.insert(access.field().get().clone().into(), rhs);
return Ok(Value::None);
}
}
let location = binary.lhs().access(vm)?;
let lhs = std::mem::take(&mut *location);
*location = op(lhs, rhs).at(binary.span())?;
Ok(Value::None)
}

View File

@ -1,11 +1,12 @@
use crate::diag::{warning, At, SourceResult};
use crate::eval::{Eval, Vm};
use crate::foundations::{
use typst_library::diag::{warning, At, SourceResult};
use typst_library::foundations::{
Element, Fields, Func, Recipe, Selector, ShowableSelector, Styles, Transformation,
};
use crate::layout::BlockElem;
use crate::model::ParElem;
use crate::syntax::ast::{self, AstNode};
use typst_library::layout::BlockElem;
use typst_library::model::ParElem;
use typst_syntax::ast::{self, AstNode};
use crate::{Eval, Vm};
impl Eval for ast::SetRule<'_> {
type Output = Styles;

View File

@ -1,15 +1,15 @@
use comemo::Tracked;
use typst_library::engine::Engine;
use typst_library::foundations::{Context, IntoValue, Scopes, Value};
use typst_library::World;
use typst_syntax::ast::{self, AstNode};
use typst_syntax::Span;
use crate::engine::Engine;
use crate::eval::FlowEvent;
use crate::foundations::{Context, IntoValue, Scopes, Value};
use crate::syntax::ast::{self, AstNode};
use crate::syntax::Span;
use crate::World;
use crate::FlowEvent;
/// A virtual machine.
///
/// Holds the state needed to [evaluate](crate::eval::eval()) Typst sources. A
/// Holds the state needed to [evaluate](crate::eval()) Typst sources. A
/// new virtual machine is created for each module evaluation and function call.
pub struct Vm<'a> {
/// The underlying virtual typesetter.

View File

@ -14,6 +14,7 @@ readme = { workspace = true }
[dependencies]
typst = { workspace = true }
typst-eval = { workspace = true }
comemo = { workspace = true }
ecow = { workspace = true }
if_chain = { workspace = true }

View File

@ -1,12 +1,12 @@
use comemo::Track;
use ecow::{eco_vec, EcoString, EcoVec};
use typst::engine::{Engine, Route, Sink, Traced};
use typst::eval::Vm;
use typst::foundations::{Context, Label, Scopes, Styles, Value};
use typst::introspection::Introspector;
use typst::model::{BibliographyElem, Document};
use typst::syntax::{ast, LinkedNode, Span, SyntaxKind};
use typst::World;
use typst_eval::Vm;
/// Try to determine a set of possible values for an expression.
pub fn analyze_expr(
@ -58,6 +58,7 @@ pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option<Value> {
let traced = Traced::default();
let mut sink = Sink::new();
let engine = Engine {
routines: &typst::ROUTINES,
world: world.track(),
introspector: introspector.track(),
traced: traced.track(),
@ -73,7 +74,7 @@ pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option<Value> {
Span::detached(),
);
typst::eval::import(&mut vm, source, source_span, true)
typst_eval::import(&mut vm, source, source_span, true)
.ok()
.map(Value::Module)
}

View File

@ -1353,7 +1353,6 @@ impl<'a> CompletionContext<'a> {
#[cfg(test)]
mod tests {
use super::autocomplete;
use crate::tests::TestWorld;

View File

@ -191,7 +191,7 @@ mod tests {
// 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 lib = Library::default();
let mut lib = typst::Library::default();
lib.styles
.set(PageElem::set_width(Smart::Custom(Abs::pt(120.0).into())));
lib.styles.set(PageElem::set_height(Smart::Auto));

View File

@ -3,13 +3,13 @@ use std::fmt::Write;
use ecow::{eco_format, EcoString};
use if_chain::if_chain;
use typst::engine::Sink;
use typst::eval::CapturesVisitor;
use typst::foundations::{repr, Capturer, CastInfo, Repr, Value};
use typst::layout::Length;
use typst::model::Document;
use typst::syntax::{ast, LinkedNode, Side, Source, SyntaxKind};
use typst::utils::{round_with_precision, Numeric};
use typst::World;
use typst_eval::CapturesVisitor;
use crate::{analyze_expr, analyze_labels, plain_docs_sentence, summarize_font_family};

View File

@ -11,13 +11,14 @@ license = { workspace = true }
readme = { workspace = true }
[dependencies]
typst = { workspace = true }
typst-assets = { workspace = true, optional = true }
typst-library = { workspace = true }
typst-syntax = { workspace = true }
typst-timing = { workspace = true }
typst-utils = { workspace = true }
dirs = { workspace = true, optional = true }
ecow = { workspace = true }
env_proxy = { workspace = true, optional = true }
dirs = { workspace = true, optional = true }
flate2 = { workspace = true, optional = true }
fontdb = { workspace = true, optional = true }
native-tls = { workspace = true, optional = true }

View File

@ -8,12 +8,12 @@
//! - For math: New Computer Modern Math
//! - For code: Deja Vu Sans Mono
use std::path::PathBuf;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use std::{fs, path::Path};
use fontdb::{Database, Source};
use typst::text::{Font, FontBook, FontInfo};
use typst_library::text::{Font, FontBook, FontInfo};
use typst_timing::TimingScope;
/// Holds details about the location of a font and lazily the font itself.
@ -46,7 +46,7 @@ impl FontSlot {
pub fn get(&self) -> Option<Font> {
self.font
.get_or_init(|| {
let _scope = TimingScope::new("load font", None);
let _scope = TimingScope::new("load font");
let data = fs::read(
self.path
.as_ref()
@ -196,7 +196,7 @@ impl FontSearcher {
#[cfg(feature = "embed-fonts")]
fn add_embedded(&mut self) {
for data in typst_assets::fonts() {
let buffer = typst::foundations::Bytes::from_static(data);
let buffer = typst_library::foundations::Bytes::from_static(data);
for (i, font) in Font::iter(buffer).enumerate() {
self.book.push(font.info().clone());
self.fonts.push(FontSlot {

View File

@ -5,8 +5,8 @@ use std::path::{Path, PathBuf};
use ecow::eco_format;
use once_cell::sync::OnceCell;
use typst::diag::{bail, PackageError, PackageResult, StrResult};
use typst::syntax::package::{
use typst_library::diag::{bail, PackageError, PackageResult, StrResult};
use typst_syntax::package::{
PackageInfo, PackageSpec, PackageVersion, VersionlessPackageSpec,
};

View File

@ -0,0 +1,43 @@
[package]
name = "typst-layout"
description = "Typst's layout engine."
version = { workspace = true }
rust-version = { workspace = true }
authors = { workspace = true }
edition = { workspace = true }
homepage = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
categories = { workspace = true }
keywords = { workspace = true }
readme = { workspace = true }
[dependencies]
typst-assets = { workspace = true }
typst-library = { workspace = true }
typst-macros = { workspace = true }
typst-syntax = { workspace = true }
typst-timing = { workspace = true }
typst-utils = { workspace = true }
az = { workspace = true }
bumpalo = { workspace = true }
comemo = { workspace = true }
ecow = { workspace = true }
hypher = { workspace = true }
icu_properties = { workspace = true }
icu_provider = { workspace = true }
icu_provider_adapters = { workspace = true }
icu_provider_blob = { workspace = true }
icu_segmenter = { workspace = true }
kurbo = { workspace = true }
once_cell = { workspace = true }
rustybuzz = { workspace = true }
smallvec = { workspace = true }
ttf-parser = { workspace = true }
unicode-bidi = { workspace = true }
unicode-math-class = { workspace = true }
unicode-script = { workspace = true }
unicode-segmentation = { workspace = true }
[lints]
workspace = true

View File

@ -0,0 +1,401 @@
use once_cell::unsync::Lazy;
use smallvec::SmallVec;
use typst_library::diag::SourceResult;
use typst_library::engine::Engine;
use typst_library::foundations::{Packed, Resolve, StyleChain};
use typst_library::introspection::Locator;
use typst_library::layout::{
Abs, Axes, BlockBody, BlockElem, Fragment, Frame, FrameKind, Region, Regions, Rel,
Sides, Size, Sizing,
};
use typst_library::visualize::Stroke;
use typst_utils::Numeric;
use crate::shapes::{clip_rect, fill_and_stroke};
/// Lay this out as an unbreakable block.
#[typst_macros::time(name = "block", span = elem.span())]
pub fn layout_single_block(
elem: &Packed<BlockElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
// Fetch sizing properties.
let width = elem.width(styles);
let height = elem.height(styles);
let inset = elem.inset(styles).unwrap_or_default();
// Build the pod regions.
let pod = unbreakable_pod(&width.into(), &height, &inset, styles, region.size);
// Layout the body.
let body = elem.body(styles);
let mut frame = match body {
// If we have no body, just create one frame. Its size will be
// adjusted below.
None => Frame::hard(Size::zero()),
// If we have content as our body, just layout it.
Some(BlockBody::Content(body)) => {
crate::layout_frame(engine, body, locator.relayout(), styles, pod)?
}
// If we have a child that wants to layout with just access to the
// base region, give it that.
Some(BlockBody::SingleLayouter(callback)) => {
callback.call(engine, locator, styles, pod)?
}
// If we have a child that wants to layout with full region access,
// we layout it.
Some(BlockBody::MultiLayouter(callback)) => {
let expand = (pod.expand | region.expand) & pod.size.map(Abs::is_finite);
let pod = Region { expand, ..pod };
callback.call(engine, locator, styles, pod.into())?.into_frame()
}
};
// Explicit blocks are boundaries for gradient relativeness.
if matches!(body, None | Some(BlockBody::Content(_))) {
frame.set_kind(FrameKind::Hard);
}
// Enforce a correct frame size on the expanded axes. Do this before
// applying the inset, since the pod shrunk.
frame.set_size(pod.expand.select(pod.size, frame.size()));
// Apply the inset.
if !inset.is_zero() {
crate::pad::grow(&mut frame, &inset);
}
// Prepare fill and stroke.
let fill = elem.fill(styles);
let stroke = elem
.stroke(styles)
.unwrap_or_default()
.map(|s| s.map(Stroke::unwrap_or_default));
// Only fetch these if necessary (for clipping or filling/stroking).
let outset = Lazy::new(|| elem.outset(styles).unwrap_or_default());
let radius = Lazy::new(|| elem.radius(styles).unwrap_or_default());
// Clip the contents, if requested.
if elem.clip(styles) {
let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis();
frame.clip(clip_rect(size, &radius, &stroke));
}
// Add fill and/or stroke.
if fill.is_some() || stroke.iter().any(Option::is_some) {
fill_and_stroke(&mut frame, fill, &stroke, &outset, &radius, elem.span());
}
// Assign label to each frame in the fragment.
if let Some(label) = elem.label() {
frame.label(label);
}
Ok(frame)
}
/// Lay this out as a breakable block.
#[typst_macros::time(name = "block", span = elem.span())]
pub fn layout_multi_block(
elem: &Packed<BlockElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
// Fetch sizing properties.
let width = elem.width(styles);
let height = elem.height(styles);
let inset = elem.inset(styles).unwrap_or_default();
// Allocate a small vector for backlogs.
let mut buf = SmallVec::<[Abs; 2]>::new();
// Build the pod regions.
let pod = breakable_pod(&width.into(), &height, &inset, styles, regions, &mut buf);
// Layout the body.
let body = elem.body(styles);
let mut fragment = match body {
// If we have no body, just create one frame plus one per backlog
// region. We create them zero-sized; if necessary, their size will
// be adjusted below.
None => {
let mut frames = vec![];
frames.push(Frame::hard(Size::zero()));
if pod.expand.y {
let mut iter = pod;
while !iter.backlog.is_empty() {
frames.push(Frame::hard(Size::zero()));
iter.next();
}
}
Fragment::frames(frames)
}
// If we have content as our body, just layout it.
Some(BlockBody::Content(body)) => {
let mut fragment =
crate::layout_fragment(engine, body, locator.relayout(), styles, pod)?;
// If the body is automatically sized and produced more than one
// fragment, ensure that the width was consistent across all
// regions. If it wasn't, we need to relayout with expansion.
if !pod.expand.x
&& fragment
.as_slice()
.windows(2)
.any(|w| !w[0].width().approx_eq(w[1].width()))
{
let max_width =
fragment.iter().map(|frame| frame.width()).max().unwrap_or_default();
let pod = Regions {
size: Size::new(max_width, pod.size.y),
expand: Axes::new(true, pod.expand.y),
..pod
};
fragment = crate::layout_fragment(engine, body, locator, styles, pod)?;
}
fragment
}
// If we have a child that wants to layout with just access to the
// base region, give it that.
Some(BlockBody::SingleLayouter(callback)) => {
let pod = Region::new(pod.base(), pod.expand);
callback.call(engine, locator, styles, pod).map(Fragment::frame)?
}
// If we have a child that wants to layout with full region access,
// we layout it.
//
// For auto-sized multi-layouters, we propagate the outer expansion
// so that they can decide for themselves. We also ensure again to
// only expand if the size is finite.
Some(BlockBody::MultiLayouter(callback)) => {
let expand = (pod.expand | regions.expand) & pod.size.map(Abs::is_finite);
let pod = Regions { expand, ..pod };
callback.call(engine, locator, styles, pod)?
}
};
// Prepare fill and stroke.
let fill = elem.fill(styles);
let stroke = elem
.stroke(styles)
.unwrap_or_default()
.map(|s| s.map(Stroke::unwrap_or_default));
// Only fetch these if necessary (for clipping or filling/stroking).
let outset = Lazy::new(|| elem.outset(styles).unwrap_or_default());
let radius = Lazy::new(|| elem.radius(styles).unwrap_or_default());
// Fetch/compute these outside of the loop.
let clip = elem.clip(styles);
let has_fill_or_stroke = fill.is_some() || stroke.iter().any(Option::is_some);
let has_inset = !inset.is_zero();
let is_explicit = matches!(body, None | Some(BlockBody::Content(_)));
// Skip filling/stroking the first frame if it is empty and a non-empty
// one follows.
let mut skip_first = false;
if let [first, rest @ ..] = fragment.as_slice() {
skip_first = has_fill_or_stroke
&& first.is_empty()
&& rest.iter().any(|frame| !frame.is_empty());
}
// Post-process to apply insets, clipping, fills, and strokes.
for (i, (frame, region)) in fragment.iter_mut().zip(pod.iter()).enumerate() {
// Explicit blocks are boundaries for gradient relativeness.
if is_explicit {
frame.set_kind(FrameKind::Hard);
}
// Enforce a correct frame size on the expanded axes. Do this before
// applying the inset, since the pod shrunk.
frame.set_size(pod.expand.select(region, frame.size()));
// Apply the inset.
if has_inset {
crate::pad::grow(frame, &inset);
}
// Clip the contents, if requested.
if clip {
let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis();
frame.clip(clip_rect(size, &radius, &stroke));
}
// Add fill and/or stroke.
if has_fill_or_stroke && (i > 0 || !skip_first) {
fill_and_stroke(frame, fill.clone(), &stroke, &outset, &radius, elem.span());
}
}
// Assign label to each frame in the fragment.
if let Some(label) = elem.label() {
for frame in fragment.iter_mut() {
frame.label(label);
}
}
Ok(fragment)
}
/// Builds the pod region for an unbreakable sized container.
pub(crate) fn unbreakable_pod(
width: &Sizing,
height: &Sizing,
inset: &Sides<Rel<Abs>>,
styles: StyleChain,
base: Size,
) -> Region {
// Resolve the size.
let mut size = Size::new(
match width {
// - For auto, the whole region is available.
// - Fr is handled outside and already factored into the `region`,
// so we can treat it equivalently to 100%.
Sizing::Auto | Sizing::Fr(_) => base.x,
// Resolve the relative sizing.
Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.x),
},
match height {
Sizing::Auto | Sizing::Fr(_) => base.y,
Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.y),
},
);
// Take the inset, if any, into account.
if !inset.is_zero() {
size = crate::pad::shrink(size, inset);
}
// If the child is manually, the size is forced and we should enable
// expansion.
let expand = Axes::new(
*width != Sizing::Auto && size.x.is_finite(),
*height != Sizing::Auto && size.y.is_finite(),
);
Region::new(size, expand)
}
/// Builds the pod regions for a breakable sized container.
fn breakable_pod<'a>(
width: &Sizing,
height: &Sizing,
inset: &Sides<Rel<Abs>>,
styles: StyleChain,
regions: Regions,
buf: &'a mut SmallVec<[Abs; 2]>,
) -> Regions<'a> {
let base = regions.base();
// The vertical region sizes we're about to build.
let first;
let full;
let backlog: &mut [Abs];
let last;
// If the block has a fixed height, things are very different, so we
// handle that case completely separately.
match height {
Sizing::Auto | Sizing::Fr(_) => {
// If the block is automatically sized, we can just inherit the
// regions.
first = regions.size.y;
full = regions.full;
buf.extend_from_slice(regions.backlog);
backlog = buf;
last = regions.last;
}
Sizing::Rel(rel) => {
// Resolve the sizing to a concrete size.
let resolved = rel.resolve(styles).relative_to(base.y);
// Since we're manually sized, the resolved size is the base height.
full = resolved;
// Distribute the fixed height across a start region and a backlog.
(first, backlog) = distribute(resolved, regions, buf);
// If the height is manually sized, we don't want a final repeatable
// region.
last = None;
}
};
// Resolve the horizontal sizing to a concrete width and combine
// `width` and `first` into `size`.
let mut size = Size::new(
match width {
Sizing::Auto | Sizing::Fr(_) => regions.size.x,
Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.x),
},
first,
);
// Take the inset, if any, into account, applying it to the
// individual region components.
let (mut full, mut last) = (full, last);
if !inset.is_zero() {
crate::pad::shrink_multiple(&mut size, &mut full, backlog, &mut last, inset);
}
// If the child is manually, the size is forced and we should enable
// expansion.
let expand = Axes::new(
*width != Sizing::Auto && size.x.is_finite(),
*height != Sizing::Auto && size.y.is_finite(),
);
Regions { size, full, backlog, last, expand }
}
/// Distribute a fixed height spread over existing regions into a new first
/// height and a new backlog.
fn distribute<'a>(
height: Abs,
mut regions: Regions,
buf: &'a mut SmallVec<[Abs; 2]>,
) -> (Abs, &'a mut [Abs]) {
// Build new region heights from old regions.
let mut remaining = height;
loop {
let limited = regions.size.y.clamp(Abs::zero(), remaining);
buf.push(limited);
remaining -= limited;
if remaining.approx_empty()
|| !regions.may_break()
|| (!regions.may_progress() && limited.approx_empty())
{
break;
}
regions.next();
}
// If there is still something remaining, apply it to the
// last region (it will overflow, but there's nothing else
// we can do).
if !remaining.approx_empty() {
if let Some(last) = buf.last_mut() {
*last += remaining;
}
}
// Distribute the heights to the first region and the
// backlog. There is no last region, since the height is
// fixed.
(buf[0], &mut buf[1..])
}

View File

@ -6,22 +6,23 @@ use bumpalo::boxed::Box as BumpBox;
use bumpalo::Bump;
use comemo::{Track, Tracked, TrackedMut};
use once_cell::unsync::Lazy;
use crate::diag::{bail, SourceResult};
use crate::engine::{Engine, Route, Sink, Traced};
use crate::foundations::{Packed, Resolve, Smart, StyleChain};
use crate::introspection::{
use typst_library::diag::{bail, SourceResult};
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Packed, Resolve, Smart, StyleChain};
use typst_library::introspection::{
Introspector, Location, Locator, LocatorLink, SplitLocator, Tag, TagElem,
};
use crate::layout::{
layout_frame, Abs, AlignElem, Alignment, Axes, BlockElem, ColbreakElem,
FixedAlignment, FlushElem, Fr, Fragment, Frame, PagebreakElem, PlaceElem,
PlacementScope, Ratio, Region, Regions, Rel, Size, Sizing, Spacing, VElem,
use typst_library::layout::{
Abs, AlignElem, Alignment, Axes, BlockElem, ColbreakElem, FixedAlignment, FlushElem,
Fr, Fragment, Frame, PagebreakElem, PlaceElem, PlacementScope, Ratio, Region,
Regions, Rel, Size, Sizing, Spacing, VElem,
};
use crate::model::ParElem;
use crate::realize::Pair;
use crate::text::TextElem;
use crate::World;
use typst_library::model::ParElem;
use typst_library::routines::{Pair, Routines};
use typst_library::text::TextElem;
use typst_library::World;
use super::{layout_multi_block, layout_single_block};
/// Collects all elements of the flow into prepared children. These are much
/// simpler to handle than the raw elements.
@ -110,7 +111,7 @@ impl<'a> Collector<'a, '_, '_> {
let spacing = ParElem::spacing_in(styles);
let costs = TextElem::costs_in(styles);
let lines = crate::layout::layout_inline(
let lines = crate::layout_inline(
self.engine,
&elem.children,
self.locator.next(&elem.span()),
@ -332,6 +333,7 @@ impl SingleChild<'_> {
// Vertical expansion is only kept if this block is the only child.
region.expand.y &= self.alone;
layout_single_impl(
engine.routines,
engine.world,
engine.introspector,
engine.traced,
@ -350,6 +352,7 @@ impl SingleChild<'_> {
#[comemo::memoize]
#[allow(clippy::too_many_arguments)]
fn layout_single_impl(
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
traced: Tracked<Traced>,
@ -363,6 +366,7 @@ fn layout_single_impl(
let link = LocatorLink::new(locator);
let locator = Locator::link(&link);
let mut engine = Engine {
routines,
world,
introspector,
traced,
@ -370,7 +374,7 @@ fn layout_single_impl(
route: Route::extend(route),
};
elem.layout_single(&mut engine, locator, styles, region)
layout_single_block(elem, &mut engine, locator, styles, region)
.map(|frame| frame.post_processed(styles))
}
@ -425,6 +429,7 @@ impl<'a> MultiChild<'a> {
// Vertical expansion is only kept if this block is the only child.
regions.expand.y &= self.alone;
layout_multi_impl(
engine.routines,
engine.world,
engine.introspector,
engine.traced,
@ -443,6 +448,7 @@ impl<'a> MultiChild<'a> {
#[comemo::memoize]
#[allow(clippy::too_many_arguments)]
fn layout_multi_impl(
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
traced: Tracked<Traced>,
@ -456,6 +462,7 @@ fn layout_multi_impl(
let link = LocatorLink::new(locator);
let locator = Locator::link(&link);
let mut engine = Engine {
routines,
world,
introspector,
traced,
@ -463,13 +470,12 @@ fn layout_multi_impl(
route: Route::extend(route),
};
elem.layout_multiple(&mut engine, locator, styles, regions)
.map(|mut fragment| {
for frame in &mut fragment {
frame.post_process(styles);
}
fragment
})
layout_multi_block(elem, &mut engine, locator, styles, regions).map(|mut fragment| {
for frame in &mut fragment {
frame.post_process(styles);
}
fragment
})
}
/// The spilled remains of a `MultiChild` that broke across two regions.
@ -571,7 +577,7 @@ impl PlacedChild<'_> {
let align = self.alignment.unwrap_or_else(|| Alignment::CENTER);
let aligned = AlignElem::set_alignment(align).wrap();
let mut frame = layout_frame(
let mut frame = crate::layout_frame(
engine,
&self.elem.body,
self.locator.relayout(),
@ -614,7 +620,7 @@ impl<T> CachedCell<T> {
T: Clone,
F: FnOnce(I) -> T,
{
let input_hash = crate::utils::hash128(&input);
let input_hash = typst_utils::hash128(&input);
let mut slot = self.0.borrow_mut();
if let Some((hash, output)) = &*slot {

View File

@ -1,22 +1,23 @@
use std::num::NonZeroUsize;
use super::{distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Stop, Work};
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{Content, NativeElement, Packed, Resolve, Smart};
use crate::introspection::{
use typst_library::diag::SourceResult;
use typst_library::engine::Engine;
use typst_library::foundations::{Content, NativeElement, Packed, Resolve, Smart};
use typst_library::introspection::{
Counter, CounterDisplayElem, CounterState, CounterUpdate, Location, Locator,
SplitLocator, Tag,
};
use crate::layout::{
layout_fragment, layout_frame, Abs, Axes, Dir, FixedAlignment, Fragment, Frame,
FrameItem, OuterHAlignment, PlacementScope, Point, Region, Regions, Rel, Size,
use typst_library::layout::{
Abs, Axes, Dir, FixedAlignment, Fragment, Frame, FrameItem, OuterHAlignment,
PlacementScope, Point, Region, Regions, Rel, Size,
};
use crate::model::{
use typst_library::model::{
FootnoteElem, FootnoteEntry, LineNumberingScope, Numbering, ParLineMarker,
};
use crate::syntax::Span;
use crate::utils::NonZeroExt;
use typst_syntax::Span;
use typst_utils::NonZeroExt;
use super::{distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Stop, Work};
/// Composes the contents of a single page/region. A region can have multiple
/// columns/subregions.
@ -517,7 +518,7 @@ fn layout_footnote_separator(
config: &Config,
base: Size,
) -> SourceResult<Frame> {
layout_frame(
crate::layout_frame(
engine,
&config.footnote.separator,
Locator::root(),
@ -534,7 +535,7 @@ fn layout_footnote(
pod: Regions,
) -> SourceResult<Fragment> {
let loc = elem.location().unwrap();
layout_fragment(
crate::layout_fragment(
engine,
&FootnoteEntry::new(elem.clone()).pack(),
Locator::synthesize(loc),
@ -785,7 +786,7 @@ fn layout_line_number_reset(
let counter = Counter::of(ParLineMarker::elem());
let update = CounterUpdate::Set(CounterState::init(false));
let content = counter.update(Span::detached(), update);
layout_frame(
crate::layout_frame(
engine,
&content,
locator.next(&()),
@ -821,7 +822,7 @@ fn layout_line_number(
]);
// Layout the number.
let mut frame = layout_frame(
let mut frame = crate::layout_frame(
engine,
&content,
locator.next(&()),

View File

@ -1,12 +1,13 @@
use typst_library::introspection::Tag;
use typst_library::layout::{
Abs, Axes, FixedAlignment, Fr, Frame, FrameItem, Point, Region, Regions, Rel, Size,
};
use typst_utils::Numeric;
use super::{
Child, Composer, FlowResult, LineChild, MultiChild, MultiSpill, PlacedChild,
SingleChild, Stop, Work,
};
use crate::introspection::Tag;
use crate::layout::{
Abs, Axes, FixedAlignment, Fr, Frame, FrameItem, Point, Region, Regions, Rel, Size,
};
use crate::utils::Numeric;
/// Distributes as many children as fit from `composer.work` into the first
/// region and returns the resulting frame.

View File

@ -1,9 +1,12 @@
//! Layout of content into a [`Frame`] or [`Fragment`].
mod block;
mod collect;
mod compose;
mod distribute;
pub(crate) use self::block::unbreakable_pod;
use std::collections::HashSet;
use std::num::NonZeroUsize;
use std::rc::Rc;
@ -11,26 +14,40 @@ use std::rc::Rc;
use bumpalo::Bump;
use comemo::{Track, Tracked, TrackedMut};
use ecow::EcoVec;
use typst_library::diag::{bail, At, SourceDiagnostic, SourceResult};
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
use typst_library::introspection::{
Introspector, Location, Locator, LocatorLink, SplitLocator, Tag,
};
use typst_library::layout::{
Abs, ColumnsElem, Dir, Em, Fragment, Frame, PageElem, PlacementScope, Region,
Regions, Rel, Size,
};
use typst_library::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine};
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
use typst_library::text::TextElem;
use typst_library::World;
use typst_utils::{NonZeroExt, Numeric};
use self::block::{layout_multi_block, layout_single_block};
use self::collect::{
collect, Child, LineChild, MultiChild, MultiSpill, PlacedChild, SingleChild,
};
use self::compose::{compose, Composer};
use self::distribute::distribute;
use crate::diag::{bail, At, SourceDiagnostic, SourceResult};
use crate::engine::{Engine, Route, Sink, Traced};
use crate::foundations::{Content, Packed, Resolve, StyleChain};
use crate::introspection::{
Introspector, Location, Locator, LocatorLink, SplitLocator, Tag,
};
use crate::layout::{
Abs, Dir, Em, Fragment, Frame, PageElem, PlacementScope, Region, Regions, Rel, Size,
};
use crate::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine};
use crate::realize::{realize, Arenas, Pair, RealizationKind};
use crate::text::TextElem;
use crate::utils::{NonZeroExt, Numeric};
use crate::World;
/// Lays out content into a single region, producing a single frame.
pub fn layout_frame(
engine: &mut Engine,
content: &Content,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
layout_fragment(engine, content, locator, styles, region.into())
.map(Fragment::into_frame)
}
/// Lays out content into multiple regions.
///
@ -43,6 +60,7 @@ pub fn layout_fragment(
regions: Regions,
) -> SourceResult<Fragment> {
layout_fragment_impl(
engine.routines,
engine.world,
engine.introspector,
engine.traced,
@ -57,50 +75,39 @@ pub fn layout_fragment(
)
}
/// Lays out content into regions with columns.
/// Layout the columns.
///
/// This is different from just laying out into column-sized regions as the
/// columns can interact due to parent-scoped placed elements.
pub fn layout_fragment_with_columns(
#[typst_macros::time(span = elem.span())]
pub fn layout_columns(
elem: &Packed<ColumnsElem>,
engine: &mut Engine,
content: &Content,
locator: Locator,
styles: StyleChain,
regions: Regions,
count: NonZeroUsize,
gutter: Rel<Abs>,
) -> SourceResult<Fragment> {
layout_fragment_impl(
engine.routines,
engine.world,
engine.introspector,
engine.traced,
TrackedMut::reborrow_mut(&mut engine.sink),
engine.route.track(),
content,
&elem.body,
locator.track(),
styles,
regions,
count,
gutter,
elem.count(styles),
elem.gutter(styles),
)
}
/// Lays out content into a single region, producing a single frame.
pub fn layout_frame(
engine: &mut Engine,
content: &Content,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
layout_fragment(engine, content, locator, styles, region.into())
.map(Fragment::into_frame)
}
/// The cached, internal implementation of [`layout_fragment`].
#[comemo::memoize]
#[allow(clippy::too_many_arguments)]
fn layout_fragment_impl(
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
traced: Tracked<Traced>,
@ -123,6 +130,7 @@ fn layout_fragment_impl(
let link = LocatorLink::new(locator);
let mut locator = Locator::link(&link).split();
let mut engine = Engine {
routines,
world,
introspector,
traced,
@ -133,7 +141,7 @@ fn layout_fragment_impl(
engine.route.check_layout_depth().at(content.span())?;
let arenas = Arenas::default();
let children = realize(
let children = (engine.routines.realize)(
RealizationKind::Container,
&mut engine,
&mut locator,

View File

@ -1,161 +1,89 @@
use std::num::NonZeroUsize;
use std::sync::Arc;
use comemo::Track;
use ecow::eco_format;
use super::lines::Line;
use super::repeated::{Footer, Header, Repeatable};
use crate::diag::{bail, At, Hint, HintedStrResult, HintedString, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
Array, CastInfo, Content, Context, Fold, FromValue, Func, IntoValue, Reflect,
Resolve, Smart, StyleChain, Value,
use typst_library::diag::{bail, At, Hint, HintedStrResult, HintedString, SourceResult};
use typst_library::engine::Engine;
use typst_library::foundations::{Content, Smart, StyleChain};
use typst_library::introspection::Locator;
use typst_library::layout::{
Abs, Alignment, Axes, Celled, Fragment, Length, Regions, Rel, ResolvedCelled, Sides,
Sizing,
};
use crate::introspection::Locator;
use crate::layout::{
layout_fragment, Abs, Alignment, Axes, Fragment, Length, LinePosition, Regions, Rel,
Sides, Sizing,
};
use crate::syntax::Span;
use crate::utils::NonZeroExt;
use crate::visualize::{Paint, Stroke};
use typst_library::visualize::{Paint, Stroke};
use typst_syntax::Span;
use typst_utils::NonZeroExt;
/// A value that can be configured per cell.
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum Celled<T> {
/// A bare value, the same for all cells.
Value(T),
/// A closure mapping from cell coordinates to a value.
Func(Func),
/// An array of alignment values corresponding to each column.
Array(Vec<T>),
}
use super::{Footer, Header, Line, Repeatable};
impl<T: Default + Clone + FromValue> Celled<T> {
/// Resolve the value based on the cell position.
pub fn resolve(
&self,
engine: &mut Engine,
styles: StyleChain,
/// Used for cell-like elements which are aware of their final properties in
/// the table, and may have property overrides.
pub trait ResolvableCell {
/// Resolves the cell's fields, given its coordinates and default grid-wide
/// fill, align, inset and stroke properties, plus the expected value of
/// the `breakable` field.
/// Returns a final Cell.
#[allow(clippy::too_many_arguments)]
fn resolve_cell<'a>(
self,
x: usize,
y: usize,
) -> SourceResult<T> {
Ok(match self {
Self::Value(value) => value.clone(),
Self::Func(func) => func
.call(engine, Context::new(None, Some(styles)).track(), [x, y])?
.cast()
.at(func.span())?,
Self::Array(array) => x
.checked_rem(array.len())
.and_then(|i| array.get(i))
.cloned()
.unwrap_or_default(),
})
}
}
impl<T: Default> Default for Celled<T> {
fn default() -> Self {
Self::Value(T::default())
}
}
impl<T: Reflect> Reflect for Celled<T> {
fn input() -> CastInfo {
T::input() + Array::input() + Func::input()
}
fn output() -> CastInfo {
T::output() + Array::output() + Func::output()
}
fn castable(value: &Value) -> bool {
Array::castable(value) || Func::castable(value) || T::castable(value)
}
}
impl<T: IntoValue> IntoValue for Celled<T> {
fn into_value(self) -> Value {
match self {
Self::Value(value) => value.into_value(),
Self::Func(func) => func.into_value(),
Self::Array(arr) => arr.into_value(),
}
}
}
impl<T: FromValue> FromValue for Celled<T> {
fn from_value(value: Value) -> HintedStrResult<Self> {
match value {
Value::Func(v) => Ok(Self::Func(v)),
Value::Array(array) => Ok(Self::Array(
array.into_iter().map(T::from_value).collect::<HintedStrResult<_>>()?,
)),
v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)),
v => Err(Self::error(&v)),
}
}
}
impl<T: Fold> Fold for Celled<T> {
fn fold(self, outer: Self) -> Self {
match (self, outer) {
(Self::Value(inner), Self::Value(outer)) => Self::Value(inner.fold(outer)),
(self_, _) => self_,
}
}
}
impl<T: Resolve> Resolve for Celled<T> {
type Output = ResolvedCelled<T>;
fn resolve(self, styles: StyleChain) -> Self::Output {
match self {
Self::Value(value) => ResolvedCelled(Celled::Value(value.resolve(styles))),
Self::Func(func) => ResolvedCelled(Celled::Func(func)),
Self::Array(values) => ResolvedCelled(Celled::Array(
values.into_iter().map(|value| value.resolve(styles)).collect(),
)),
}
}
}
/// The result of resolving a Celled's value according to styles.
/// Holds resolved values which depend on each grid cell's position.
/// When it is a closure, however, it is only resolved when the closure is
/// called.
#[derive(Default, Clone)]
pub struct ResolvedCelled<T: Resolve>(Celled<T::Output>);
impl<T> ResolvedCelled<T>
where
T: FromValue + Resolve,
<T as Resolve>::Output: Default + Clone,
{
/// Resolve the value based on the cell position.
pub fn resolve(
&self,
engine: &mut Engine,
fill: &Option<Paint>,
align: Smart<Alignment>,
inset: Sides<Option<Rel<Length>>>,
stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>,
breakable: bool,
locator: Locator<'a>,
styles: StyleChain,
x: usize,
y: usize,
) -> SourceResult<T::Output> {
Ok(match &self.0 {
Celled::Value(value) => value.clone(),
Celled::Func(func) => func
.call(engine, Context::new(None, Some(styles)).track(), [x, y])?
.cast::<T>()
.at(func.span())?
.resolve(styles),
Celled::Array(array) => x
.checked_rem(array.len())
.and_then(|i| array.get(i))
.cloned()
.unwrap_or_default(),
})
}
) -> Cell<'a>;
/// Returns this cell's column override.
fn x(&self, styles: StyleChain) -> Smart<usize>;
/// Returns this cell's row override.
fn y(&self, styles: StyleChain) -> Smart<usize>;
/// The amount of columns spanned by this cell.
fn colspan(&self, styles: StyleChain) -> NonZeroUsize;
/// The amount of rows spanned by this cell.
fn rowspan(&self, styles: StyleChain) -> NonZeroUsize;
/// The cell's span, for errors.
fn span(&self) -> Span;
}
/// A grid item, possibly affected by automatic cell positioning. Can be either
/// a line or a cell.
pub enum ResolvableGridItem<T: ResolvableCell> {
/// A horizontal line in the grid.
HLine {
/// The row above which the horizontal line is drawn.
y: Smart<usize>,
start: usize,
end: Option<NonZeroUsize>,
stroke: Option<Arc<Stroke<Abs>>>,
/// The span of the corresponding line element.
span: Span,
/// The line's position. "before" here means on top of row `y`, while
/// "after" means below it.
position: LinePosition,
},
/// A vertical line in the grid.
VLine {
/// The column before which the vertical line is drawn.
x: Smart<usize>,
start: usize,
end: Option<NonZeroUsize>,
stroke: Option<Arc<Stroke<Abs>>>,
/// The span of the corresponding line element.
span: Span,
/// The line's position. "before" here means to the left of column `x`,
/// while "after" means to its right (both considering LTR).
position: LinePosition,
},
/// A cell in the grid.
Cell(T),
}
/// Represents a cell in CellGrid, to be laid out by GridLayouter.
@ -221,12 +149,24 @@ impl<'a> Cell<'a> {
if disambiguator > 0 {
locator = locator.split().next_inner(disambiguator as u128);
}
layout_fragment(engine, &self.body, locator, styles, regions)
crate::layout_fragment(engine, &self.body, locator, styles, regions)
}
}
/// Indicates whether the line should be drawn before or after the track with
/// its index. This is mostly only relevant when gutter is used, since, then,
/// the position after a track is not the same as before the next
/// non-gutter track.
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum LinePosition {
/// The line should be drawn before its track (e.g. hline on top of a row).
Before,
/// The line should be drawn after its track (e.g. hline below a row).
After,
}
/// A grid entry.
pub(super) enum Entry<'a> {
pub enum Entry<'a> {
/// An entry which holds a cell.
Cell(Cell<'a>),
/// An entry which is merged with another cell.
@ -246,39 +186,6 @@ impl<'a> Entry<'a> {
}
}
/// A grid item, possibly affected by automatic cell positioning. Can be either
/// a line or a cell.
pub enum ResolvableGridItem<T: ResolvableCell> {
/// A horizontal line in the grid.
HLine {
/// The row above which the horizontal line is drawn.
y: Smart<usize>,
start: usize,
end: Option<NonZeroUsize>,
stroke: Option<Arc<Stroke<Abs>>>,
/// The span of the corresponding line element.
span: Span,
/// The line's position. "before" here means on top of row `y`, while
/// "after" means below it.
position: LinePosition,
},
/// A vertical line in the grid.
VLine {
/// The column before which the vertical line is drawn.
x: Smart<usize>,
start: usize,
end: Option<NonZeroUsize>,
stroke: Option<Arc<Stroke<Abs>>>,
/// The span of the corresponding line element.
span: Span,
/// The line's position. "before" here means to the left of column `x`,
/// while "after" means to its right (both considering LTR).
position: LinePosition,
},
/// A cell in the grid.
Cell(T),
}
/// Any grid child, which can be either a header or an item.
pub enum ResolvableGridChild<T: ResolvableCell, I> {
Header { repeat: bool, span: Span, items: I },
@ -286,65 +193,28 @@ pub enum ResolvableGridChild<T: ResolvableCell, I> {
Item(ResolvableGridItem<T>),
}
/// Used for cell-like elements which are aware of their final properties in
/// the table, and may have property overrides.
pub trait ResolvableCell {
/// Resolves the cell's fields, given its coordinates and default grid-wide
/// fill, align, inset and stroke properties, plus the expected value of
/// the `breakable` field.
/// Returns a final Cell.
#[allow(clippy::too_many_arguments)]
fn resolve_cell<'a>(
self,
x: usize,
y: usize,
fill: &Option<Paint>,
align: Smart<Alignment>,
inset: Sides<Option<Rel<Length>>>,
stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>,
breakable: bool,
locator: Locator<'a>,
styles: StyleChain,
) -> Cell<'a>;
/// Returns this cell's column override.
fn x(&self, styles: StyleChain) -> Smart<usize>;
/// Returns this cell's row override.
fn y(&self, styles: StyleChain) -> Smart<usize>;
/// The amount of columns spanned by this cell.
fn colspan(&self, styles: StyleChain) -> NonZeroUsize;
/// The amount of rows spanned by this cell.
fn rowspan(&self, styles: StyleChain) -> NonZeroUsize;
/// The cell's span, for errors.
fn span(&self) -> Span;
}
/// A grid of cells, including the columns, rows, and cell data.
pub struct CellGrid<'a> {
/// The grid cells.
pub(super) entries: Vec<Entry<'a>>,
pub entries: Vec<Entry<'a>>,
/// The column tracks including gutter tracks.
pub(super) cols: Vec<Sizing>,
pub cols: Vec<Sizing>,
/// The row tracks including gutter tracks.
pub(super) rows: Vec<Sizing>,
pub rows: Vec<Sizing>,
/// The vertical lines before each column, or on the end border.
/// Gutter columns are not included.
/// Contains up to 'cols_without_gutter.len() + 1' vectors of lines.
pub(super) vlines: Vec<Vec<Line>>,
pub vlines: Vec<Vec<Line>>,
/// The horizontal lines on top of each row, or on the bottom border.
/// Gutter rows are not included.
/// Contains up to 'rows_without_gutter.len() + 1' vectors of lines.
pub(super) hlines: Vec<Vec<Line>>,
pub hlines: Vec<Vec<Line>>,
/// The repeatable header of this grid.
pub(super) header: Option<Repeatable<Header>>,
pub header: Option<Repeatable<Header>>,
/// The repeatable footer of this grid.
pub(super) footer: Option<Repeatable<Footer>>,
pub footer: Option<Repeatable<Footer>>,
/// Whether this grid has gutters.
pub(super) has_gutter: bool,
pub has_gutter: bool,
}
impl<'a> CellGrid<'a> {
@ -1125,7 +995,7 @@ impl<'a> CellGrid<'a> {
}
/// Generates the cell grid, given the tracks and resolved entries.
pub(super) fn new_internal(
pub fn new_internal(
tracks: Axes<&[Sizing]>,
gutter: Axes<&[Sizing]>,
vlines: Vec<Vec<Line>>,
@ -1194,7 +1064,7 @@ impl<'a> CellGrid<'a> {
///
/// Returns `None` if it's a gutter cell.
#[track_caller]
pub(super) fn entry(&self, x: usize, y: usize) -> Option<&Entry<'a>> {
pub fn entry(&self, x: usize, y: usize) -> Option<&Entry<'a>> {
assert!(x < self.cols.len());
assert!(y < self.rows.len());
@ -1216,7 +1086,7 @@ impl<'a> CellGrid<'a> {
///
/// Returns `None` if it's a gutter cell or merged position.
#[track_caller]
pub(super) fn cell(&self, x: usize, y: usize) -> Option<&Cell<'a>> {
pub fn cell(&self, x: usize, y: usize) -> Option<&Cell<'a>> {
self.entry(x, y).and_then(Entry::as_cell)
}
@ -1228,7 +1098,7 @@ impl<'a> CellGrid<'a> {
/// - If it is a merged cell, returns the parent cell's position.
/// - If it is a gutter cell, returns None.
#[track_caller]
pub(super) fn parent_cell_position(&self, x: usize, y: usize) -> Option<Axes<usize>> {
pub fn parent_cell_position(&self, x: usize, y: usize) -> Option<Axes<usize>> {
self.entry(x, y).map(|entry| match entry {
Entry::Cell(_) => Axes::new(x, y),
Entry::Merged { parent } => {
@ -1260,7 +1130,7 @@ impl<'a> CellGrid<'a> {
/// position, if it is gutter), if it exists; otherwise returns None (it's
/// gutter and no cell spans it).
#[track_caller]
pub(super) fn effective_parent_cell_position(
pub fn effective_parent_cell_position(
&self,
x: usize,
y: usize,
@ -1283,14 +1153,14 @@ impl<'a> CellGrid<'a> {
/// Checks if the track with the given index is gutter.
/// Does not check if the index is a valid track.
#[inline]
pub(super) fn is_gutter_track(&self, index: usize) -> bool {
pub fn is_gutter_track(&self, index: usize) -> bool {
self.has_gutter && index % 2 == 1
}
/// Returns the effective colspan of a cell, considering the gutters it
/// might span if the grid has gutters.
#[inline]
pub(super) fn effective_colspan_of_cell(&self, cell: &Cell) -> usize {
pub fn effective_colspan_of_cell(&self, cell: &Cell) -> usize {
if self.has_gutter {
2 * cell.colspan.get() - 1
} else {
@ -1301,7 +1171,7 @@ impl<'a> CellGrid<'a> {
/// Returns the effective rowspan of a cell, considering the gutters it
/// might span if the grid has gutters.
#[inline]
pub(super) fn effective_rowspan_of_cell(&self, cell: &Cell) -> usize {
pub fn effective_rowspan_of_cell(&self, cell: &Cell) -> usize {
if self.has_gutter {
2 * cell.rowspan.get() - 1
} else {

View File

@ -1,22 +1,21 @@
use std::fmt::Debug;
use super::lines::{
generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, LinePosition,
LineSegment,
use typst_library::diag::{bail, SourceResult};
use typst_library::engine::Engine;
use typst_library::foundations::{Resolve, StyleChain};
use typst_library::layout::{
Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel,
Size, Sizing,
};
use super::repeated::Repeatable;
use super::rowspans::{Rowspan, UnbreakableRowGroup};
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{Resolve, StyleChain};
use crate::layout::{
Abs, Axes, Cell, CellGrid, Dir, Fr, Fragment, Frame, FrameItem, Length, Point,
Region, Regions, Rel, Size, Sizing,
use typst_library::text::TextElem;
use typst_library::visualize::Geometry;
use typst_syntax::Span;
use typst_utils::{MaybeReverseIter, Numeric};
use super::{
generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Cell, CellGrid,
LinePosition, LineSegment, Repeatable, Rowspan, UnbreakableRowGroup,
};
use crate::syntax::Span;
use crate::text::TextElem;
use crate::utils::{MaybeReverseIter, Numeric};
use crate::visualize::Geometry;
/// Performs grid layout.
pub struct GridLayouter<'a> {

View File

@ -1,12 +1,11 @@
use std::num::NonZeroUsize;
use std::sync::Arc;
use super::cells::CellGrid;
use super::layout::RowPiece;
use super::repeated::Repeatable;
use crate::foundations::{AlternativeFold, Fold};
use crate::layout::Abs;
use crate::visualize::Stroke;
use typst_library::foundations::{AlternativeFold, Fold};
use typst_library::layout::Abs;
use typst_library::visualize::Stroke;
use super::{CellGrid, LinePosition, Repeatable, RowPiece};
/// Represents an explicit grid line (horizontal or vertical) specified by the
/// user.
@ -38,22 +37,10 @@ pub struct Line {
pub position: LinePosition,
}
/// Indicates whether the line should be drawn before or after the track with
/// its index. This is mostly only relevant when gutter is used, since, then,
/// the position after a track is not the same as before the next
/// non-gutter track.
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum LinePosition {
/// The line should be drawn before its track (e.g. hline on top of a row).
Before,
/// The line should be drawn after its track (e.g. hline below a row).
After,
}
/// Indicates which priority a particular grid line segment should have, based
/// on the highest priority configuration that defined the segment's stroke.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum StrokePriority {
pub enum StrokePriority {
/// The stroke of the segment was derived solely from the grid's global
/// stroke setting, so it should have the lowest priority.
GridStroke = 0,
@ -71,19 +58,19 @@ pub(super) enum StrokePriority {
/// Data for a particular line segment in the grid as generated by
/// `generate_line_segments`.
#[derive(Debug, PartialEq, Eq)]
pub(super) struct LineSegment {
pub struct LineSegment {
/// The stroke with which to draw this segment.
pub(super) stroke: Arc<Stroke<Abs>>,
pub stroke: Arc<Stroke<Abs>>,
/// The offset of this segment since the beginning of its axis.
/// For a vertical line segment, this is the offset since the top of the
/// table in the current page; for a horizontal line segment, this is the
/// offset since the start border of the table.
pub(super) offset: Abs,
pub offset: Abs,
/// The length of this segment.
pub(super) length: Abs,
pub length: Abs,
/// The segment's drawing priority, indicating on top of which other
/// segments this one should be drawn.
pub(super) priority: StrokePriority,
pub priority: StrokePriority,
}
/// Generates the segments of lines that should be drawn alongside a certain
@ -119,7 +106,7 @@ pub(super) struct LineSegment {
/// number, and they must be iterable over pairs of (number, size). For
/// vertical lines, for instance, `tracks` would describe the rows in the
/// current region, as pairs (row index, row height).
pub(super) fn generate_line_segments<'grid, F, I, L>(
pub fn generate_line_segments<'grid, F, I, L>(
grid: &'grid CellGrid,
tracks: I,
index: usize,
@ -269,7 +256,7 @@ where
current_segment =
Some(LineSegment { stroke, offset, length: size, priority });
}
} else if let Some(old_segment) = current_segment.take() {
} else if let Some(old_segment) = Option::take(&mut current_segment) {
// We shouldn't draw here (stroke of None), so we yield the
// current segment, as it was interrupted.
offset += size;
@ -289,7 +276,9 @@ where
// closure, the current segment will necessarily be 'None',
// so the iterator will necessarily end (that is, we will return None)
// after this.
current_segment.take()
//
// Note: Fully-qualified notation because rust-analyzer is confused.
Option::take(&mut current_segment)
})
}
@ -312,7 +301,7 @@ where
///
/// The priority associated with the returned stroke follows the rules
/// described in the docs for `generate_line_segment`.
pub(super) fn vline_stroke_at_row(
pub fn vline_stroke_at_row(
grid: &CellGrid,
x: usize,
y: usize,
@ -432,7 +421,7 @@ pub(super) fn vline_stroke_at_row(
///
/// This function assumes columns are sorted by increasing `x`, and rows are
/// sorted by increasing `y`.
pub(super) fn hline_stroke_at_column(
pub fn hline_stroke_at_column(
grid: &CellGrid,
rows: &[RowPiece],
local_top_y: Option<usize>,
@ -599,12 +588,14 @@ pub(super) fn hline_stroke_at_column(
#[cfg(test)]
mod test {
use typst_library::foundations::Content;
use typst_library::introspection::Locator;
use typst_library::layout::{Axes, Sides, Sizing};
use typst_utils::NonZeroExt;
use super::super::cells::Entry;
use super::super::Cell;
use super::*;
use crate::foundations::Content;
use crate::introspection::Locator;
use crate::layout::{Axes, Cell, Sides, Sizing};
use crate::utils::NonZeroExt;
fn sample_cell() -> Cell<'static> {
Cell {

View File

@ -0,0 +1,416 @@
mod cells;
mod layouter;
mod lines;
mod repeated;
mod rowspans;
pub use self::cells::{Cell, CellGrid};
pub use self::layouter::GridLayouter;
use std::num::NonZeroUsize;
use std::sync::Arc;
use ecow::eco_format;
use typst_library::diag::{SourceResult, Trace, Tracepoint};
use typst_library::engine::Engine;
use typst_library::foundations::{Fold, Packed, Smart, StyleChain};
use typst_library::introspection::Locator;
use typst_library::layout::{
Abs, Alignment, Axes, Dir, Fragment, GridCell, GridChild, GridElem, GridItem, Length,
OuterHAlignment, OuterVAlignment, Regions, Rel, Sides,
};
use typst_library::model::{TableCell, TableChild, TableElem, TableItem};
use typst_library::text::TextElem;
use typst_library::visualize::{Paint, Stroke};
use typst_syntax::Span;
use self::cells::{
LinePosition, ResolvableCell, ResolvableGridChild, ResolvableGridItem,
};
use self::layouter::RowPiece;
use self::lines::{
generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Line,
LineSegment,
};
use self::repeated::{Footer, Header, Repeatable};
use self::rowspans::{Rowspan, UnbreakableRowGroup};
/// Layout the grid.
#[typst_macros::time(span = elem.span())]
pub fn layout_grid(
elem: &Packed<GridElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let inset = elem.inset(styles);
let align = elem.align(styles);
let columns = elem.columns(styles);
let rows = elem.rows(styles);
let column_gutter = elem.column_gutter(styles);
let row_gutter = elem.row_gutter(styles);
let fill = elem.fill(styles);
let stroke = elem.stroke(styles);
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
// Use trace to link back to the grid when a specific cell errors
let tracepoint = || Tracepoint::Call(Some(eco_format!("grid")));
let resolve_item = |item: &GridItem| grid_item_to_resolvable(item, styles);
let children = elem.children().iter().map(|child| match child {
GridChild::Header(header) => ResolvableGridChild::Header {
repeat: header.repeat(styles),
span: header.span(),
items: header.children().iter().map(resolve_item),
},
GridChild::Footer(footer) => ResolvableGridChild::Footer {
repeat: footer.repeat(styles),
span: footer.span(),
items: footer.children().iter().map(resolve_item),
},
GridChild::Item(item) => {
ResolvableGridChild::Item(grid_item_to_resolvable(item, styles))
}
});
let grid = CellGrid::resolve(
tracks,
gutter,
locator,
children,
fill,
align,
&inset,
&stroke,
engine,
styles,
elem.span(),
)
.trace(engine.world, tracepoint, elem.span())?;
let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
// Measure the columns and layout the grid row-by-row.
layouter.layout(engine)
}
/// Layout the table.
#[typst_macros::time(span = elem.span())]
pub fn layout_table(
elem: &Packed<TableElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let inset = elem.inset(styles);
let align = elem.align(styles);
let columns = elem.columns(styles);
let rows = elem.rows(styles);
let column_gutter = elem.column_gutter(styles);
let row_gutter = elem.row_gutter(styles);
let fill = elem.fill(styles);
let stroke = elem.stroke(styles);
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
// Use trace to link back to the table when a specific cell errors
let tracepoint = || Tracepoint::Call(Some(eco_format!("table")));
let resolve_item = |item: &TableItem| table_item_to_resolvable(item, styles);
let children = elem.children().iter().map(|child| match child {
TableChild::Header(header) => ResolvableGridChild::Header {
repeat: header.repeat(styles),
span: header.span(),
items: header.children().iter().map(resolve_item),
},
TableChild::Footer(footer) => ResolvableGridChild::Footer {
repeat: footer.repeat(styles),
span: footer.span(),
items: footer.children().iter().map(resolve_item),
},
TableChild::Item(item) => {
ResolvableGridChild::Item(table_item_to_resolvable(item, styles))
}
});
let grid = CellGrid::resolve(
tracks,
gutter,
locator,
children,
fill,
align,
&inset,
&stroke,
engine,
styles,
elem.span(),
)
.trace(engine.world, tracepoint, elem.span())?;
let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
layouter.layout(engine)
}
fn grid_item_to_resolvable(
item: &GridItem,
styles: StyleChain,
) -> ResolvableGridItem<Packed<GridCell>> {
match item {
GridItem::HLine(hline) => ResolvableGridItem::HLine {
y: hline.y(styles),
start: hline.start(styles),
end: hline.end(styles),
stroke: hline.stroke(styles),
span: hline.span(),
position: match hline.position(styles) {
OuterVAlignment::Top => LinePosition::Before,
OuterVAlignment::Bottom => LinePosition::After,
},
},
GridItem::VLine(vline) => ResolvableGridItem::VLine {
x: vline.x(styles),
start: vline.start(styles),
end: vline.end(styles),
stroke: vline.stroke(styles),
span: vline.span(),
position: match vline.position(styles) {
OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => {
LinePosition::After
}
OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => {
LinePosition::Before
}
OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before,
OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After,
},
},
GridItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()),
}
}
fn table_item_to_resolvable(
item: &TableItem,
styles: StyleChain,
) -> ResolvableGridItem<Packed<TableCell>> {
match item {
TableItem::HLine(hline) => ResolvableGridItem::HLine {
y: hline.y(styles),
start: hline.start(styles),
end: hline.end(styles),
stroke: hline.stroke(styles),
span: hline.span(),
position: match hline.position(styles) {
OuterVAlignment::Top => LinePosition::Before,
OuterVAlignment::Bottom => LinePosition::After,
},
},
TableItem::VLine(vline) => ResolvableGridItem::VLine {
x: vline.x(styles),
start: vline.start(styles),
end: vline.end(styles),
stroke: vline.stroke(styles),
span: vline.span(),
position: match vline.position(styles) {
OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => {
LinePosition::After
}
OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => {
LinePosition::Before
}
OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before,
OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After,
},
},
TableItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()),
}
}
impl ResolvableCell for Packed<TableCell> {
fn resolve_cell<'a>(
mut self,
x: usize,
y: usize,
fill: &Option<Paint>,
align: Smart<Alignment>,
inset: Sides<Option<Rel<Length>>>,
stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>,
breakable: bool,
locator: Locator<'a>,
styles: StyleChain,
) -> Cell<'a> {
let cell = &mut *self;
let colspan = cell.colspan(styles);
let rowspan = cell.rowspan(styles);
let breakable = cell.breakable(styles).unwrap_or(breakable);
let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
let cell_stroke = cell.stroke(styles);
let stroke_overridden =
cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_))));
// Using a typical 'Sides' fold, an unspecified side loses to a
// specified side. Additionally, when both are specified, an inner
// None wins over the outer Some, and vice-versa. When both are
// specified and Some, fold occurs, which, remarkably, leads to an Arc
// clone.
//
// In the end, we flatten because, for layout purposes, an unspecified
// cell stroke is the same as specifying 'none', so we equate the two
// concepts.
let stroke = cell_stroke.fold(stroke).map(Option::flatten);
cell.push_x(Smart::Custom(x));
cell.push_y(Smart::Custom(y));
cell.push_fill(Smart::Custom(fill.clone()));
cell.push_align(match align {
Smart::Custom(align) => {
Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align)))
}
// Don't fold if the table is using outer alignment. Use the
// cell's alignment instead (which, in the end, will fold with
// the outer alignment when it is effectively displayed).
Smart::Auto => cell.align(styles),
});
cell.push_inset(Smart::Custom(
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)),
));
cell.push_stroke(
// Here we convert the resolved stroke to a regular stroke, however
// with resolved units (that is, 'em' converted to absolute units).
// We also convert any stroke unspecified by both the cell and the
// outer stroke ('None' in the folded stroke) to 'none', that is,
// all sides are present in the resulting Sides object accessible
// by show rules on table cells.
stroke.as_ref().map(|side| {
Some(side.as_ref().map(|cell_stroke| {
Arc::new((**cell_stroke).clone().map(Length::from))
}))
}),
);
cell.push_breakable(Smart::Custom(breakable));
Cell {
body: self.pack(),
locator,
fill,
colspan,
rowspan,
stroke,
stroke_overridden,
breakable,
}
}
fn x(&self, styles: StyleChain) -> Smart<usize> {
(**self).x(styles)
}
fn y(&self, styles: StyleChain) -> Smart<usize> {
(**self).y(styles)
}
fn colspan(&self, styles: StyleChain) -> NonZeroUsize {
(**self).colspan(styles)
}
fn rowspan(&self, styles: StyleChain) -> NonZeroUsize {
(**self).rowspan(styles)
}
fn span(&self) -> Span {
Packed::span(self)
}
}
impl ResolvableCell for Packed<GridCell> {
fn resolve_cell<'a>(
mut self,
x: usize,
y: usize,
fill: &Option<Paint>,
align: Smart<Alignment>,
inset: Sides<Option<Rel<Length>>>,
stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>,
breakable: bool,
locator: Locator<'a>,
styles: StyleChain,
) -> Cell<'a> {
let cell = &mut *self;
let colspan = cell.colspan(styles);
let rowspan = cell.rowspan(styles);
let breakable = cell.breakable(styles).unwrap_or(breakable);
let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
let cell_stroke = cell.stroke(styles);
let stroke_overridden =
cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_))));
// Using a typical 'Sides' fold, an unspecified side loses to a
// specified side. Additionally, when both are specified, an inner
// None wins over the outer Some, and vice-versa. When both are
// specified and Some, fold occurs, which, remarkably, leads to an Arc
// clone.
//
// In the end, we flatten because, for layout purposes, an unspecified
// cell stroke is the same as specifying 'none', so we equate the two
// concepts.
let stroke = cell_stroke.fold(stroke).map(Option::flatten);
cell.push_x(Smart::Custom(x));
cell.push_y(Smart::Custom(y));
cell.push_fill(Smart::Custom(fill.clone()));
cell.push_align(match align {
Smart::Custom(align) => {
Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align)))
}
// Don't fold if the grid is using outer alignment. Use the
// cell's alignment instead (which, in the end, will fold with
// the outer alignment when it is effectively displayed).
Smart::Auto => cell.align(styles),
});
cell.push_inset(Smart::Custom(
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)),
));
cell.push_stroke(
// Here we convert the resolved stroke to a regular stroke, however
// with resolved units (that is, 'em' converted to absolute units).
// We also convert any stroke unspecified by both the cell and the
// outer stroke ('None' in the folded stroke) to 'none', that is,
// all sides are present in the resulting Sides object accessible
// by show rules on grid cells.
stroke.as_ref().map(|side| {
Some(side.as_ref().map(|cell_stroke| {
Arc::new((**cell_stroke).clone().map(Length::from))
}))
}),
);
cell.push_breakable(Smart::Custom(breakable));
Cell {
body: self.pack(),
locator,
fill,
colspan,
rowspan,
stroke,
stroke_overridden,
breakable,
}
}
fn x(&self, styles: StyleChain) -> Smart<usize> {
(**self).x(styles)
}
fn y(&self, styles: StyleChain) -> Smart<usize> {
(**self).y(styles)
}
fn colspan(&self, styles: StyleChain) -> NonZeroUsize {
(**self).colspan(styles)
}
fn rowspan(&self, styles: StyleChain) -> NonZeroUsize {
(**self).rowspan(styles)
}
fn span(&self) -> Span {
Packed::span(self)
}
}

View File

@ -1,25 +1,27 @@
use typst_library::diag::SourceResult;
use typst_library::engine::Engine;
use typst_library::layout::{Abs, Axes, Frame, Regions};
use super::layouter::GridLayouter;
use super::rowspans::UnbreakableRowGroup;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::layout::{Abs, Axes, Frame, GridLayouter, Regions};
/// A repeatable grid header. Starts at the first row.
pub(super) struct Header {
pub struct Header {
/// The index after the last row included in this header.
pub(super) end: usize,
pub end: usize,
}
/// A repeatable grid footer. Stops at the last row.
pub(super) struct Footer {
pub struct Footer {
/// The first row included in this footer.
pub(super) start: usize,
pub start: usize,
}
/// A possibly repeatable grid object.
/// It still exists even when not repeatable, but must not have additional
/// considerations by grid layout, other than for consistency (such as making
/// a certain group of rows unbreakable).
pub(super) enum Repeatable<T> {
pub enum Repeatable<T> {
Repeated(T),
NotRepeated(T),
}
@ -27,7 +29,7 @@ pub(super) enum Repeatable<T> {
impl<T> Repeatable<T> {
/// Gets the value inside this repeatable, regardless of whether
/// it repeats.
pub(super) fn unwrap(&self) -> &T {
pub fn unwrap(&self) -> &T {
match self {
Self::Repeated(repeated) => repeated,
Self::NotRepeated(not_repeated) => not_repeated,
@ -35,7 +37,7 @@ impl<T> Repeatable<T> {
}
/// Returns `Some` if the value is repeated, `None` otherwise.
pub(super) fn as_repeated(&self) -> Option<&T> {
pub fn as_repeated(&self) -> Option<&T> {
match self {
Self::Repeated(repeated) => Some(repeated),
Self::NotRepeated(_) => None,
@ -46,7 +48,7 @@ impl<T> Repeatable<T> {
impl<'a> GridLayouter<'a> {
/// Layouts the header's rows.
/// Skips regions as necessary.
pub(super) fn layout_header(
pub fn layout_header(
&mut self,
header: &Header,
engine: &mut Engine,
@ -90,7 +92,7 @@ impl<'a> GridLayouter<'a> {
}
/// Simulate the header's group of rows.
pub(super) fn simulate_header(
pub fn simulate_header(
&self,
header: &Header,
regions: &Regions<'_>,
@ -112,7 +114,7 @@ impl<'a> GridLayouter<'a> {
}
/// Updates `self.footer_height` by simulating the footer, and skips to fitting region.
pub(super) fn prepare_footer(
pub fn prepare_footer(
&mut self,
footer: &Footer,
engine: &mut Engine,
@ -146,7 +148,7 @@ impl<'a> GridLayouter<'a> {
/// Lays out all rows in the footer.
/// They are unbreakable.
pub(super) fn layout_footer(
pub fn layout_footer(
&mut self,
footer: &Footer,
engine: &mut Engine,
@ -167,7 +169,7 @@ impl<'a> GridLayouter<'a> {
}
// Simulate the footer's group of rows.
pub(super) fn simulate_footer(
pub fn simulate_footer(
&self,
footer: &Footer,
regions: &Regions<'_>,

View File

@ -1,88 +1,88 @@
use super::layout::{in_last_with_offset, points, Row, RowPiece};
use typst_library::diag::SourceResult;
use typst_library::engine::Engine;
use typst_library::foundations::Resolve;
use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing};
use typst_utils::MaybeReverseIter;
use super::layouter::{in_last_with_offset, points, Row, RowPiece};
use super::repeated::Repeatable;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::Resolve;
use crate::layout::{
Abs, Axes, Cell, Frame, GridLayouter, Point, Region, Regions, Size, Sizing,
};
use crate::utils::MaybeReverseIter;
use super::{Cell, GridLayouter};
/// All information needed to layout a single rowspan.
pub(super) struct Rowspan {
pub struct Rowspan {
/// First column of this rowspan.
pub(super) x: usize,
pub x: usize,
/// First row of this rowspan.
pub(super) y: usize,
pub y: usize,
/// The disambiguator for laying out the cells.
pub(super) disambiguator: usize,
pub disambiguator: usize,
/// Amount of rows spanned by the cell at (x, y).
pub(super) rowspan: usize,
pub rowspan: usize,
/// Whether all rows of the rowspan are part of an unbreakable row group.
/// This is true e.g. in headers and footers, regardless of what the user
/// specified for the parent cell's `breakable` field.
pub(super) is_effectively_unbreakable: bool,
pub is_effectively_unbreakable: bool,
/// The horizontal offset of this rowspan in all regions.
pub(super) dx: Abs,
pub dx: Abs,
/// The vertical offset of this rowspan in the first region.
pub(super) dy: Abs,
pub dy: Abs,
/// The index of the first region this rowspan appears in.
pub(super) first_region: usize,
pub first_region: usize,
/// The full height in the first region this rowspan appears in, for
/// relative sizing.
pub(super) region_full: Abs,
pub region_full: Abs,
/// The vertical space available for this rowspan in each region.
pub(super) heights: Vec<Abs>,
pub heights: Vec<Abs>,
/// The index of the largest resolved spanned row so far.
/// Once a spanned row is resolved and its height added to `heights`, this
/// number is increased. Older rows, even if repeated through e.g. a
/// header, will no longer contribute height to this rowspan.
///
/// This is `None` if no spanned rows were resolved in `finish_region` yet.
pub(super) max_resolved_row: Option<usize>,
pub max_resolved_row: Option<usize>,
}
/// The output of the simulation of an unbreakable row group.
#[derive(Default)]
pub(super) struct UnbreakableRowGroup {
pub struct UnbreakableRowGroup {
/// The rows in this group of unbreakable rows.
/// Includes their indices and their predicted heights.
pub(super) rows: Vec<(usize, Abs)>,
pub rows: Vec<(usize, Abs)>,
/// The total height of this row group.
pub(super) height: Abs,
pub height: Abs,
}
/// Data used to measure a cell in an auto row.
pub(super) struct CellMeasurementData<'layouter> {
pub struct CellMeasurementData<'layouter> {
/// The available width for the cell across all regions.
pub(super) width: Abs,
pub width: Abs,
/// The available height for the cell in its first region.
/// Infinite when the auto row is unbreakable.
pub(super) height: Abs,
pub height: Abs,
/// The backlog of heights available for the cell in later regions.
///
/// When this is `None`, the `custom_backlog` field should be used instead.
/// That's because, otherwise, this field would have to contain a reference
/// to the `custom_backlog` field, which isn't possible in Rust without
/// resorting to unsafe hacks.
pub(super) backlog: Option<&'layouter [Abs]>,
pub backlog: Option<&'layouter [Abs]>,
/// If the backlog needs to be built from scratch instead of reusing the
/// one at the current region, which is the case of a multi-region rowspan
/// (needs to join its backlog of already laid out heights with the current
/// backlog), then this vector will store the new backlog.
pub(super) custom_backlog: Vec<Abs>,
pub custom_backlog: Vec<Abs>,
/// The full height of the first region of the cell.
/// Infinite when the auto row is unbreakable.
pub(super) full: Abs,
pub full: Abs,
/// The height of the last repeated region to use in the measurement pod,
/// if any.
pub(super) last: Option<Abs>,
pub last: Option<Abs>,
/// The total height of previous rows spanned by the cell in the current
/// region (so far).
pub(super) height_in_this_region: Abs,
pub height_in_this_region: Abs,
/// The amount of previous regions spanned by the cell.
/// They are skipped for measurement purposes.
pub(super) frames_in_previous_regions: usize,
pub frames_in_previous_regions: usize,
}
impl<'a> GridLayouter<'a> {
@ -95,7 +95,7 @@ impl<'a> GridLayouter<'a> {
/// We need to do this only once we already know the heights of all
/// spanned rows, which is only possible after laying out the last row
/// spanned by the rowspan (or some row immediately after the last one).
pub(super) fn layout_rowspan(
pub fn layout_rowspan(
&mut self,
rowspan_data: Rowspan,
current_region_data: Option<(&mut Frame, &[RowPiece])>,
@ -184,7 +184,7 @@ impl<'a> GridLayouter<'a> {
/// Checks if a row contains the beginning of one or more rowspan cells.
/// If so, adds them to the rowspans vector.
pub(super) fn check_for_rowspans(&mut self, disambiguator: usize, y: usize) {
pub fn check_for_rowspans(&mut self, disambiguator: usize, y: usize) {
// We will compute the horizontal offset of each rowspan in advance.
// For that reason, we must reverse the column order when using RTL.
let offsets = points(self.rcols.iter().copied().rev_if(self.is_rtl));
@ -219,7 +219,7 @@ impl<'a> GridLayouter<'a> {
/// unbreakable row group, and, if so, advances regions until there is
/// enough space for them. This can be needed, for example, if there's an
/// unbreakable rowspan crossing those rows.
pub(super) fn check_for_unbreakable_rows(
pub fn check_for_unbreakable_rows(
&mut self,
current_row: usize,
engine: &mut Engine,
@ -292,7 +292,7 @@ impl<'a> GridLayouter<'a> {
///
/// This is used to figure out how much height the next unbreakable row
/// group (if any) needs.
pub(super) fn simulate_unbreakable_row_group(
pub fn simulate_unbreakable_row_group(
&self,
first_row: usize,
amount_unbreakable_rows: Option<usize>,
@ -359,7 +359,7 @@ impl<'a> GridLayouter<'a> {
/// Checks if one or more of the cells at the given row are unbreakable.
/// If so, returns the largest rowspan among the unbreakable cells;
/// the spanned rows must, as a result, be laid out in the same region.
pub(super) fn check_for_unbreakable_cells(&self, y: usize) -> usize {
pub fn check_for_unbreakable_cells(&self, y: usize) -> usize {
(0..self.grid.cols.len())
.filter_map(|x| self.grid.cell(x, y))
.filter(|cell| !cell.breakable)
@ -369,7 +369,7 @@ impl<'a> GridLayouter<'a> {
}
/// Used by `measure_auto_row` to gather data needed to measure the cell.
pub(super) fn prepare_auto_row_cell_measurement(
pub fn prepare_auto_row_cell_measurement(
&self,
parent: Axes<usize>,
cell: &Cell,
@ -577,7 +577,7 @@ impl<'a> GridLayouter<'a> {
/// expand the auto row based on the rowspan's demanded size, or `false`
/// otherwise.
#[allow(clippy::too_many_arguments)]
pub(super) fn prepare_rowspan_sizes(
pub fn prepare_rowspan_sizes(
&self,
auto_row_y: usize,
sizes: &mut Vec<Abs>,
@ -667,7 +667,7 @@ impl<'a> GridLayouter<'a> {
/// in each region and the pending rowspans' data (parent Y, rowspan amount
/// and vector of requested sizes).
#[allow(clippy::too_many_arguments)]
pub(super) fn simulate_and_measure_rowspans_in_auto_row(
pub fn simulate_and_measure_rowspans_in_auto_row(
&self,
y: usize,
resolved: &mut Vec<Abs>,

View File

@ -0,0 +1,142 @@
use std::ffi::OsStr;
use typst_library::diag::{bail, warning, At, SourceResult, StrResult};
use typst_library::engine::Engine;
use typst_library::foundations::{Packed, Smart, StyleChain};
use typst_library::introspection::Locator;
use typst_library::layout::{
Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size,
};
use typst_library::loading::Readable;
use typst_library::text::families;
use typst_library::visualize::{
Image, ImageElem, ImageFit, ImageFormat, Path, RasterFormat, VectorFormat,
};
/// Layout the image.
#[typst_macros::time(span = elem.span())]
pub fn layout_image(
elem: &Packed<ImageElem>,
engine: &mut Engine,
_: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let span = elem.span();
// Take the format that was explicitly defined, or parse the extension,
// or try to detect the format.
let data = elem.data();
let format = match elem.format(styles) {
Smart::Custom(v) => v,
Smart::Auto => determine_format(elem.path().as_str(), data).at(span)?,
};
// Warn the user if the image contains a foreign object. Not perfect
// because the svg could also be encoded, but that's an edge case.
if format == ImageFormat::Vector(VectorFormat::Svg) {
let has_foreign_object =
data.as_str().is_some_and(|s| s.contains("<foreignObject"));
if has_foreign_object {
engine.sink.warn(warning!(
span,
"image contains foreign object";
hint: "SVG images with foreign objects might render incorrectly in typst";
hint: "see https://github.com/typst/typst/issues/1421 for more information"
));
}
}
// Construct the image itself.
let image = Image::with_fonts(
data.clone().into(),
format,
elem.alt(styles),
engine.world,
&families(styles).collect::<Vec<_>>(),
)
.at(span)?;
// Determine the image's pixel aspect ratio.
let pxw = image.width();
let pxh = image.height();
let px_ratio = pxw / pxh;
// Determine the region's aspect ratio.
let region_ratio = region.size.x / region.size.y;
// Find out whether the image is wider or taller than the region.
let wide = px_ratio > region_ratio;
// The space into which the image will be placed according to its fit.
let target = if region.expand.x && region.expand.y {
// If both width and height are forced, take them.
region.size
} else if region.expand.x {
// If just width is forced, take it.
Size::new(region.size.x, region.size.y.min(region.size.x / px_ratio))
} else if region.expand.y {
// If just height is forced, take it.
Size::new(region.size.x.min(region.size.y * px_ratio), region.size.y)
} else {
// If neither is forced, take the natural image size at the image's
// DPI bounded by the available space.
let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI);
let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi));
Size::new(
natural.x.min(region.size.x).min(region.size.y * px_ratio),
natural.y.min(region.size.y).min(region.size.x / px_ratio),
)
};
// Compute the actual size of the fitted image.
let fit = elem.fit(styles);
let fitted = match fit {
ImageFit::Cover | ImageFit::Contain => {
if wide == (fit == ImageFit::Contain) {
Size::new(target.x, target.x / px_ratio)
} else {
Size::new(target.y * px_ratio, target.y)
}
}
ImageFit::Stretch => target,
};
// First, place the image in a frame of exactly its size and then resize
// the frame to the target size, center aligning the image in the
// process.
let mut frame = Frame::soft(fitted);
frame.push(Point::zero(), FrameItem::Image(image, fitted, span));
frame.resize(target, Axes::splat(FixedAlignment::Center));
// Create a clipping group if only part of the image should be visible.
if fit == ImageFit::Cover && !target.fits(fitted) {
frame.clip(Path::rect(frame.size()));
}
Ok(frame)
}
/// Determine the image format based on path and data.
fn determine_format(path: &str, data: &Readable) -> StrResult<ImageFormat> {
let ext = std::path::Path::new(path)
.extension()
.and_then(OsStr::to_str)
.unwrap_or_default()
.to_lowercase();
Ok(match ext.as_str() {
"png" => ImageFormat::Raster(RasterFormat::Png),
"jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg),
"gif" => ImageFormat::Raster(RasterFormat::Gif),
"svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
_ => match &data {
Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg),
Readable::Bytes(bytes) => match RasterFormat::detect(bytes) {
Some(f) => ImageFormat::Raster(f),
None => bail!("unknown image format"),
},
},
})
}

View File

@ -0,0 +1,87 @@
use once_cell::unsync::Lazy;
use typst_library::diag::SourceResult;
use typst_library::engine::Engine;
use typst_library::foundations::{Packed, StyleChain};
use typst_library::introspection::Locator;
use typst_library::layout::{BoxElem, Frame, FrameKind, Size};
use typst_library::visualize::Stroke;
use typst_utils::Numeric;
use crate::flow::unbreakable_pod;
use crate::shapes::{clip_rect, fill_and_stroke};
/// Lay out a box as part of a paragraph.
#[typst_macros::time(name = "box", span = elem.span())]
pub fn layout_box(
elem: &Packed<BoxElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Size,
) -> SourceResult<Frame> {
// Fetch sizing properties.
let width = elem.width(styles);
let height = elem.height(styles);
let inset = elem.inset(styles).unwrap_or_default();
// Build the pod region.
let pod = unbreakable_pod(&width, &height.into(), &inset, styles, region);
// Layout the body.
let mut frame = match elem.body(styles) {
// If we have no body, just create an empty frame. If necessary,
// its size will be adjusted below.
None => Frame::hard(Size::zero()),
// If we have a child, layout it into the body. Boxes are boundaries
// for gradient relativeness, so we set the `FrameKind` to `Hard`.
Some(body) => crate::layout_frame(engine, body, locator, styles, pod)?
.with_kind(FrameKind::Hard),
};
// Enforce a correct frame size on the expanded axes. Do this before
// applying the inset, since the pod shrunk.
frame.set_size(pod.expand.select(pod.size, frame.size()));
// Apply the inset.
if !inset.is_zero() {
crate::pad::grow(&mut frame, &inset);
}
// Prepare fill and stroke.
let fill = elem.fill(styles);
let stroke = elem
.stroke(styles)
.unwrap_or_default()
.map(|s| s.map(Stroke::unwrap_or_default));
// Only fetch these if necessary (for clipping or filling/stroking).
let outset = Lazy::new(|| elem.outset(styles).unwrap_or_default());
let radius = Lazy::new(|| elem.radius(styles).unwrap_or_default());
// Clip the contents, if requested.
if elem.clip(styles) {
let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis();
frame.clip(clip_rect(size, &radius, &stroke));
}
// Add fill and/or stroke.
if fill.is_some() || stroke.iter().any(Option::is_some) {
fill_and_stroke(&mut frame, fill, &stroke, &outset, &radius, elem.span());
}
// Assign label to the frame.
if let Some(label) = elem.label() {
frame.label(label);
}
// Apply baseline shift. Do this after setting the size and applying the
// inset, so that a relative shift is resolved relative to the final
// height.
let shift = elem.baseline(styles).relative_to(frame.height());
if !shift.is_zero() {
frame.set_baseline(frame.baseline() - shift);
}
Ok(frame)
}

View File

@ -1,17 +1,18 @@
use super::*;
use crate::diag::bail;
use crate::foundations::{Packed, Resolve};
use crate::introspection::{SplitLocator, Tag, TagElem};
use crate::layout::{
use typst_library::diag::bail;
use typst_library::foundations::{Packed, Resolve};
use typst_library::introspection::{SplitLocator, Tag, TagElem};
use typst_library::layout::{
Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing,
Spacing,
};
use crate::syntax::Span;
use crate::text::{
use typst_library::text::{
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
SpaceElem, TextElem,
};
use crate::utils::Numeric;
use typst_syntax::Span;
use typst_utils::Numeric;
use super::*;
// The characters by which spacing, inline content and pins are replaced in the
// paragraph's full text.
@ -222,7 +223,7 @@ pub fn collect<'a>(
if let Sizing::Fr(v) = elem.width(styles) {
collector.push_item(Item::Fractional(v, Some((elem, loc, styles))));
} else {
let frame = elem.layout(engine, loc, styles, region)?;
let frame = layout_box(elem, engine, loc, styles, region)?;
collector.push_item(Item::Frame(frame, styles));
}
} else if let Some(elem) = child.to_packed::<TagElem>() {

View File

@ -0,0 +1,213 @@
use kurbo::{BezPath, Line, ParamCurve};
use ttf_parser::{GlyphId, OutlineBuilder};
use typst_library::layout::{Abs, Em, Frame, FrameItem, Point, Size};
use typst_library::text::{
BottomEdge, DecoLine, Decoration, TextEdgeBounds, TextItem, TopEdge,
};
use typst_library::visualize::{FixedStroke, Geometry};
use typst_syntax::Span;
use crate::shapes::styled_rect;
/// Add line decorations to a single run of shaped text.
pub fn decorate(
frame: &mut Frame,
deco: &Decoration,
text: &TextItem,
width: Abs,
shift: Abs,
pos: Point,
) {
let font_metrics = text.font.metrics();
if let DecoLine::Highlight { fill, stroke, top_edge, bottom_edge, radius } =
&deco.line
{
let (top, bottom) = determine_edges(text, *top_edge, *bottom_edge);
let size = Size::new(width + 2.0 * deco.extent, top + bottom);
let rects = styled_rect(size, radius, fill.clone(), stroke);
let origin = Point::new(pos.x - deco.extent, pos.y - top - shift);
frame.prepend_multiple(
rects
.into_iter()
.map(|shape| (origin, FrameItem::Shape(shape, Span::detached()))),
);
return;
}
let (stroke, metrics, offset, evade, background) = match &deco.line {
DecoLine::Strikethrough { stroke, offset, background } => {
(stroke, font_metrics.strikethrough, offset, false, *background)
}
DecoLine::Overline { stroke, offset, evade, background } => {
(stroke, font_metrics.overline, offset, *evade, *background)
}
DecoLine::Underline { stroke, offset, evade, background } => {
(stroke, font_metrics.underline, offset, *evade, *background)
}
_ => return,
};
let offset = offset.unwrap_or(-metrics.position.at(text.size)) - shift;
let stroke = stroke.clone().unwrap_or(FixedStroke::from_pair(
text.fill.as_decoration(),
metrics.thickness.at(text.size),
));
let gap_padding = 0.08 * text.size;
let min_width = 0.162 * text.size;
let start = pos.x - deco.extent;
let end = pos.x + width + deco.extent;
let mut push_segment = |from: Abs, to: Abs, prepend: bool| {
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.clone());
if prepend {
frame.prepend(origin, FrameItem::Shape(shape, Span::detached()));
} else {
frame.push(origin, FrameItem::Shape(shape, Span::detached()));
}
}
};
if !evade {
push_segment(start, end, background);
return;
}
let line = Line::new(
kurbo::Point::new(pos.x.to_raw(), offset.to_raw()),
kurbo::Point::new((pos.x + width).to_raw(), offset.to_raw()),
);
let mut x = pos.x;
let mut intersections = vec![];
for glyph in text.glyphs.iter() {
let dx = glyph.x_offset.at(text.size) + x;
let mut builder =
BezPathBuilder::new(font_metrics.units_per_em, text.size, dx.to_raw());
let bbox = text.font.ttf().outline_glyph(GlyphId(glyph.id), &mut builder);
let path = builder.finish();
x += glyph.x_advance.at(text.size);
// Only do the costly segments intersection test if the line
// intersects the bounding box.
let intersect = bbox.is_some_and(|bbox| {
let y_min = -text.font.to_em(bbox.y_max).at(text.size);
let y_max = -text.font.to_em(bbox.y_min).at(text.size);
offset >= y_min && offset <= y_max
});
if intersect {
// Find all intersections of segments with the line.
intersections.extend(
path.segments()
.flat_map(|seg| seg.intersect_line(line))
.map(|is| Abs::raw(line.eval(is.line_t).x)),
);
}
}
// Add start and end points, taking padding into account.
intersections.push(start - gap_padding);
intersections.push(end + gap_padding);
// When emitting the decorative line segments, we move from left to
// right. The intersections are not necessarily in this order, yet.
intersections.sort();
for edge in intersections.windows(2) {
let l = edge[0];
let r = edge[1];
// If we are too close, don't draw the segment
if r - l < gap_padding {
continue;
} else {
push_segment(l + gap_padding, r - gap_padding, background);
}
}
}
// Return the top/bottom edge of the text given the metric of the font.
fn determine_edges(
text: &TextItem,
top_edge: TopEdge,
bottom_edge: BottomEdge,
) -> (Abs, Abs) {
let mut top = Abs::zero();
let mut bottom = Abs::zero();
for g in text.glyphs.iter() {
let (t, b) = text.font.edges(
top_edge,
bottom_edge,
text.size,
TextEdgeBounds::Glyph(g.id),
);
top.set_max(t);
bottom.set_max(b);
}
(top, bottom)
}
/// 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();
}
}

View File

@ -1,6 +1,7 @@
use typst_library::introspection::SplitLocator;
use typst_utils::Numeric;
use super::*;
use crate::introspection::SplitLocator;
use crate::utils::Numeric;
/// Turns the selected lines into frames.
#[typst_macros::time]

View File

@ -1,14 +1,15 @@
use std::fmt::{self, Debug, Formatter};
use std::ops::{Deref, DerefMut};
use typst_library::engine::Engine;
use typst_library::foundations::NativeElement;
use typst_library::introspection::{SplitLocator, Tag};
use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
use typst_library::model::{ParLine, ParLineMarker};
use typst_library::text::{Lang, TextElem};
use typst_utils::Numeric;
use super::*;
use crate::engine::Engine;
use crate::foundations::NativeElement;
use crate::introspection::{SplitLocator, Tag};
use crate::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
use crate::model::{ParLine, ParLineMarker};
use crate::text::{Lang, TextElem};
use crate::utils::Numeric;
const SHY: char = '\u{ad}';
const HYPHEN: char = '-';
@ -510,7 +511,7 @@ pub fn commit(
if let Some((elem, loc, styles)) = elem {
let region = Size::new(amount, full);
let mut frame =
elem.layout(engine, loc.relayout(), *styles, region)?;
layout_box(elem, engine, loc.relayout(), *styles, region)?;
frame.translate(Point::with_y(TextElem::baseline_in(*styles)));
push(&mut offset, frame.post_processed(*styles));
} else {
@ -590,7 +591,7 @@ fn add_par_line_marker(
// where line numbers can be displayed), so we just need it to be in a tag
// and to be valid (to have a location).
let mut marker = ParLineMarker::new(numbering, align, margin, clearance).pack();
let key = crate::utils::hash128(&marker);
let key = typst_utils::hash128(&marker);
let loc = locator.next_location(engine.introspector, key);
marker.set_location(loc);

View File

@ -8,14 +8,14 @@ use icu_provider_adapters::fork::ForkByKeyProvider;
use icu_provider_blob::BlobDataProvider;
use icu_segmenter::LineSegmenter;
use once_cell::sync::Lazy;
use typst_library::engine::Engine;
use typst_library::layout::{Abs, Em};
use typst_library::model::Linebreaks;
use typst_library::text::{is_default_ignorable, Lang, TextElem};
use typst_syntax::link_prefix;
use unicode_segmentation::UnicodeSegmentation;
use super::*;
use crate::engine::Engine;
use crate::layout::{Abs, Em};
use crate::model::Linebreaks;
use crate::syntax::link_prefix;
use crate::text::{is_default_ignorable, Lang, TextElem};
/// The cost of a line or paragraph layout.
type Cost = f64;

View File

@ -1,13 +1,27 @@
#[path = "box.rs"]
mod box_;
mod collect;
mod deco;
mod finalize;
mod line;
mod linebreak;
mod prepare;
mod shaping;
pub use self::box_::layout_box;
use comemo::{Track, Tracked, TrackedMut};
use typst_library::diag::SourceResult;
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{StyleChain, StyleVec};
use typst_library::introspection::{Introspector, Locator, LocatorLink};
use typst_library::layout::{Fragment, Size};
use typst_library::model::ParElem;
use typst_library::routines::Routines;
use typst_library::World;
use self::collect::{collect, Item, Segment, SpanMapper};
use self::deco::decorate;
use self::finalize::finalize;
use self::line::{commit, line, Line};
use self::linebreak::{linebreak, Breakpoint};
@ -16,19 +30,12 @@ use self::shaping::{
cjk_punct_style, is_of_cj_script, shape_range, ShapedGlyph, ShapedText,
BEGIN_PUNCT_PAT, END_PUNCT_PAT,
};
use crate::diag::SourceResult;
use crate::engine::{Engine, Route, Sink, Traced};
use crate::foundations::{StyleChain, StyleVec};
use crate::introspection::{Introspector, Locator, LocatorLink};
use crate::layout::{Fragment, Size};
use crate::model::ParElem;
use crate::World;
/// Range of a substring of text.
type Range = std::ops::Range<usize>;
/// Layouts content inline.
pub(crate) fn layout_inline(
pub fn layout_inline(
engine: &mut Engine,
children: &StyleVec,
locator: Locator,
@ -39,6 +46,7 @@ pub(crate) fn layout_inline(
) -> SourceResult<Fragment> {
layout_inline_impl(
children,
engine.routines,
engine.world,
engine.introspector,
engine.traced,
@ -57,6 +65,7 @@ pub(crate) fn layout_inline(
#[allow(clippy::too_many_arguments)]
fn layout_inline_impl(
children: &StyleVec,
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
traced: Tracked<Traced>,
@ -71,6 +80,7 @@ fn layout_inline_impl(
let link = LocatorLink::new(locator);
let locator = Locator::link(&link);
let mut engine = Engine {
routines,
world,
introspector,
traced,

View File

@ -1,10 +1,10 @@
use typst_library::foundations::{Resolve, Smart};
use typst_library::layout::{Abs, AlignElem, Dir, Em, FixedAlignment};
use typst_library::model::Linebreaks;
use typst_library::text::{Costs, Lang, TextElem};
use unicode_bidi::{BidiInfo, Level as BidiLevel};
use super::*;
use crate::foundations::{Resolve, Smart};
use crate::layout::{Abs, AlignElem, Dir, Em, FixedAlignment};
use crate::model::Linebreaks;
use crate::text::{Costs, Lang, TextElem};
/// A paragraph representation in which children are already layouted and text
/// is already preshaped.

View File

@ -7,19 +7,19 @@ use az::SaturatingAs;
use ecow::EcoString;
use rustybuzz::{BufferFlags, ShapePlan, UnicodeBuffer};
use ttf_parser::Tag;
use typst_library::engine::Engine;
use typst_library::foundations::{Smart, StyleChain};
use typst_library::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size};
use typst_library::text::{
families, features, is_default_ignorable, variant, Font, FontVariant, Glyph, Lang,
Region, TextEdgeBounds, TextElem, TextItem,
};
use typst_library::World;
use typst_utils::SliceExt;
use unicode_bidi::{BidiInfo, Level as BidiLevel};
use unicode_script::{Script, UnicodeScript};
use super::{Item, Range, SpanMapper};
use crate::engine::Engine;
use crate::foundations::{Smart, StyleChain};
use crate::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size};
use crate::text::{
decorate, families, features, is_default_ignorable, variant, Font, FontVariant,
Glyph, Lang, Region, TextEdgeBounds, TextElem, TextItem,
};
use crate::utils::SliceExt;
use crate::World;
use super::{decorate, Item, Range, SpanMapper};
/// The result of shaping text.
///

View File

@ -0,0 +1,30 @@
//! Typst's layout engine.
mod flow;
mod grid;
mod image;
mod inline;
mod lists;
mod math;
mod pad;
mod pages;
mod repeat;
mod shapes;
mod stack;
mod transforms;
pub use self::flow::{layout_columns, layout_fragment, layout_frame};
pub use self::grid::{layout_grid, layout_table};
pub use self::image::layout_image;
pub use self::inline::{layout_box, layout_inline};
pub use self::lists::{layout_enum, layout_list};
pub use self::math::{layout_equation_block, layout_equation_inline};
pub use self::pad::layout_pad;
pub use self::pages::layout_document;
pub use self::repeat::layout_repeat;
pub use self::shapes::{
layout_circle, layout_ellipse, layout_line, layout_path, layout_polygon, layout_rect,
layout_square,
};
pub use self::stack::layout_stack;
pub use self::transforms::{layout_move, layout_rotate, layout_scale, layout_skew};

View File

@ -0,0 +1,146 @@
use comemo::Track;
use smallvec::smallvec;
use typst_library::diag::SourceResult;
use typst_library::engine::Engine;
use typst_library::foundations::{Content, Context, Depth, Packed, StyleChain};
use typst_library::introspection::Locator;
use typst_library::layout::{Axes, Fragment, HAlignment, Regions, Sizing, VAlignment};
use typst_library::model::{EnumElem, ListElem, Numbering, ParElem};
use typst_library::text::TextElem;
use crate::grid::{Cell, CellGrid, GridLayouter};
/// Layout the list.
#[typst_macros::time(span = elem.span())]
pub fn layout_list(
elem: &Packed<ListElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let indent = elem.indent(styles);
let body_indent = elem.body_indent(styles);
let gutter = elem.spacing(styles).unwrap_or_else(|| {
if elem.tight(styles) {
ParElem::leading_in(styles).into()
} else {
ParElem::spacing_in(styles).into()
}
});
let Depth(depth) = ListElem::depth_in(styles);
let marker = elem
.marker(styles)
.resolve(engine, styles, depth)?
// avoid '#set align' interference with the list
.aligned(HAlignment::Start + VAlignment::Top);
let mut cells = vec![];
let mut locator = locator.split();
for item in elem.children() {
cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new(marker.clone(), locator.next(&marker.span())));
cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new(
item.body.clone().styled(ListElem::set_depth(Depth(1))),
locator.next(&item.body.span()),
));
}
let grid = CellGrid::new(
Axes::with_x(&[
Sizing::Rel(indent.into()),
Sizing::Auto,
Sizing::Rel(body_indent.into()),
Sizing::Auto,
]),
Axes::with_y(&[gutter.into()]),
cells,
);
let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
layouter.layout(engine)
}
/// Layout the enumeration.
#[typst_macros::time(span = elem.span())]
pub fn layout_enum(
elem: &Packed<EnumElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let numbering = elem.numbering(styles);
let indent = elem.indent(styles);
let body_indent = elem.body_indent(styles);
let gutter = elem.spacing(styles).unwrap_or_else(|| {
if elem.tight(styles) {
ParElem::leading_in(styles).into()
} else {
ParElem::spacing_in(styles).into()
}
});
let mut cells = vec![];
let mut locator = locator.split();
let mut number = elem.start(styles);
let mut parents = EnumElem::parents_in(styles);
let full = elem.full(styles);
// Horizontally align based on the given respective parameter.
// Vertically align to the top to avoid inheriting `horizon` or `bottom`
// alignment from the context and having the number be displaced in
// relation to the item it refers to.
let number_align = elem.number_align(styles);
for item in elem.children() {
number = item.number(styles).unwrap_or(number);
let context = Context::new(None, Some(styles));
let resolved = if full {
parents.push(number);
let content = numbering.apply(engine, context.track(), &parents)?.display();
parents.pop();
content
} else {
match numbering {
Numbering::Pattern(pattern) => {
TextElem::packed(pattern.apply_kth(parents.len(), number))
}
other => other.apply(engine, context.track(), &[number])?.display(),
}
};
// Disable overhang as a workaround to end-aligned dots glitching
// and decreasing spacing between numbers and items.
let resolved =
resolved.aligned(number_align).styled(TextElem::set_overhang(false));
cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new(resolved, locator.next(&())));
cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new(
item.body.clone().styled(EnumElem::set_parents(smallvec![number])),
locator.next(&item.body.span()),
));
number = number.saturating_add(1);
}
let grid = CellGrid::new(
Axes::with_x(&[
Sizing::Rel(indent.into()),
Sizing::Auto,
Sizing::Rel(body_indent.into()),
Sizing::Auto,
]),
Axes::with_y(&[gutter.into()]),
cells,
);
let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
layouter.layout(engine)
}

View File

@ -0,0 +1,75 @@
use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain};
use typst_library::layout::{Em, Frame, Point, Rel, Size};
use typst_library::math::{Accent, AccentElem};
use super::{
scaled_font_size, style_cramped, FrameFragment, GlyphFragment, MathContext,
MathFragment,
};
/// How much the accent can be shorter than the base.
const ACCENT_SHORT_FALL: Em = Em::new(0.5);
/// Lays out an [`AccentElem`].
#[typst_macros::time(name = "math.accent", span = elem.span())]
pub fn layout_accent(
elem: &Packed<AccentElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let cramped = style_cramped();
let base = ctx.layout_into_fragment(elem.base(), styles.chain(&cramped))?;
// Preserve class to preserve automatic spacing.
let base_class = base.class();
let base_attach = base.accent_attach();
let width = elem
.size(styles)
.unwrap_or(Rel::one())
.at(scaled_font_size(ctx, styles))
.relative_to(base.width());
// Forcing the accent to be at least as large as the base makes it too
// wide in many case.
let Accent(c) = elem.accent();
let glyph = GlyphFragment::new(ctx, styles, *c, elem.span());
let short_fall = ACCENT_SHORT_FALL.at(glyph.font_size);
let variant = glyph.stretch_horizontal(ctx, width, short_fall);
let accent = variant.frame;
let accent_attach = variant.accent_attach;
// Descent is negative because the accent's ink bottom is above the
// baseline. Therefore, the default gap is the accent's negated descent
// minus the accent base height. Only if the base is very small, we need
// a larger gap so that the accent doesn't move too low.
let accent_base_height = scaled!(ctx, styles, accent_base_height);
let gap = -accent.descent() - base.height().min(accent_base_height);
let size = Size::new(base.width(), accent.height() + gap + base.height());
let accent_pos = Point::with_x(base_attach - accent_attach);
let base_pos = Point::with_y(accent.height() + gap);
let baseline = base_pos.y + base.ascent();
let base_italics_correction = base.italics_correction();
let base_text_like = base.is_text_like();
let base_ascent = match &base {
MathFragment::Frame(frame) => frame.base_ascent,
_ => base.ascent(),
};
let mut frame = Frame::soft(size);
frame.set_baseline(baseline);
frame.push_frame(accent_pos, accent);
frame.push_frame(base_pos, base.into_frame());
ctx.push(
FrameFragment::new(ctx, styles, frame)
.with_class(base_class)
.with_base_ascent(base_ascent)
.with_italics_correction(base_italics_correction)
.with_accent_attach(base_attach)
.with_text_like(base_text_like),
);
Ok(())
}

View File

@ -1,14 +1,16 @@
use unicode_math_class::MathClass;
use crate::diag::SourceResult;
use crate::foundations::{elem, Content, Packed, Smart, StyleChain};
use crate::layout::{Abs, Axis, Corner, Frame, Length, Point, Rel, Size};
use crate::math::{
stretch_fragment, style_for_subscript, style_for_superscript, EquationElem,
FrameFragment, LayoutMath, MathContext, MathFragment, MathSize, Scaled, StretchElem,
use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, Smart, StyleChain};
use typst_library::layout::{Abs, Axis, Corner, Frame, Length, Point, Rel, Size};
use typst_library::math::{
AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem,
};
use typst_library::text::TextElem;
use typst_utils::OptionExt;
use super::{
stretch_fragment, style_for_subscript, style_for_superscript, FrameFragment, Limits,
MathContext, MathFragment,
};
use crate::text::TextElem;
use crate::utils::OptionExt;
macro_rules! measure {
($e: ident, $attr: ident) => {
@ -16,300 +18,140 @@ macro_rules! measure {
};
}
/// A base with optional attachments.
///
/// ```example
/// $ attach(
/// Pi, t: alpha, b: beta,
/// tl: 1, tr: 2+3, bl: 4+5, br: 6,
/// ) $
/// ```
#[elem(LayoutMath)]
pub struct AttachElem {
/// The base to which things are attached.
#[required]
pub base: Content,
/// Lays out an [`AttachElem`].
#[typst_macros::time(name = "math.attach", span = elem.span())]
pub fn layout_attach(
elem: &Packed<AttachElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let merged = elem.merge_base();
let elem = merged.as_ref().unwrap_or(elem);
let stretch = stretch_size(styles, elem);
/// The top attachment, smartly positioned at top-right or above the base.
///
/// You can wrap the base in `{limits()}` or `{scripts()}` to override the
/// smart positioning.
pub t: Option<Content>,
let mut base = ctx.layout_into_fragment(elem.base(), styles)?;
let sup_style = style_for_superscript(styles);
let sup_style_chain = styles.chain(&sup_style);
let tl = elem.tl(sup_style_chain);
let tr = elem.tr(sup_style_chain);
let primed = tr.as_ref().is_some_and(|content| content.is::<PrimesElem>());
let t = elem.t(sup_style_chain);
/// The bottom attachment, smartly positioned at the bottom-right or below
/// the base.
///
/// You can wrap the base in `{limits()}` or `{scripts()}` to override the
/// smart positioning.
pub b: Option<Content>,
let sub_style = style_for_subscript(styles);
let sub_style_chain = styles.chain(&sub_style);
let bl = elem.bl(sub_style_chain);
let br = elem.br(sub_style_chain);
let b = elem.b(sub_style_chain);
/// The top-left attachment (before the base).
pub tl: Option<Content>,
let limits = base.limits().active(styles);
let (t, tr) = match (t, tr) {
(Some(t), Some(tr)) if primed && !limits => (None, Some(tr + t)),
(Some(t), None) if !limits => (None, Some(t)),
(t, tr) => (t, tr),
};
let (b, br) = if limits || br.is_some() { (b, br) } else { (None, b) };
/// The bottom-left attachment (before base).
pub bl: Option<Content>,
/// The top-right attachment (after the base).
pub tr: Option<Content>,
/// The bottom-right attachment (after the base).
pub br: Option<Content>,
}
impl LayoutMath for Packed<AttachElem> {
#[typst_macros::time(name = "math.attach", span = self.span())]
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
let new_elem = merge_base(self);
let elem = new_elem.as_ref().unwrap_or(self);
let stretch = stretch_size(styles, elem);
let mut base = ctx.layout_into_fragment(elem.base(), styles)?;
let sup_style = style_for_superscript(styles);
let sup_style_chain = styles.chain(&sup_style);
let tl = elem.tl(sup_style_chain);
let tr = elem.tr(sup_style_chain);
let primed = tr.as_ref().is_some_and(|content| content.is::<PrimesElem>());
let t = elem.t(sup_style_chain);
let sub_style = style_for_subscript(styles);
let sub_style_chain = styles.chain(&sub_style);
let bl = elem.bl(sub_style_chain);
let br = elem.br(sub_style_chain);
let b = elem.b(sub_style_chain);
let limits = base.limits().active(styles);
let (t, tr) = match (t, tr) {
(Some(t), Some(tr)) if primed && !limits => (None, Some(tr + t)),
(Some(t), None) if !limits => (None, Some(t)),
(t, tr) => (t, tr),
macro_rules! layout {
($content:ident, $style_chain:ident) => {
$content
.map(|elem| ctx.layout_into_fragment(&elem, $style_chain))
.transpose()
};
let (b, br) = if limits || br.is_some() { (b, br) } else { (None, b) };
}
macro_rules! layout {
($content:ident, $style_chain:ident) => {
$content
.map(|elem| ctx.layout_into_fragment(&elem, $style_chain))
.transpose()
// Layout the top and bottom attachments early so we can measure their
// widths, in order to calculate what the stretch size is relative to.
let t = layout!(t, sup_style_chain)?;
let b = layout!(b, sub_style_chain)?;
if let Some(stretch) = stretch {
let relative_to_width = measure!(t, width).max(measure!(b, width));
stretch_fragment(
ctx,
styles,
&mut base,
Some(Axis::X),
Some(relative_to_width),
stretch,
Abs::zero(),
);
}
let fragments = [
layout!(tl, sup_style_chain)?,
t,
layout!(tr, sup_style_chain)?,
layout!(bl, sub_style_chain)?,
b,
layout!(br, sub_style_chain)?,
];
layout_attachments(ctx, styles, base, fragments)
}
/// Lays out a [`PrimeElem`].
#[typst_macros::time(name = "math.primes", span = elem.span())]
pub fn layout_primes(
elem: &Packed<PrimesElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
match *elem.count() {
count @ 1..=4 => {
let c = match count {
1 => '',
2 => '″',
3 => '‴',
4 => '⁗',
_ => unreachable!(),
};
let f = ctx.layout_into_fragment(&TextElem::packed(c), styles)?;
ctx.push(f);
}
count => {
// Custom amount of primes
let prime =
ctx.layout_into_fragment(&TextElem::packed(''), styles)?.into_frame();
let width = prime.width() * (count + 1) as f64 / 2.0;
let mut frame = Frame::soft(Size::new(width, prime.height()));
frame.set_baseline(prime.ascent());
// Layout the top and bottom attachments early so we can measure their
// widths, in order to calculate what the stretch size is relative to.
let t = layout!(t, sup_style_chain)?;
let b = layout!(b, sub_style_chain)?;
if let Some(stretch) = stretch {
let relative_to_width = measure!(t, width).max(measure!(b, width));
stretch_fragment(
ctx,
styles,
&mut base,
Some(Axis::X),
Some(relative_to_width),
stretch,
Abs::zero(),
);
}
let fragments = [
layout!(tl, sup_style_chain)?,
t,
layout!(tr, sup_style_chain)?,
layout!(bl, sub_style_chain)?,
b,
layout!(br, sub_style_chain)?,
];
layout_attachments(ctx, styles, base, fragments)
}
}
/// Grouped primes.
///
/// ```example
/// $ a'''_b = a^'''_b $
/// ```
///
/// # Syntax
/// This function has dedicated syntax: use apostrophes instead of primes. They
/// will automatically attach to the previous element, moving superscripts to
/// the next level.
#[elem(LayoutMath)]
pub struct PrimesElem {
/// The number of grouped primes.
#[required]
pub count: usize,
}
impl LayoutMath for Packed<PrimesElem> {
#[typst_macros::time(name = "math.primes", span = self.span())]
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
match *self.count() {
count @ 1..=4 => {
let c = match count {
1 => '',
2 => '″',
3 => '‴',
4 => '⁗',
_ => unreachable!(),
};
let f = ctx.layout_into_fragment(&TextElem::packed(c), styles)?;
ctx.push(f);
for i in 0..count {
frame.push_frame(
Point::new(prime.width() * (i as f64 / 2.0), Abs::zero()),
prime.clone(),
)
}
count => {
// Custom amount of primes
let prime = ctx
.layout_into_fragment(&TextElem::packed(''), styles)?
.into_frame();
let width = prime.width() * (count + 1) as f64 / 2.0;
let mut frame = Frame::soft(Size::new(width, prime.height()));
frame.set_baseline(prime.ascent());
for i in 0..count {
frame.push_frame(
Point::new(prime.width() * (i as f64 / 2.0), Abs::zero()),
prime.clone(),
)
}
ctx.push(FrameFragment::new(ctx, styles, frame).with_text_like(true));
}
}
Ok(())
}
}
/// Forces a base to display attachments as scripts.
///
/// ```example
/// $ scripts(sum)_1^2 != sum_1^2 $
/// ```
#[elem(LayoutMath)]
pub struct ScriptsElem {
/// The base to attach the scripts to.
#[required]
pub body: Content,
}
impl LayoutMath for Packed<ScriptsElem> {
#[typst_macros::time(name = "math.scripts", span = self.span())]
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
let mut fragment = ctx.layout_into_fragment(self.body(), styles)?;
fragment.set_limits(Limits::Never);
ctx.push(fragment);
Ok(())
}
}
/// Forces a base to display attachments as limits.
///
/// ```example
/// $ limits(A)_1^2 != A_1^2 $
/// ```
#[elem(LayoutMath)]
pub struct LimitsElem {
/// The base to attach the limits to.
#[required]
pub body: Content,
/// Whether to also force limits in inline equations.
///
/// When applying limits globally (e.g., through a show rule), it is
/// typically a good idea to disable this.
#[default(true)]
pub inline: bool,
}
impl LayoutMath for Packed<LimitsElem> {
#[typst_macros::time(name = "math.limits", span = self.span())]
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
let limits = if self.inline(styles) { Limits::Always } else { Limits::Display };
let mut fragment = ctx.layout_into_fragment(self.body(), styles)?;
fragment.set_limits(limits);
ctx.push(fragment);
Ok(())
}
}
/// Describes in which situation a frame should use limits for attachments.
#[derive(Debug, Copy, Clone)]
pub enum Limits {
/// Always scripts.
Never,
/// Display limits only in `display` math.
Display,
/// Always limits.
Always,
}
impl Limits {
/// The default limit configuration if the given character is the base.
pub fn for_char(c: char) -> Self {
match unicode_math_class::class(c) {
Some(MathClass::Large) => {
if is_integral_char(c) {
Limits::Never
} else {
Limits::Display
}
}
Some(MathClass::Relation) => Limits::Always,
_ => Limits::Never,
}
}
/// The default limit configuration for a math class.
pub fn for_class(class: MathClass) -> Self {
match class {
MathClass::Large => Self::Display,
MathClass::Relation => Self::Always,
_ => Self::Never,
}
}
/// Whether limits should be displayed in this context.
pub fn active(&self, styles: StyleChain) -> bool {
match self {
Self::Always => true,
Self::Display => EquationElem::size_in(styles) == MathSize::Display,
Self::Never => false,
ctx.push(FrameFragment::new(ctx, styles, frame).with_text_like(true));
}
}
Ok(())
}
/// If an AttachElem's base is also an AttachElem, merge attachments into the
/// base AttachElem where possible.
fn merge_base(elem: &Packed<AttachElem>) -> Option<Packed<AttachElem>> {
// Extract from an EquationElem.
let mut base = elem.base();
if let Some(equation) = base.to_packed::<EquationElem>() {
base = equation.body();
}
/// Lays out a [`ScriptsElem`].
#[typst_macros::time(name = "math.scripts", span = elem.span())]
pub fn layout_scripts(
elem: &Packed<ScriptsElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?;
fragment.set_limits(Limits::Never);
ctx.push(fragment);
Ok(())
}
// Move attachments from elem into base where possible.
if let Some(base) = base.to_packed::<AttachElem>() {
let mut elem = elem.clone();
let mut base = base.clone();
macro_rules! merge {
($content:ident) => {
if base.$content.is_none() && elem.$content.is_some() {
base.$content = elem.$content.clone();
elem.$content = None;
}
};
}
merge!(t);
merge!(b);
merge!(tl);
merge!(tr);
merge!(bl);
merge!(br);
elem.base = base.pack();
return Some(elem);
}
None
/// Lays out a [`LimitsElem`].
#[typst_macros::time(name = "math.limits", span = elem.span())]
pub fn layout_limits(
elem: &Packed<LimitsElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let limits = if elem.inline(styles) { Limits::Always } else { Limits::Display };
let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?;
fragment.set_limits(limits);
ctx.push(fragment);
Ok(())
}
/// Get the size to stretch the base to, if the attach argument is true.
@ -326,7 +168,7 @@ fn stretch_size(
base.to_packed::<StretchElem>().map(|stretch| stretch.size(styles))
}
/// Layout the attachments.
/// Lay out the attachments.
fn layout_attachments(
ctx: &mut MathContext,
styles: StyleChain,
@ -671,8 +513,3 @@ fn math_kern(
// result in glyphs colliding.
summed_kern(corr_height_top).max(summed_kern(corr_height_bot))
}
/// Determines if the character is one of a variety of integral signs.
fn is_integral_char(c: char) -> bool {
('∫'..='∳').contains(&c) || ('⨋'..='⨜').contains(&c)
}

View File

@ -0,0 +1,144 @@
use comemo::Track;
use typst_library::diag::{At, SourceResult};
use typst_library::foundations::{Context, Packed, Smart, StyleChain};
use typst_library::layout::{Abs, Angle, Frame, FrameItem, Point, Rel, Size, Transform};
use typst_library::math::{CancelAngle, CancelElem};
use typst_library::text::TextElem;
use typst_library::visualize::{FixedStroke, Geometry};
use typst_syntax::Span;
use super::{scaled_font_size, FrameFragment, MathContext};
/// Lays out a [`CancelElem`].
#[typst_macros::time(name = "math.cancel", span = elem.span())]
pub fn layout_cancel(
elem: &Packed<CancelElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let body = ctx.layout_into_fragment(elem.body(), styles)?;
// Preserve properties of body.
let body_class = body.class();
let body_italics = body.italics_correction();
let body_attach = body.accent_attach();
let body_text_like = body.is_text_like();
let mut body = body.into_frame();
let body_size = body.size();
let span = elem.span();
let length = elem.length(styles).at(scaled_font_size(ctx, styles));
let stroke = elem.stroke(styles).unwrap_or(FixedStroke {
paint: TextElem::fill_in(styles).as_decoration(),
..Default::default()
});
let invert = elem.inverted(styles);
let cross = elem.cross(styles);
let angle = elem.angle(styles);
let invert_first_line = !cross && invert;
let first_line = draw_cancel_line(
ctx,
length,
stroke.clone(),
invert_first_line,
&angle,
body_size,
styles,
span,
)?;
// The origin of our line is the very middle of the element.
let center = body_size.to_point() / 2.0;
body.push_frame(center, first_line);
if cross {
// Draw the second line.
let second_line =
draw_cancel_line(ctx, length, stroke, true, &angle, body_size, styles, span)?;
body.push_frame(center, second_line);
}
ctx.push(
FrameFragment::new(ctx, styles, body)
.with_class(body_class)
.with_italics_correction(body_italics)
.with_accent_attach(body_attach)
.with_text_like(body_text_like),
);
Ok(())
}
/// Draws a cancel line.
#[allow(clippy::too_many_arguments)]
fn draw_cancel_line(
ctx: &mut MathContext,
length_scale: Rel<Abs>,
stroke: FixedStroke,
invert: bool,
angle: &Smart<CancelAngle>,
body_size: Size,
styles: StyleChain,
span: Span,
) -> SourceResult<Frame> {
let default = default_angle(body_size);
let mut angle = match angle {
// Non specified angle defaults to the diagonal
Smart::Auto => default,
Smart::Custom(angle) => match angle {
// This specifies the absolute angle w.r.t y-axis clockwise.
CancelAngle::Angle(v) => *v,
// This specifies a function that takes the default angle as input.
CancelAngle::Func(func) => func
.call(ctx.engine, Context::new(None, Some(styles)).track(), [default])?
.cast()
.at(span)?,
},
};
// invert means flipping along the y-axis
if invert {
angle *= -1.0;
}
// same as above, the default length is the diagonal of the body box.
let default_length = body_size.to_point().hypot();
let length = length_scale.relative_to(default_length);
// Draw a vertical line of length and rotate it by angle
let start = Point::new(Abs::zero(), length / 2.0);
let delta = Point::new(Abs::zero(), -length);
let mut frame = Frame::soft(body_size);
frame.push(start, FrameItem::Shape(Geometry::Line(delta).stroked(stroke), span));
// Having the middle of the line at the origin is convenient here.
frame.transform(Transform::rotate(angle));
Ok(frame)
}
/// The default line angle for a body of the given size.
fn default_angle(body: Size) -> Angle {
// The default cancel line is the diagonal.
// We infer the default angle from
// the diagonal w.r.t to the body box.
//
// The returned angle is in the range of [0, Pi/2]
//
// Note that the angle is computed w.r.t to the y-axis
//
// B
// /|
// diagonal / | height
// / |
// / |
// O ----
// width
let (width, height) = (body.x, body.y);
let default_angle = (width / height).atan(); // arctangent (in the range [0, Pi/2])
Angle::rad(default_angle)
}

View File

@ -1,90 +1,47 @@
use crate::diag::{bail, SourceResult};
use crate::foundations::{elem, Content, Packed, StyleChain, Value};
use crate::layout::{Em, Frame, FrameItem, Point, Size};
use crate::math::{
use typst_library::diag::SourceResult;
use typst_library::foundations::{Content, Packed, StyleChain};
use typst_library::layout::{Em, Frame, FrameItem, Point, Size};
use typst_library::math::{BinomElem, FracElem};
use typst_library::text::TextElem;
use typst_library::visualize::{FixedStroke, Geometry};
use typst_syntax::Span;
use super::{
scaled_font_size, style_for_denominator, style_for_numerator, FrameFragment,
GlyphFragment, LayoutMath, MathContext, Scaled, DELIM_SHORT_FALL,
GlyphFragment, MathContext, DELIM_SHORT_FALL,
};
use crate::syntax::{Span, Spanned};
use crate::text::TextElem;
use crate::visualize::{FixedStroke, Geometry};
const FRAC_AROUND: Em = Em::new(0.1);
/// A mathematical fraction.
///
/// # Example
/// ```example
/// $ 1/2 < (x+1)/2 $
/// $ ((x+1)) / 2 = frac(a, b) $
/// ```
///
/// # Syntax
/// This function also has dedicated syntax: Use a slash to turn neighbouring
/// expressions into a fraction. Multiple atoms can be grouped into a single
/// expression using round grouping parenthesis. Such parentheses are removed
/// from the output, but you can nest multiple to force them.
#[elem(title = "Fraction", LayoutMath)]
pub struct FracElem {
/// The fraction's numerator.
#[required]
pub num: Content,
/// The fraction's denominator.
#[required]
pub denom: Content,
/// Lays out a [`FracElem`].
#[typst_macros::time(name = "math.frac", span = elem.span())]
pub fn layout_frac(
elem: &Packed<FracElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
layout_frac_like(
ctx,
styles,
elem.num(),
std::slice::from_ref(elem.denom()),
false,
elem.span(),
)
}
impl LayoutMath for Packed<FracElem> {
#[typst_macros::time(name = "math.frac", span = self.span())]
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
layout(
ctx,
styles,
self.num(),
std::slice::from_ref(self.denom()),
false,
self.span(),
)
}
}
/// A binomial expression.
///
/// # Example
/// ```example
/// $ binom(n, k) $
/// $ binom(n, k_1, k_2, k_3, ..., k_m) $
/// ```
#[elem(title = "Binomial", LayoutMath)]
pub struct BinomElem {
/// The binomial's upper index.
#[required]
pub upper: Content,
/// The binomial's lower index.
#[required]
#[variadic]
#[parse(
let values = args.all::<Spanned<Value>>()?;
if values.is_empty() {
// Prevents one element binomials
bail!(args.span, "missing argument: lower");
}
values.into_iter().map(|spanned| spanned.v.display()).collect()
)]
pub lower: Vec<Content>,
}
impl LayoutMath for Packed<BinomElem> {
#[typst_macros::time(name = "math.binom", span = self.span())]
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
layout(ctx, styles, self.upper(), self.lower(), true, self.span())
}
/// Lays out a [`BinomElem`].
#[typst_macros::time(name = "math.binom", span = elem.span())]
pub fn layout_binom(
elem: &Packed<BinomElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
layout_frac_like(ctx, styles, elem.upper(), elem.lower(), true, elem.span())
}
/// Layout a fraction or binomial.
fn layout(
fn layout_frac_like(
ctx: &mut MathContext,
styles: StyleChain,
num: &Content,

View File

@ -1,22 +1,25 @@
use std::fmt::{self, Debug, Formatter};
use rustybuzz::Feature;
use smallvec::SmallVec;
use ttf_parser::gsub::AlternateSet;
use ttf_parser::gsub::{
AlternateSet, AlternateSubstitution, SingleSubstitution, SubstitutionSubtable,
};
use ttf_parser::opentype_layout::LayoutTable;
use ttf_parser::{GlyphId, Rect};
use typst_library::foundations::StyleChain;
use typst_library::introspection::Tag;
use typst_library::layout::{
Abs, Axis, Corner, Em, Frame, FrameItem, HideElem, Point, Size, VAlignment,
};
use typst_library::math::{EquationElem, MathSize};
use typst_library::model::{Destination, LinkElem};
use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem};
use typst_library::visualize::Paint;
use typst_syntax::Span;
use unicode_math_class::MathClass;
use crate::foundations::StyleChain;
use crate::introspection::Tag;
use crate::layout::{
Abs, Corner, Em, Frame, FrameItem, HideElem, Point, Size, VAlignment,
};
use crate::math::{
scaled_font_size, EquationElem, Limits, MathContext, MathSize, Scaled,
};
use crate::model::{Destination, LinkElem};
use crate::syntax::Span;
use crate::text::{Font, Glyph, Lang, Region, TextElem, TextItem};
use crate::visualize::Paint;
use super::{scaled_font_size, stretch_glyph, MathContext, Scaled};
#[derive(Debug, Clone)]
pub enum MathFragment {
@ -350,7 +353,6 @@ impl GlyphFragment {
pub fn into_variant(self) -> VariantFragment {
VariantFragment {
c: self.c,
id: Some(self.id),
font_size: self.font_size,
italics_correction: self.italics_correction,
accent_attach: self.accent_attach,
@ -406,6 +408,26 @@ impl GlyphFragment {
self.set_id(ctx, alt_id);
}
}
/// Try to stretch a glyph to a desired height.
pub fn stretch_vertical(
self,
ctx: &MathContext,
height: Abs,
short_fall: Abs,
) -> VariantFragment {
stretch_glyph(ctx, self, height, short_fall, Axis::Y)
}
/// Try to stretch a glyph to a desired width.
pub fn stretch_horizontal(
self,
ctx: &MathContext,
width: Abs,
short_fall: Abs,
) -> VariantFragment {
stretch_glyph(ctx, self, width, short_fall, Axis::X)
}
}
impl Debug for GlyphFragment {
@ -417,7 +439,6 @@ impl Debug for GlyphFragment {
#[derive(Clone)]
pub struct VariantFragment {
pub c: char,
pub id: Option<GlyphId>,
pub italics_correction: Abs,
pub accent_attach: Abs,
pub frame: Frame,
@ -582,3 +603,102 @@ fn kern_at_height(
Some(kern.kern(i)?.scaled(ctx, font_size))
}
/// Describes in which situation a frame should use limits for attachments.
#[derive(Debug, Copy, Clone)]
pub enum Limits {
/// Always scripts.
Never,
/// Display limits only in `display` math.
Display,
/// Always limits.
Always,
}
impl Limits {
/// The default limit configuration if the given character is the base.
pub fn for_char(c: char) -> Self {
match unicode_math_class::class(c) {
Some(MathClass::Large) => {
if is_integral_char(c) {
Limits::Never
} else {
Limits::Display
}
}
Some(MathClass::Relation) => Limits::Always,
_ => Limits::Never,
}
}
/// The default limit configuration for a math class.
pub fn for_class(class: MathClass) -> Self {
match class {
MathClass::Large => Self::Display,
MathClass::Relation => Self::Always,
_ => Self::Never,
}
}
/// Whether limits should be displayed in this context.
pub fn active(&self, styles: StyleChain) -> bool {
match self {
Self::Always => true,
Self::Display => EquationElem::size_in(styles) == MathSize::Display,
Self::Never => false,
}
}
}
/// Determines if the character is one of a variety of integral signs.
fn is_integral_char(c: char) -> bool {
('∫'..='∳').contains(&c) || ('⨋'..='⨜').contains(&c)
}
/// An OpenType substitution table that is applicable to glyph-wise substitutions.
pub enum GlyphwiseSubsts<'a> {
Single(SingleSubstitution<'a>),
Alternate(AlternateSubstitution<'a>, u32),
}
impl<'a> GlyphwiseSubsts<'a> {
pub fn new(gsub: LayoutTable<'a>, feature: Feature) -> Option<Self> {
let table = gsub
.features
.find(ttf_parser::Tag(feature.tag.0))
.and_then(|feature| feature.lookup_indices.get(0))
.and_then(|index| gsub.lookups.get(index))?;
let table = table.subtables.get::<SubstitutionSubtable>(0)?;
match table {
SubstitutionSubtable::Single(single_glyphs) => {
Some(Self::Single(single_glyphs))
}
SubstitutionSubtable::Alternate(alt_glyphs) => {
Some(Self::Alternate(alt_glyphs, feature.value))
}
_ => None,
}
}
pub fn try_apply(&self, glyph_id: GlyphId) -> Option<GlyphId> {
match self {
Self::Single(single) => match single {
SingleSubstitution::Format1 { coverage, delta } => coverage
.get(glyph_id)
.map(|_| GlyphId(glyph_id.0.wrapping_add(*delta as u16))),
SingleSubstitution::Format2 { coverage, substitutes } => {
coverage.get(glyph_id).and_then(|idx| substitutes.get(idx))
}
},
Self::Alternate(alternate, value) => alternate
.coverage
.get(glyph_id)
.and_then(|idx| alternate.alternate_sets.get(idx))
.and_then(|set| set.alternates.get(*value as u16)),
}
}
pub fn apply(&self, glyph_id: GlyphId) -> GlyphId {
self.try_apply(glyph_id).unwrap_or(glyph_id)
}
}

View File

@ -0,0 +1,135 @@
use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, Smart, StyleChain};
use typst_library::layout::{Abs, Axis, Length, Rel};
use typst_library::math::{EquationElem, LrElem, MidElem};
use unicode_math_class::MathClass;
use super::{stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL};
/// Lays out an [`LrElem`].
#[typst_macros::time(name = "math.lr", span = elem.span())]
pub fn layout_lr(
elem: &Packed<LrElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let mut body = elem.body();
// Extract from an EquationElem.
if let Some(equation) = body.to_packed::<EquationElem>() {
body = equation.body();
}
// Extract implicit LrElem.
if let Some(lr) = body.to_packed::<LrElem>() {
if lr.size(styles).is_auto() {
body = lr.body();
}
}
let mut fragments = ctx.layout_into_fragments(body, styles)?;
let axis = scaled!(ctx, styles, axis_height);
let max_extent = fragments
.iter()
.map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis))
.max()
.unwrap_or_default();
let relative_to = 2.0 * max_extent;
let height = elem.size(styles);
// Scale up fragments at both ends.
match fragments.as_mut_slice() {
[one] => scale(ctx, styles, one, relative_to, height, None),
[first, .., last] => {
scale(ctx, styles, first, relative_to, height, Some(MathClass::Opening));
scale(ctx, styles, last, relative_to, height, Some(MathClass::Closing));
}
_ => {}
}
// Handle MathFragment::Variant fragments that should be scaled up.
for fragment in &mut fragments {
if let MathFragment::Variant(ref mut variant) = fragment {
if variant.mid_stretched == Some(false) {
variant.mid_stretched = Some(true);
scale(ctx, styles, fragment, relative_to, height, Some(MathClass::Large));
}
}
}
// Remove weak SpacingFragment immediately after the opening or immediately
// before the closing.
let original_len = fragments.len();
let mut index = 0;
fragments.retain(|fragment| {
index += 1;
(index != 2 && index + 1 != original_len)
|| !matches!(fragment, MathFragment::Spacing(_, true))
});
ctx.extend(fragments);
Ok(())
}
/// Lays out a [`MidElem`].
#[typst_macros::time(name = "math.mid", span = elem.span())]
pub fn layout_mid(
elem: &Packed<MidElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let mut fragments = ctx.layout_into_fragments(elem.body(), styles)?;
for fragment in &mut fragments {
match fragment {
MathFragment::Glyph(glyph) => {
let mut new = glyph.clone().into_variant();
new.mid_stretched = Some(false);
new.class = MathClass::Fence;
*fragment = MathFragment::Variant(new);
}
MathFragment::Variant(variant) => {
variant.mid_stretched = Some(false);
variant.class = MathClass::Fence;
}
_ => {}
}
}
ctx.extend(fragments);
Ok(())
}
/// Scale a math fragment to a height.
fn scale(
ctx: &mut MathContext,
styles: StyleChain,
fragment: &mut MathFragment,
relative_to: Abs,
height: Smart<Rel<Length>>,
apply: Option<MathClass>,
) {
if matches!(
fragment.class(),
MathClass::Opening | MathClass::Closing | MathClass::Fence
) {
// This unwrap doesn't really matter. If it is None, then the fragment
// won't be stretchable anyways.
let short_fall = DELIM_SHORT_FALL.at(fragment.font_size().unwrap_or_default());
stretch_fragment(
ctx,
styles,
fragment,
Some(Axis::Y),
Some(relative_to),
height,
short_fall,
);
if let Some(class) = apply {
fragment.set_class(class);
}
}
}

View File

@ -0,0 +1,333 @@
use typst_library::diag::{bail, SourceResult};
use typst_library::foundations::{Content, Packed, StyleChain};
use typst_library::layout::{
Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size,
};
use typst_library::math::{Augment, AugmentOffsets, CasesElem, MatElem, VecElem};
use typst_library::text::TextElem;
use typst_library::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape};
use typst_syntax::Span;
use super::{
alignments, delimiter_alignment, scaled_font_size, stack, style_for_denominator,
AlignmentResult, FrameFragment, GlyphFragment, LeftRightAlternator, MathContext,
Scaled, DELIM_SHORT_FALL,
};
const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
const DEFAULT_STROKE_THICKNESS: Em = Em::new(0.05);
/// Lays out a [`VecElem`].
#[typst_macros::time(name = "math.vec", span = elem.span())]
pub fn layout_vec(
elem: &Packed<VecElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let delim = elem.delim(styles);
let frame = layout_vec_body(
ctx,
styles,
elem.children(),
elem.align(styles),
elem.gap(styles).at(scaled_font_size(ctx, styles)),
LeftRightAlternator::Right,
)?;
layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), elem.span())
}
/// Lays out a [`MatElem`].
#[typst_macros::time(name = "math.mat", span = elem.span())]
pub fn layout_mat(
elem: &Packed<MatElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let augment = elem.augment(styles);
let rows = elem.rows();
if let Some(aug) = &augment {
for &offset in &aug.hline.0 {
if offset == 0 || offset.unsigned_abs() >= rows.len() {
bail!(
elem.span(),
"cannot draw a horizontal line after row {} of a matrix with {} rows",
if offset < 0 { rows.len() as isize + offset } else { offset },
rows.len()
);
}
}
let ncols = elem.rows().first().map_or(0, |row| row.len());
for &offset in &aug.vline.0 {
if offset == 0 || offset.unsigned_abs() >= ncols {
bail!(
elem.span(),
"cannot draw a vertical line after column {} of a matrix with {} columns",
if offset < 0 { ncols as isize + offset } else { offset },
ncols
);
}
}
}
let font_size = scaled_font_size(ctx, styles);
let column_gap = elem.column_gap(styles).at(font_size);
let row_gap = elem.row_gap(styles).at(font_size);
let delim = elem.delim(styles);
let frame = layout_mat_body(
ctx,
styles,
rows,
elem.align(styles),
augment,
Axes::new(column_gap, row_gap),
elem.span(),
)?;
layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), elem.span())
}
/// Lays out a [`CasesElem`].
#[typst_macros::time(name = "math.cases", span = elem.span())]
pub fn layout_cases(
elem: &Packed<CasesElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let delim = elem.delim(styles);
let frame = layout_vec_body(
ctx,
styles,
elem.children(),
FixedAlignment::Start,
elem.gap(styles).at(scaled_font_size(ctx, styles)),
LeftRightAlternator::None,
)?;
let (open, close) =
if elem.reverse(styles) { (None, delim.close()) } else { (delim.open(), None) };
layout_delimiters(ctx, styles, frame, open, close, elem.span())
}
/// Layout the inner contents of a vector.
fn layout_vec_body(
ctx: &mut MathContext,
styles: StyleChain,
column: &[Content],
align: FixedAlignment,
row_gap: Rel<Abs>,
alternator: LeftRightAlternator,
) -> SourceResult<Frame> {
let gap = row_gap.relative_to(ctx.region.size.y);
let denom_style = style_for_denominator(styles);
let mut flat = vec![];
for child in column {
flat.push(ctx.layout_into_run(child, styles.chain(&denom_style))?);
}
// We pad ascent and descent with the ascent and descent of the paren
// to ensure that normal vectors are aligned with others unless they are
// way too big.
let paren =
GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached());
Ok(stack(flat, align, gap, 0, alternator, Some((paren.ascent, paren.descent))))
}
/// Layout the inner contents of a matrix.
fn layout_mat_body(
ctx: &mut MathContext,
styles: StyleChain,
rows: &[Vec<Content>],
align: FixedAlignment,
augment: Option<Augment<Abs>>,
gap: Axes<Rel<Abs>>,
span: Span,
) -> SourceResult<Frame> {
let ncols = rows.first().map_or(0, |row| row.len());
let nrows = rows.len();
if ncols == 0 || nrows == 0 {
return Ok(Frame::soft(Size::zero()));
}
let gap = gap.zip_map(ctx.region.size, Rel::relative_to);
let half_gap = gap * 0.5;
// We provide a default stroke thickness that scales
// with font size to ensure that augmentation lines
// look correct by default at all matrix sizes.
// The line cap is also set to square because it looks more "correct".
let font_size = scaled_font_size(ctx, styles);
let default_stroke_thickness = DEFAULT_STROKE_THICKNESS.at(font_size);
let default_stroke = FixedStroke {
thickness: default_stroke_thickness,
paint: TextElem::fill_in(styles).as_decoration(),
cap: LineCap::Square,
..Default::default()
};
let (hline, vline, stroke) = match augment {
Some(augment) => {
// We need to get stroke here for ownership.
let stroke = augment.stroke.unwrap_or_default().unwrap_or(default_stroke);
(augment.hline, augment.vline, stroke)
}
_ => (AugmentOffsets::default(), AugmentOffsets::default(), default_stroke),
};
// Before the full matrix body can be laid out, the
// individual cells must first be independently laid out
// so we can ensure alignment across rows and columns.
// This variable stores the maximum ascent and descent for each row.
let mut heights = vec![(Abs::zero(), Abs::zero()); nrows];
// We want to transpose our data layout to columns
// before final layout. For efficiency, the columns
// variable is set up here and newly generated
// individual cells are then added to it.
let mut cols = vec![vec![]; ncols];
let denom_style = style_for_denominator(styles);
// We pad ascent and descent with the ascent and descent of the paren
// to ensure that normal matrices are aligned with others unless they are
// way too big.
let paren =
GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached());
for (row, (ascent, descent)) in rows.iter().zip(&mut heights) {
for (cell, col) in row.iter().zip(&mut cols) {
let cell = ctx.layout_into_run(cell, styles.chain(&denom_style))?;
ascent.set_max(cell.ascent().max(paren.ascent));
descent.set_max(cell.descent().max(paren.descent));
col.push(cell);
}
}
// For each row, combine maximum ascent and descent into a row height.
// Sum the row heights, then add the total height of the gaps between rows.
let total_height =
heights.iter().map(|&(a, b)| a + b).sum::<Abs>() + gap.y * (nrows - 1) as f64;
// Width starts at zero because it can't be calculated until later
let mut frame = Frame::soft(Size::new(Abs::zero(), total_height));
let mut x = Abs::zero();
for (index, col) in cols.into_iter().enumerate() {
let AlignmentResult { points, width: rcol } = alignments(&col);
let mut y = Abs::zero();
for (cell, &(ascent, descent)) in col.into_iter().zip(&heights) {
let cell = cell.into_line_frame(&points, LeftRightAlternator::Right);
let pos = Point::new(
if points.is_empty() {
x + align.position(rcol - cell.width())
} else {
x
},
y + ascent - cell.ascent(),
);
frame.push_frame(pos, cell);
y += ascent + descent + gap.y;
}
// Advance to the end of the column
x += rcol;
// If a vertical line should be inserted after this column
if vline.0.contains(&(index as isize + 1))
|| vline.0.contains(&(1 - ((ncols - index) as isize)))
{
frame.push(
Point::with_x(x + half_gap.x),
line_item(total_height, true, stroke.clone(), span),
);
}
// Advance to the start of the next column
x += gap.x;
}
// Once all the columns are laid out, the total width can be calculated
let total_width = x - gap.x;
// This allows the horizontal lines to be laid out
for line in hline.0 {
let real_line =
if line < 0 { nrows - line.unsigned_abs() } else { line as usize };
let offset = (heights[0..real_line].iter().map(|&(a, b)| a + b).sum::<Abs>()
+ gap.y * (real_line - 1) as f64)
+ half_gap.y;
frame.push(
Point::with_y(offset),
line_item(total_width, false, stroke.clone(), span),
);
}
frame.size_mut().x = total_width;
Ok(frame)
}
fn line_item(length: Abs, vertical: bool, stroke: FixedStroke, span: Span) -> FrameItem {
let line_geom = if vertical {
Geometry::Line(Point::with_y(length))
} else {
Geometry::Line(Point::with_x(length))
};
FrameItem::Shape(
Shape {
geometry: line_geom,
fill: None,
fill_rule: FillRule::default(),
stroke: Some(stroke),
},
span,
)
}
/// Layout the outer wrapper around the body of a vector or matrix.
fn layout_delimiters(
ctx: &mut MathContext,
styles: StyleChain,
mut frame: Frame,
left: Option<char>,
right: Option<char>,
span: Span,
) -> SourceResult<()> {
let font_size = scaled_font_size(ctx, styles);
let short_fall = DELIM_SHORT_FALL.at(font_size);
let axis = ctx.constants.axis_height().scaled(ctx, font_size);
let height = frame.height();
let target = height + VERTICAL_PADDING.of(height);
frame.set_baseline(height / 2.0 + axis);
if let Some(left) = left {
let mut left = GlyphFragment::new(ctx, styles, left, span)
.stretch_vertical(ctx, target, short_fall);
left.align_on_axis(ctx, delimiter_alignment(left.c));
ctx.push(left);
}
ctx.push(FrameFragment::new(ctx, styles, frame));
if let Some(right) = right {
let mut right = GlyphFragment::new(ctx, styles, right, span)
.stretch_vertical(ctx, target, short_fall);
right.align_on_axis(ctx, delimiter_alignment(right.c));
ctx.push(right);
}
Ok(())
}

View File

@ -0,0 +1,703 @@
#[macro_use]
mod shared;
mod accent;
mod attach;
mod cancel;
mod frac;
mod fragment;
mod lr;
mod mat;
mod root;
mod run;
mod stretch;
mod text;
mod underover;
use ttf_parser::gsub::SubstitutionSubtable;
use typst_library::diag::{bail, SourceResult};
use typst_library::engine::Engine;
use typst_library::foundations::{Content, NativeElement, Packed, Resolve, StyleChain};
use typst_library::introspection::{Counter, Locator, SplitLocator, TagElem};
use typst_library::layout::{
Abs, AlignElem, Axes, BlockElem, BoxElem, Em, FixedAlignment, Fragment, Frame, HElem,
InlineItem, OuterHAlignment, PlaceElem, Point, Region, Regions, Size, Spacing,
SpecificAlignment, VAlignment,
};
use typst_library::math::*;
use typst_library::model::ParElem;
use typst_library::routines::{Arenas, RealizationKind};
use typst_library::text::{
families, features, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds,
TextElem, TextSize,
};
use typst_library::World;
use typst_syntax::Span;
use typst_utils::Numeric;
use unicode_math_class::MathClass;
use self::fragment::{
FrameFragment, GlyphFragment, GlyphwiseSubsts, Limits, MathFragment, VariantFragment,
};
use self::run::{LeftRightAlternator, MathRun, MathRunFrameBuilder};
use self::shared::*;
use self::stretch::{stretch_fragment, stretch_glyph};
/// Layout an inline equation (in a paragraph).
#[typst_macros::time(span = elem.span())]
pub fn layout_equation_inline(
elem: &Packed<EquationElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Size,
) -> SourceResult<Vec<InlineItem>> {
assert!(!elem.block(styles));
let font = find_math_font(engine, styles, elem.span())?;
let mut locator = locator.split();
let mut ctx = MathContext::new(engine, &mut locator, styles, region, &font);
let run = ctx.layout_into_run(&elem.body, styles)?;
let mut items = if run.row_count() == 1 {
run.into_par_items()
} else {
vec![InlineItem::Frame(run.into_fragment(&ctx, styles).into_frame())]
};
// An empty equation should have a height, so we still create a frame
// (which is then resized in the loop).
if items.is_empty() {
items.push(InlineItem::Frame(Frame::soft(Size::zero())));
}
for item in &mut items {
let InlineItem::Frame(frame) = item else { continue };
let font_size = scaled_font_size(&ctx, styles);
let slack = ParElem::leading_in(styles) * 0.7;
let (t, b) = font.edges(
TextElem::top_edge_in(styles),
TextElem::bottom_edge_in(styles),
font_size,
TextEdgeBounds::Frame(frame),
);
let ascent = t.max(frame.ascent() - slack);
let descent = b.max(frame.descent() - slack);
frame.translate(Point::with_y(ascent - frame.baseline()));
frame.size_mut().y = ascent + descent;
}
Ok(items)
}
/// Layout a block-level equation (in a flow).
#[typst_macros::time(span = elem.span())]
pub fn layout_equation_block(
elem: &Packed<EquationElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
assert!(elem.block(styles));
let span = elem.span();
let font = find_math_font(engine, styles, span)?;
let mut locator = locator.split();
let mut ctx = MathContext::new(engine, &mut locator, styles, regions.base(), &font);
let full_equation_builder = ctx
.layout_into_run(&elem.body, styles)?
.multiline_frame_builder(&ctx, styles);
let width = full_equation_builder.size.x;
let equation_builders = if BlockElem::breakable_in(styles) {
let mut rows = full_equation_builder.frames.into_iter().peekable();
let mut equation_builders = vec![];
let mut last_first_pos = Point::zero();
let mut regions = regions;
loop {
// Keep track of the position of the first row in this region,
// so that the offset can be reverted later.
let Some(&(_, first_pos)) = rows.peek() else { break };
last_first_pos = first_pos;
let mut frames = vec![];
let mut height = Abs::zero();
while let Some((sub, pos)) = rows.peek() {
let mut pos = *pos;
pos.y -= first_pos.y;
// Finish this region if the line doesn't fit. Only do it if
// we placed at least one line _or_ we still have non-last
// regions. Crucially, we don't want to infinitely create
// new regions which are too small.
if !regions.size.y.fits(sub.height() + pos.y)
&& (regions.may_progress()
|| (regions.may_break() && !frames.is_empty()))
{
break;
}
let (sub, _) = rows.next().unwrap();
height = height.max(pos.y + sub.height());
frames.push((sub, pos));
}
equation_builders
.push(MathRunFrameBuilder { frames, size: Size::new(width, height) });
regions.next();
}
// Append remaining rows to the equation builder of the last region.
if let Some(equation_builder) = equation_builders.last_mut() {
equation_builder.frames.extend(rows.map(|(frame, mut pos)| {
pos.y -= last_first_pos.y;
(frame, pos)
}));
let height = equation_builder
.frames
.iter()
.map(|(frame, pos)| frame.height() + pos.y)
.max()
.unwrap_or(equation_builder.size.y);
equation_builder.size.y = height;
}
// Ensure that there is at least one frame, even for empty equations.
if equation_builders.is_empty() {
equation_builders
.push(MathRunFrameBuilder { frames: vec![], size: Size::zero() });
}
equation_builders
} else {
vec![full_equation_builder]
};
let Some(numbering) = (**elem).numbering(styles) else {
let frames = equation_builders
.into_iter()
.map(MathRunFrameBuilder::build)
.collect();
return Ok(Fragment::frames(frames));
};
let pod = Region::new(regions.base(), Axes::splat(false));
let counter = Counter::of(EquationElem::elem())
.display_at_loc(engine, elem.location().unwrap(), styles, numbering)?
.spanned(span);
let number =
(engine.routines.layout_frame)(engine, &counter, locator.next(&()), styles, pod)?;
static NUMBER_GUTTER: Em = Em::new(0.5);
let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles);
let number_align = match elem.number_align(styles) {
SpecificAlignment::H(h) => SpecificAlignment::Both(h, VAlignment::Horizon),
SpecificAlignment::V(v) => SpecificAlignment::Both(OuterHAlignment::End, v),
SpecificAlignment::Both(h, v) => SpecificAlignment::Both(h, v),
};
// Add equation numbers to each equation region.
let region_count = equation_builders.len();
let frames = equation_builders
.into_iter()
.map(|builder| {
if builder.frames.is_empty() && region_count > 1 {
// Don't number empty regions, but do number empty equations.
return builder.build();
}
add_equation_number(
builder,
number.clone(),
number_align.resolve(styles),
AlignElem::alignment_in(styles).resolve(styles).x,
regions.size.x,
full_number_width,
)
})
.collect();
Ok(Fragment::frames(frames))
}
fn find_math_font(
engine: &mut Engine<'_>,
styles: StyleChain,
span: Span,
) -> SourceResult<Font> {
let variant = variant(styles);
let world = engine.world;
let Some(font) = families(styles).find_map(|family| {
let id = world.book().select(family, variant)?;
let font = world.font(id)?;
let _ = font.ttf().tables().math?.constants?;
Some(font)
}) else {
bail!(span, "current font does not support math");
};
Ok(font)
}
fn add_equation_number(
equation_builder: MathRunFrameBuilder,
number: Frame,
number_align: Axes<FixedAlignment>,
equation_align: FixedAlignment,
region_size_x: Abs,
full_number_width: Abs,
) -> Frame {
let first =
equation_builder.frames.first().map_or(
(equation_builder.size, Point::zero(), Abs::zero()),
|(frame, pos)| (frame.size(), *pos, frame.baseline()),
);
let last =
equation_builder.frames.last().map_or(
(equation_builder.size, Point::zero(), Abs::zero()),
|(frame, pos)| (frame.size(), *pos, frame.baseline()),
);
let line_count = equation_builder.frames.len();
let mut equation = equation_builder.build();
let width = if region_size_x.is_finite() {
region_size_x
} else {
equation.width() + 2.0 * full_number_width
};
let is_multiline = line_count >= 2;
let resizing_offset = resize_equation(
&mut equation,
&number,
number_align,
equation_align,
width,
is_multiline,
[first, last],
);
equation.translate(Point::with_x(match (equation_align, number_align.x) {
(FixedAlignment::Start, FixedAlignment::Start) => full_number_width,
(FixedAlignment::End, FixedAlignment::End) => -full_number_width,
_ => Abs::zero(),
}));
let x = match number_align.x {
FixedAlignment::Start => Abs::zero(),
FixedAlignment::End => equation.width() - number.width(),
_ => unreachable!(),
};
let y = {
let align_baselines = |(_, pos, baseline): (_, Point, Abs), number: &Frame| {
resizing_offset.y + pos.y + baseline - number.baseline()
};
match number_align.y {
FixedAlignment::Start => align_baselines(first, &number),
FixedAlignment::Center if !is_multiline => align_baselines(first, &number),
// In this case, the center lines (not baselines) of the number frame
// and the equation frame shall be aligned.
FixedAlignment::Center => (equation.height() - number.height()) / 2.0,
FixedAlignment::End => align_baselines(last, &number),
}
};
equation.push_frame(Point::new(x, y), number);
equation
}
/// Resize the equation's frame accordingly so that it encompasses the number.
fn resize_equation(
equation: &mut Frame,
number: &Frame,
number_align: Axes<FixedAlignment>,
equation_align: FixedAlignment,
width: Abs,
is_multiline: bool,
[first, last]: [(Axes<Abs>, Point, Abs); 2],
) -> Point {
if matches!(number_align.y, FixedAlignment::Center if is_multiline) {
// In this case, the center lines (not baselines) of the number frame
// and the equation frame shall be aligned.
return equation.resize(
Size::new(width, equation.height().max(number.height())),
Axes::<FixedAlignment>::new(equation_align, FixedAlignment::Center),
);
}
let excess_above = Abs::zero().max({
if !is_multiline || matches!(number_align.y, FixedAlignment::Start) {
let (.., baseline) = first;
number.baseline() - baseline
} else {
Abs::zero()
}
});
let excess_below = Abs::zero().max({
if !is_multiline || matches!(number_align.y, FixedAlignment::End) {
let (size, .., baseline) = last;
(number.height() - number.baseline()) - (size.y - baseline)
} else {
Abs::zero()
}
});
// The vertical expansion is asymmetric on the top and bottom edges, so we
// first align at the top then translate the content downward later.
let resizing_offset = equation.resize(
Size::new(width, equation.height() + excess_above + excess_below),
Axes::<FixedAlignment>::new(equation_align, FixedAlignment::Start),
);
equation.translate(Point::with_y(excess_above));
resizing_offset + Point::with_y(excess_above)
}
/// The context for math layout.
struct MathContext<'a, 'v, 'e> {
// External.
engine: &'v mut Engine<'e>,
locator: &'v mut SplitLocator<'a>,
region: Region,
// Font-related.
font: &'a Font,
ttf: &'a ttf_parser::Face<'a>,
table: ttf_parser::math::Table<'a>,
constants: ttf_parser::math::Constants<'a>,
ssty_table: Option<ttf_parser::gsub::AlternateSubstitution<'a>>,
glyphwise_tables: Option<Vec<GlyphwiseSubsts<'a>>>,
space_width: Em,
// Mutable.
fragments: Vec<MathFragment>,
}
impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
/// Create a new math context.
fn new(
engine: &'v mut Engine<'e>,
locator: &'v mut SplitLocator<'a>,
styles: StyleChain<'a>,
base: Size,
font: &'a Font,
) -> Self {
let math_table = font.ttf().tables().math.unwrap();
let gsub_table = font.ttf().tables().gsub;
let constants = math_table.constants.unwrap();
let ssty_table = gsub_table
.and_then(|gsub| {
gsub.features
.find(ttf_parser::Tag::from_bytes(b"ssty"))
.and_then(|feature| feature.lookup_indices.get(0))
.and_then(|index| gsub.lookups.get(index))
})
.and_then(|ssty| ssty.subtables.get::<SubstitutionSubtable>(0))
.and_then(|ssty| match ssty {
SubstitutionSubtable::Alternate(alt_glyphs) => Some(alt_glyphs),
_ => None,
});
let features = features(styles);
let glyphwise_tables = gsub_table.map(|gsub| {
features
.into_iter()
.filter_map(|feature| GlyphwiseSubsts::new(gsub, feature))
.collect()
});
let ttf = font.ttf();
let space_width = ttf
.glyph_index(' ')
.and_then(|id| ttf.glyph_hor_advance(id))
.map(|advance| font.to_em(advance))
.unwrap_or(THICK);
Self {
engine,
locator,
region: Region::new(base, Axes::splat(false)),
font,
ttf: font.ttf(),
table: math_table,
constants,
ssty_table,
glyphwise_tables,
space_width,
fragments: vec![],
}
}
/// Push a fragment.
fn push(&mut self, fragment: impl Into<MathFragment>) {
self.fragments.push(fragment.into());
}
/// Push multiple fragments.
fn extend(&mut self, fragments: impl IntoIterator<Item = MathFragment>) {
self.fragments.extend(fragments);
}
/// Layout the given element and return the result as a [`MathRun`].
fn layout_into_run(
&mut self,
elem: &Content,
styles: StyleChain,
) -> SourceResult<MathRun> {
Ok(MathRun::new(self.layout_into_fragments(elem, styles)?))
}
/// Layout the given element and return the resulting [`MathFragment`]s.
fn layout_into_fragments(
&mut self,
elem: &Content,
styles: StyleChain,
) -> SourceResult<Vec<MathFragment>> {
// The element's layout_math() changes the fragments held in this
// MathContext object, but for convenience this function shouldn't change
// them, so we restore the MathContext's fragments after obtaining the
// layout result.
let prev = std::mem::take(&mut self.fragments);
self.layout_into_self(elem, styles)?;
Ok(std::mem::replace(&mut self.fragments, prev))
}
/// Layout the given element and return the result as a
/// unified [`MathFragment`].
fn layout_into_fragment(
&mut self,
elem: &Content,
styles: StyleChain,
) -> SourceResult<MathFragment> {
Ok(self.layout_into_run(elem, styles)?.into_fragment(self, styles))
}
/// Layout the given element and return the result as a [`Frame`].
fn layout_into_frame(
&mut self,
elem: &Content,
styles: StyleChain,
) -> SourceResult<Frame> {
Ok(self.layout_into_fragment(elem, styles)?.into_frame())
}
/// Layout arbitrary content.
fn layout_into_self(
&mut self,
content: &Content,
styles: StyleChain,
) -> SourceResult<()> {
let arenas = Arenas::default();
let pairs = (self.engine.routines.realize)(
RealizationKind::Math,
self.engine,
self.locator,
&arenas,
content,
styles,
)?;
let outer = styles;
for (elem, styles) in pairs {
// Hack because the font is fixed in math.
if styles != outer && TextElem::font_in(styles) != TextElem::font_in(outer) {
let frame = layout_external(elem, self, styles)?;
self.push(FrameFragment::new(self, styles, frame).with_spaced(true));
continue;
}
layout_realized(elem, self, styles)?;
}
Ok(())
}
}
/// Lays out a leaf element resulting from realization.
fn layout_realized(
elem: &Content,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
if let Some(elem) = elem.to_packed::<TagElem>() {
ctx.push(MathFragment::Tag(elem.tag.clone()));
} else if elem.is::<SpaceElem>() {
let font_size = scaled_font_size(ctx, styles);
ctx.push(MathFragment::Space(ctx.space_width.at(font_size)));
} else if elem.is::<LinebreakElem>() {
ctx.push(MathFragment::Linebreak);
} else if let Some(elem) = elem.to_packed::<HElem>() {
layout_h(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<TextElem>() {
self::text::layout_text(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<BoxElem>() {
layout_box(elem, ctx, styles)?;
} else if elem.is::<AlignPointElem>() {
ctx.push(MathFragment::Align);
} else if let Some(elem) = elem.to_packed::<ClassElem>() {
layout_class(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<AccentElem>() {
self::accent::layout_accent(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<AttachElem>() {
self::attach::layout_attach(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<PrimesElem>() {
self::attach::layout_primes(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<ScriptsElem>() {
self::attach::layout_scripts(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<LimitsElem>() {
self::attach::layout_limits(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<CancelElem>() {
self::cancel::layout_cancel(elem, ctx, styles)?
} else if let Some(elem) = elem.to_packed::<FracElem>() {
self::frac::layout_frac(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<BinomElem>() {
self::frac::layout_binom(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<LrElem>() {
self::lr::layout_lr(elem, ctx, styles)?
} else if let Some(elem) = elem.to_packed::<MidElem>() {
self::lr::layout_mid(elem, ctx, styles)?
} else if let Some(elem) = elem.to_packed::<VecElem>() {
self::mat::layout_vec(elem, ctx, styles)?
} else if let Some(elem) = elem.to_packed::<MatElem>() {
self::mat::layout_mat(elem, ctx, styles)?
} else if let Some(elem) = elem.to_packed::<CasesElem>() {
self::mat::layout_cases(elem, ctx, styles)?
} else if let Some(elem) = elem.to_packed::<OpElem>() {
layout_op(elem, ctx, styles)?
} else if let Some(elem) = elem.to_packed::<RootElem>() {
self::root::layout_root(elem, ctx, styles)?
} else if let Some(elem) = elem.to_packed::<StretchElem>() {
self::stretch::layout_stretch(elem, ctx, styles)?
} else if let Some(elem) = elem.to_packed::<UnderlineElem>() {
self::underover::layout_underline(elem, ctx, styles)?
} else if let Some(elem) = elem.to_packed::<OverlineElem>() {
self::underover::layout_overline(elem, ctx, styles)?
} else if let Some(elem) = elem.to_packed::<UnderbraceElem>() {
self::underover::layout_underbrace(elem, ctx, styles)?
} else if let Some(elem) = elem.to_packed::<OverbraceElem>() {
self::underover::layout_overbrace(elem, ctx, styles)?
} else if let Some(elem) = elem.to_packed::<UnderbracketElem>() {
self::underover::layout_underbracket(elem, ctx, styles)?
} else if let Some(elem) = elem.to_packed::<OverbracketElem>() {
self::underover::layout_overbracket(elem, ctx, styles)?
} else if let Some(elem) = elem.to_packed::<UnderparenElem>() {
self::underover::layout_underparen(elem, ctx, styles)?
} else if let Some(elem) = elem.to_packed::<OverparenElem>() {
self::underover::layout_overparen(elem, ctx, styles)?
} else if let Some(elem) = elem.to_packed::<UndershellElem>() {
self::underover::layout_undershell(elem, ctx, styles)?
} else if let Some(elem) = elem.to_packed::<OvershellElem>() {
self::underover::layout_overshell(elem, ctx, styles)?
} else {
let mut frame = layout_external(elem, ctx, styles)?;
if !frame.has_baseline() {
let axis = scaled!(ctx, styles, axis_height);
frame.set_baseline(frame.height() / 2.0 + axis);
}
ctx.push(
FrameFragment::new(ctx, styles, frame)
.with_spaced(true)
.with_ignorant(elem.is::<PlaceElem>()),
);
}
Ok(())
}
/// Lays out an [`BoxElem`].
fn layout_box(
elem: &Packed<BoxElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let local = TextElem::set_size(TextSize(scaled_font_size(ctx, styles).into())).wrap();
let frame = (ctx.engine.routines.layout_box)(
elem,
ctx.engine,
ctx.locator.next(&elem.span()),
styles.chain(&local),
ctx.region.size,
)?;
ctx.push(FrameFragment::new(ctx, styles, frame).with_spaced(true));
Ok(())
}
/// Lays out an [`HElem`].
fn layout_h(
elem: &Packed<HElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
if let Spacing::Rel(rel) = elem.amount() {
if rel.rel.is_zero() {
ctx.push(MathFragment::Spacing(
rel.abs.at(scaled_font_size(ctx, styles)),
elem.weak(styles),
));
}
}
Ok(())
}
/// Lays out a [`ClassElem`].
#[typst_macros::time(name = "math.op", span = elem.span())]
fn layout_class(
elem: &Packed<ClassElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let class = *elem.class();
let style = EquationElem::set_class(Some(class)).wrap();
let mut fragment = ctx.layout_into_fragment(elem.body(), styles.chain(&style))?;
fragment.set_class(class);
fragment.set_limits(Limits::for_class(class));
ctx.push(fragment);
Ok(())
}
/// Lays out an [`OpElem`].
#[typst_macros::time(name = "math.op", span = elem.span())]
fn layout_op(
elem: &Packed<OpElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let fragment = ctx.layout_into_fragment(elem.text(), styles)?;
let italics = fragment.italics_correction();
let accent_attach = fragment.accent_attach();
let text_like = fragment.is_text_like();
ctx.push(
FrameFragment::new(ctx, styles, fragment.into_frame())
.with_class(MathClass::Large)
.with_italics_correction(italics)
.with_accent_attach(accent_attach)
.with_text_like(text_like)
.with_limits(if elem.limits(styles) {
Limits::Display
} else {
Limits::Never
}),
);
Ok(())
}
/// Layout into a frame with normal layout.
fn layout_external(
content: &Content,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<Frame> {
let local = TextElem::set_size(TextSize(scaled_font_size(ctx, styles).into())).wrap();
(ctx.engine.routines.layout_frame)(
ctx.engine,
content,
ctx.locator.next(&content.span()),
styles.chain(&local),
ctx.region,
)
}

View File

@ -1,63 +1,26 @@
use crate::diag::SourceResult;
use crate::foundations::{elem, func, Content, NativeElement, Packed, StyleChain};
use crate::layout::{Abs, Frame, FrameItem, Point, Size};
use crate::math::{
style_cramped, EquationElem, FrameFragment, GlyphFragment, LayoutMath, MathContext,
MathSize, Scaled,
};
use crate::syntax::Span;
use crate::text::TextElem;
use crate::visualize::{FixedStroke, Geometry};
use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain};
use typst_library::layout::{Abs, Frame, FrameItem, Point, Size};
use typst_library::math::{EquationElem, MathSize, RootElem};
use typst_library::text::TextElem;
use typst_library::visualize::{FixedStroke, Geometry};
/// A square root.
///
/// ```example
/// $ sqrt(3 - 2 sqrt(2)) = sqrt(2) - 1 $
/// ```
#[func(title = "Square Root")]
pub fn sqrt(
/// The call span of this function.
span: Span,
/// The expression to take the square root of.
radicand: Content,
) -> Content {
RootElem::new(radicand).pack().spanned(span)
}
use super::{style_cramped, FrameFragment, GlyphFragment, MathContext};
/// A general root.
///
/// ```example
/// $ root(3, x) $
/// ```
#[elem(LayoutMath)]
pub struct RootElem {
/// Which root of the radicand to take.
#[positional]
pub index: Option<Content>,
/// The expression to take the root of.
#[required]
pub radicand: Content,
}
impl LayoutMath for Packed<RootElem> {
#[typst_macros::time(name = "math.root", span = self.span())]
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
layout(ctx, styles, self.index(styles).as_ref(), self.radicand(), self.span())
}
}
/// Layout a root.
/// Lays out a [`RootElem`].
///
/// TeXbook page 443, page 360
/// See also: <https://www.w3.org/TR/mathml-core/#radicals-msqrt-mroot>
fn layout(
#[typst_macros::time(name = "math.root", span = elem.span())]
pub fn layout_root(
elem: &Packed<RootElem>,
ctx: &mut MathContext,
styles: StyleChain,
index: Option<&Content>,
radicand: &Content,
span: Span,
) -> SourceResult<()> {
let index = elem.index(styles);
let radicand = elem.radicand();
let span = elem.span();
let gap = scaled!(
ctx, styles,
text: radical_vertical_gap,
@ -94,6 +57,7 @@ fn layout(
// Layout the index.
let sscript = EquationElem::set_size(MathSize::ScriptScript).wrap();
let index = index
.as_ref()
.map(|elem| ctx.layout_into_frame(elem, styles.chain(&sscript)))
.transpose()?;

View File

@ -1,16 +1,14 @@
use std::iter::once;
use typst_library::foundations::{Resolve, StyleChain};
use typst_library::layout::{Abs, AlignElem, Em, Frame, InlineItem, Point, Size};
use typst_library::math::{EquationElem, MathSize, MEDIUM, THICK, THIN};
use typst_library::model::ParElem;
use unicode_math_class::MathClass;
use crate::foundations::{Resolve, StyleChain};
use crate::layout::{Abs, AlignElem, Em, Frame, InlineItem, Point, Size};
use crate::math::{
alignments, scaled_font_size, spacing, EquationElem, FrameFragment, MathContext,
MathFragment, MathSize,
};
use crate::model::ParElem;
use super::{alignments, scaled_font_size, FrameFragment, MathContext, MathFragment};
pub const TIGHT_LEADING: Em = Em::new(0.25);
const TIGHT_LEADING: Em = Em::new(0.25);
/// A linear collection of [`MathFragment`]s.
#[derive(Debug, Default, Clone)]
@ -423,3 +421,49 @@ impl MathRunFrameBuilder {
fn affects_row_height(fragment: &MathFragment) -> bool {
!matches!(fragment, MathFragment::Align | MathFragment::Linebreak)
}
/// Create the spacing between two fragments in a given style.
fn spacing(
l: &MathFragment,
space: Option<MathFragment>,
r: &MathFragment,
) -> Option<MathFragment> {
use MathClass::*;
let resolve = |v: Em, size_ref: &MathFragment| -> Option<MathFragment> {
let width = size_ref.font_size().map_or(Abs::zero(), |size| v.at(size));
Some(MathFragment::Spacing(width, false))
};
let script = |f: &MathFragment| f.math_size().is_some_and(|s| s <= MathSize::Script);
match (l.class(), r.class()) {
// No spacing before punctuation; thin spacing after punctuation, unless
// in script size.
(_, Punctuation) => None,
(Punctuation, _) if !script(l) => resolve(THIN, l),
// No spacing after opening delimiters and before closing delimiters.
(Opening, _) | (_, Closing) => None,
// Thick spacing around relations, unless followed by a another relation
// or in script size.
(Relation, Relation) => None,
(Relation, _) if !script(l) => resolve(THICK, l),
(_, Relation) if !script(r) => resolve(THICK, r),
// Medium spacing around binary operators, unless in script size.
(Binary, _) if !script(l) => resolve(MEDIUM, l),
(_, Binary) if !script(r) => resolve(MEDIUM, r),
// Thin spacing around large operators, unless to the left of
// an opening delimiter. TeXBook, p170
(Large, Opening | Fence) => None,
(Large, _) => resolve(THIN, l),
(_, Large) => resolve(THIN, r),
// Spacing around spaced frames.
_ if (l.is_spaced() || r.is_spaced()) => space,
_ => None,
}
}

View File

@ -0,0 +1,207 @@
use ttf_parser::math::MathValue;
use typst_library::foundations::{Style, StyleChain};
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size, VAlignment};
use typst_library::math::{EquationElem, MathSize};
use typst_library::text::TextElem;
use typst_utils::LazyHash;
use super::{LeftRightAlternator, MathContext, MathFragment, MathRun};
macro_rules! scaled {
($ctx:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => {
match typst_library::math::EquationElem::size_in($styles) {
typst_library::math::MathSize::Display => scaled!($ctx, $styles, $display),
_ => scaled!($ctx, $styles, $text),
}
};
($ctx:expr, $styles:expr, $name:ident) => {
$crate::math::Scaled::scaled(
$ctx.constants.$name(),
$ctx,
$crate::math::scaled_font_size($ctx, $styles),
)
};
}
macro_rules! percent {
($ctx:expr, $name:ident) => {
$ctx.constants.$name() as f64 / 100.0
};
}
/// How much less high scaled delimiters can be than what they wrap.
pub const DELIM_SHORT_FALL: Em = Em::new(0.1);
/// Converts some unit to an absolute length with the current font & font size.
pub trait Scaled {
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs;
}
impl Scaled for i16 {
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs {
ctx.font.to_em(self).at(font_size)
}
}
impl Scaled for u16 {
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs {
ctx.font.to_em(self).at(font_size)
}
}
impl Scaled for MathValue<'_> {
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs {
self.value.scaled(ctx, font_size)
}
}
/// Get the font size scaled with the `MathSize`.
pub fn scaled_font_size(ctx: &MathContext, styles: StyleChain) -> Abs {
let factor = match EquationElem::size_in(styles) {
MathSize::Display | MathSize::Text => 1.0,
MathSize::Script => percent!(ctx, script_percent_scale_down),
MathSize::ScriptScript => percent!(ctx, script_script_percent_scale_down),
};
factor * TextElem::size_in(styles)
}
/// Styles something as cramped.
pub fn style_cramped() -> LazyHash<Style> {
EquationElem::set_cramped(true).wrap()
}
/// The style for subscripts in the current style.
pub fn style_for_subscript(styles: StyleChain) -> [LazyHash<Style>; 2] {
[style_for_superscript(styles), EquationElem::set_cramped(true).wrap()]
}
/// The style for superscripts in the current style.
pub fn style_for_superscript(styles: StyleChain) -> LazyHash<Style> {
EquationElem::set_size(match EquationElem::size_in(styles) {
MathSize::Display | MathSize::Text => MathSize::Script,
MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript,
})
.wrap()
}
/// The style for numerators in the current style.
pub fn style_for_numerator(styles: StyleChain) -> LazyHash<Style> {
EquationElem::set_size(match EquationElem::size_in(styles) {
MathSize::Display => MathSize::Text,
MathSize::Text => MathSize::Script,
MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript,
})
.wrap()
}
/// The style for denominators in the current style.
pub fn style_for_denominator(styles: StyleChain) -> [LazyHash<Style>; 2] {
[style_for_numerator(styles), EquationElem::set_cramped(true).wrap()]
}
/// How a delimieter should be aligned when scaling.
pub fn delimiter_alignment(delimiter: char) -> VAlignment {
match delimiter {
'⌜' | '⌝' => VAlignment::Top,
'⌞' | '⌟' => VAlignment::Bottom,
_ => VAlignment::Horizon,
}
}
/// Stack rows on top of each other.
///
/// Add a `gap` between each row and uses the baseline of the `baseline`-th
/// row for the whole frame. `alternator` controls the left/right alternating
/// alignment behavior of `AlignPointElem` in the rows.
pub fn stack(
rows: Vec<MathRun>,
align: FixedAlignment,
gap: Abs,
baseline: usize,
alternator: LeftRightAlternator,
minimum_ascent_descent: Option<(Abs, Abs)>,
) -> Frame {
let rows: Vec<_> = rows.into_iter().flat_map(|r| r.rows()).collect();
let AlignmentResult { points, width } = alignments(&rows);
let rows: Vec<_> = rows
.into_iter()
.map(|row| row.into_line_frame(&points, alternator))
.collect();
let padded_height = |height: Abs| {
height.max(minimum_ascent_descent.map_or(Abs::zero(), |(a, d)| a + d))
};
let mut frame = Frame::soft(Size::new(
width,
rows.iter().map(|row| padded_height(row.height())).sum::<Abs>()
+ rows.len().saturating_sub(1) as f64 * gap,
));
let mut y = Abs::zero();
for (i, row) in rows.into_iter().enumerate() {
let x = if points.is_empty() {
align.position(width - row.width())
} else {
Abs::zero()
};
let ascent_padded_part = minimum_ascent_descent
.map_or(Abs::zero(), |(a, _)| (a - row.ascent()))
.max(Abs::zero());
let pos = Point::new(x, y + ascent_padded_part);
if i == baseline {
frame.set_baseline(y + row.baseline() + ascent_padded_part);
}
y += padded_height(row.height()) + gap;
frame.push_frame(pos, row);
}
frame
}
/// Determine the positions of the alignment points, according to the input rows combined.
pub fn alignments(rows: &[MathRun]) -> AlignmentResult {
let mut widths = Vec::<Abs>::new();
let mut pending_width = Abs::zero();
for row in rows {
let mut width = Abs::zero();
let mut alignment_index = 0;
for fragment in row.iter() {
if matches!(fragment, MathFragment::Align) {
if alignment_index < widths.len() {
widths[alignment_index].set_max(width);
} else {
widths.push(width.max(pending_width));
}
width = Abs::zero();
alignment_index += 1;
} else {
width += fragment.width();
}
}
if widths.is_empty() {
pending_width.set_max(width);
} else if alignment_index < widths.len() {
widths[alignment_index].set_max(width);
} else {
widths.push(width.max(pending_width));
}
}
let mut points = widths;
for i in 1..points.len() {
let prev = points[i - 1];
points[i] += prev;
}
AlignmentResult {
width: points.last().copied().unwrap_or(pending_width),
points,
}
}
pub struct AlignmentResult {
pub points: Vec<Abs>,
pub width: Abs,
}

View File

@ -1,64 +1,42 @@
use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
use ttf_parser::LazyArray16;
use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, Smart, StyleChain};
use typst_library::layout::{Abs, Axis, Frame, Length, Point, Rel, Size};
use typst_library::math::StretchElem;
use typst_utils::Get;
use crate::diag::SourceResult;
use crate::foundations::{elem, Content, Packed, Smart, StyleChain};
use crate::layout::{Abs, Axis, Frame, Length, Point, Rel, Size, VAlignment};
use crate::math::{
scaled_font_size, GlyphFragment, LayoutMath, MathContext, MathFragment, Scaled,
VariantFragment,
use super::{
delimiter_alignment, scaled_font_size, GlyphFragment, MathContext, MathFragment,
Scaled, VariantFragment,
};
use crate::utils::Get;
/// Maximum number of times extenders can be repeated.
const MAX_REPEATS: usize = 1024;
/// Stretches a glyph.
///
/// This function can also be used to automatically stretch the base of an
/// attachment, so that it fits the top and bottom attachments.
///
/// Note that only some glyphs can be stretched, and which ones can depend on
/// the math font being used. However, most math fonts are the same in this
/// regard.
///
/// ```example
/// $ H stretch(=)^"define" U + p V $
/// $ f : X stretch(->>, size: #150%)_"surjective" Y $
/// $ x stretch(harpoons.ltrb, size: #3em) y
/// stretch(\[, size: #150%) z $
/// ```
#[elem(LayoutMath)]
pub struct StretchElem {
/// The glyph to stretch.
#[required]
pub body: Content,
/// The size to stretch to, relative to the maximum size of the glyph and
/// its attachments.
pub size: Smart<Rel<Length>>,
}
impl LayoutMath for Packed<StretchElem> {
#[typst_macros::time(name = "math.stretch", span = self.span())]
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
let mut fragment = ctx.layout_into_fragment(self.body(), styles)?;
stretch_fragment(
ctx,
styles,
&mut fragment,
None,
None,
self.size(styles),
Abs::zero(),
);
ctx.push(fragment);
Ok(())
}
/// Lays out a [`StretchElem`].
#[typst_macros::time(name = "math.stretch", span = elem.span())]
pub fn layout_stretch(
elem: &Packed<StretchElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?;
stretch_fragment(
ctx,
styles,
&mut fragment,
None,
None,
elem.size(styles),
Abs::zero(),
);
ctx.push(fragment);
Ok(())
}
/// Attempts to stretch the given fragment by/to the amount given in stretch.
pub(super) fn stretch_fragment(
pub fn stretch_fragment(
ctx: &mut MathContext,
styles: StyleChain,
fragment: &mut MathFragment,
@ -77,9 +55,7 @@ pub(super) fn stretch_fragment(
// Return if we attempt to stretch along an axis which isn't stretchable,
// so that the original fragment isn't modified.
let Some(stretch_axis) = stretch_axis(ctx, &glyph) else {
return;
};
let Some(stretch_axis) = stretch_axis(ctx, &glyph) else { return };
let axis = axis.unwrap_or(stretch_axis);
if axis != stretch_axis {
return;
@ -105,67 +81,10 @@ pub(super) fn stretch_fragment(
*fragment = MathFragment::Variant(variant);
}
pub(super) fn delimiter_alignment(delimiter: char) -> VAlignment {
match delimiter {
'\u{231c}' | '\u{231d}' => VAlignment::Top,
'\u{231e}' | '\u{231f}' => VAlignment::Bottom,
_ => VAlignment::Horizon,
}
}
/// Return whether the glyph is stretchable and if it is, along which axis it
/// can be stretched.
fn stretch_axis(ctx: &MathContext, base: &GlyphFragment) -> Option<Axis> {
let base_id = base.id;
let vertical = ctx
.table
.variants
.and_then(|variants| variants.vertical_constructions.get(base_id))
.map(|_| Axis::Y);
let horizontal = ctx
.table
.variants
.and_then(|variants| variants.horizontal_constructions.get(base_id))
.map(|_| Axis::X);
match (vertical, horizontal) {
(vertical, None) => vertical,
(None, horizontal) => horizontal,
_ => {
// As far as we know, there aren't any glyphs that have both
// vertical and horizontal constructions. So for the time being, we
// will assume that a glyph cannot have both.
panic!("glyph {:?} has both vertical and horizontal constructions", base.c);
}
}
}
impl GlyphFragment {
/// Try to stretch a glyph to a desired height.
pub fn stretch_vertical(
self,
ctx: &MathContext,
height: Abs,
short_fall: Abs,
) -> VariantFragment {
stretch_glyph(ctx, self, height, short_fall, Axis::Y)
}
/// Try to stretch a glyph to a desired width.
pub fn stretch_horizontal(
self,
ctx: &MathContext,
width: Abs,
short_fall: Abs,
) -> VariantFragment {
stretch_glyph(ctx, self, width, short_fall, Axis::X)
}
}
/// Try to stretch a glyph to a desired width or height.
///
/// The resulting frame may not have the exact desired width.
fn stretch_glyph(
pub fn stretch_glyph(
ctx: &MathContext,
mut base: GlyphFragment,
target: Abs,
@ -218,6 +137,33 @@ fn stretch_glyph(
assemble(ctx, base, assembly, min_overlap, target, axis)
}
/// Return whether the glyph is stretchable and if it is, along which axis it
/// can be stretched.
fn stretch_axis(ctx: &MathContext, base: &GlyphFragment) -> Option<Axis> {
let base_id = base.id;
let vertical = ctx
.table
.variants
.and_then(|variants| variants.vertical_constructions.get(base_id))
.map(|_| Axis::Y);
let horizontal = ctx
.table
.variants
.and_then(|variants| variants.horizontal_constructions.get(base_id))
.map(|_| Axis::X);
match (vertical, horizontal) {
(vertical, None) => vertical,
(None, horizontal) => horizontal,
_ => {
// As far as we know, there aren't any glyphs that have both
// vertical and horizontal constructions. So for the time being, we
// will assume that a glyph cannot have both.
panic!("glyph {:?} has both vertical and horizontal constructions", base.c);
}
}
}
/// Assemble a glyph from parts.
fn assemble(
ctx: &MathContext,
@ -323,7 +269,6 @@ fn assemble(
VariantFragment {
c: base.c,
id: None,
frame,
font_size: base.font_size,
italics_correction: Abs::zero(),

View File

@ -0,0 +1,344 @@
use std::f64::consts::SQRT_2;
use ecow::{eco_vec, EcoString};
use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain, StyleVec};
use typst_library::layout::{Abs, Size};
use typst_library::math::{EquationElem, MathSize, MathVariant};
use typst_library::text::{
BottomEdge, BottomEdgeMetric, TextElem, TextSize, TopEdge, TopEdgeMetric,
};
use typst_syntax::{is_newline, Span};
use unicode_math_class::MathClass;
use unicode_segmentation::UnicodeSegmentation;
use super::{
scaled_font_size, FrameFragment, GlyphFragment, MathContext, MathFragment, MathRun,
};
/// Lays out a [`TextElem`].
pub fn layout_text(
elem: &Packed<TextElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let text = elem.text();
let span = elem.span();
let mut chars = text.chars();
let math_size = EquationElem::size_in(styles);
let fragment: MathFragment = if let Some(mut glyph) = chars
.next()
.filter(|_| chars.next().is_none())
.map(|c| styled_char(styles, c, true))
.and_then(|c| GlyphFragment::try_new(ctx, styles, c, span))
{
// A single letter that is available in the math font.
match math_size {
MathSize::Script => {
glyph.make_scriptsize(ctx);
}
MathSize::ScriptScript => {
glyph.make_scriptscriptsize(ctx);
}
_ => (),
}
if glyph.class == MathClass::Large {
let mut variant = if math_size == MathSize::Display {
let height = scaled!(ctx, styles, display_operator_min_height)
.max(SQRT_2 * glyph.height());
glyph.stretch_vertical(ctx, height, Abs::zero())
} else {
glyph.into_variant()
};
// TeXbook p 155. Large operators are always vertically centered on the axis.
variant.center_on_axis(ctx);
variant.into()
} else {
glyph.into()
}
} else if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
// Numbers aren't that difficult.
let mut fragments = vec![];
for c in text.chars() {
let c = styled_char(styles, c, false);
fragments.push(GlyphFragment::new(ctx, styles, c, span).into());
}
let frame = MathRun::new(fragments).into_frame(ctx, styles);
FrameFragment::new(ctx, styles, frame).with_text_like(true).into()
} else {
let local = [
TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)),
TextElem::set_bottom_edge(BottomEdge::Metric(BottomEdgeMetric::Bounds)),
TextElem::set_size(TextSize(scaled_font_size(ctx, styles).into())),
]
.map(|p| p.wrap());
// Anything else is handled by Typst's standard text layout.
let styles = styles.chain(&local);
let text: EcoString =
text.chars().map(|c| styled_char(styles, c, false)).collect();
if text.contains(is_newline) {
let mut fragments = vec![];
for (i, piece) in text.split(is_newline).enumerate() {
if i != 0 {
fragments.push(MathFragment::Linebreak);
}
if !piece.is_empty() {
fragments.push(layout_complex_text(piece, ctx, span, styles)?.into());
}
}
let mut frame = MathRun::new(fragments).into_frame(ctx, styles);
let axis = scaled!(ctx, styles, axis_height);
frame.set_baseline(frame.height() / 2.0 + axis);
FrameFragment::new(ctx, styles, frame).into()
} else {
layout_complex_text(&text, ctx, span, styles)?.into()
}
};
ctx.push(fragment);
Ok(())
}
/// Layout the given text string into a [`FrameFragment`].
fn layout_complex_text(
text: &str,
ctx: &mut MathContext,
span: Span,
styles: StyleChain,
) -> SourceResult<FrameFragment> {
// There isn't a natural width for a paragraph in a math environment;
// because it will be placed somewhere probably not at the left margin
// it will overflow. So emulate an `hbox` instead and allow the paragraph
// to extend as far as needed.
let spaced = text.graphemes(true).nth(1).is_some();
let elem = TextElem::packed(text).spanned(span);
let frame = (ctx.engine.routines.layout_inline)(
ctx.engine,
&StyleVec::wrap(eco_vec![elem]),
ctx.locator.next(&span),
styles,
false,
Size::splat(Abs::inf()),
false,
)?
.into_frame();
Ok(FrameFragment::new(ctx, styles, frame)
.with_class(MathClass::Alphabetic)
.with_text_like(true)
.with_spaced(spaced))
}
/// Select the correct styled math letter.
///
/// <https://www.w3.org/TR/mathml-core/#new-text-transform-mappings>
/// <https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols>
fn styled_char(styles: StyleChain, c: char, auto_italic: bool) -> char {
use MathVariant::*;
let variant = EquationElem::variant_in(styles);
let bold = EquationElem::bold_in(styles);
let italic = EquationElem::italic_in(styles).unwrap_or(
auto_italic
&& matches!(
c,
'a'..='z' | 'ı' | 'ȷ' | 'A'..='Z' | 'α'..='ω' |
'∂' | 'ϵ' | 'ϑ' | 'ϰ' | 'ϕ' | 'ϱ' | 'ϖ'
)
&& matches!(variant, Sans | Serif),
);
if let Some(c) = basic_exception(c) {
return c;
}
if let Some(c) = latin_exception(c, variant, bold, italic) {
return c;
}
if let Some(c) = greek_exception(c, variant, bold, italic) {
return c;
}
let base = match c {
'A'..='Z' => 'A',
'a'..='z' => 'a',
'Α'..='Ω' => 'Α',
'α'..='ω' => 'α',
'0'..='9' => '0',
// Hebrew Alef -> Dalet.
'\u{05D0}'..='\u{05D3}' => '\u{05D0}',
_ => return c,
};
let tuple = (variant, bold, italic);
let start = match c {
// Latin upper.
'A'..='Z' => match tuple {
(Serif, false, false) => 0x0041,
(Serif, true, false) => 0x1D400,
(Serif, false, true) => 0x1D434,
(Serif, true, true) => 0x1D468,
(Sans, false, false) => 0x1D5A0,
(Sans, true, false) => 0x1D5D4,
(Sans, false, true) => 0x1D608,
(Sans, true, true) => 0x1D63C,
(Cal, false, _) => 0x1D49C,
(Cal, true, _) => 0x1D4D0,
(Frak, false, _) => 0x1D504,
(Frak, true, _) => 0x1D56C,
(Mono, _, _) => 0x1D670,
(Bb, _, _) => 0x1D538,
},
// Latin lower.
'a'..='z' => match tuple {
(Serif, false, false) => 0x0061,
(Serif, true, false) => 0x1D41A,
(Serif, false, true) => 0x1D44E,
(Serif, true, true) => 0x1D482,
(Sans, false, false) => 0x1D5BA,
(Sans, true, false) => 0x1D5EE,
(Sans, false, true) => 0x1D622,
(Sans, true, true) => 0x1D656,
(Cal, false, _) => 0x1D4B6,
(Cal, true, _) => 0x1D4EA,
(Frak, false, _) => 0x1D51E,
(Frak, true, _) => 0x1D586,
(Mono, _, _) => 0x1D68A,
(Bb, _, _) => 0x1D552,
},
// Greek upper.
'Α'..='Ω' => match tuple {
(Serif, false, false) => 0x0391,
(Serif, true, false) => 0x1D6A8,
(Serif, false, true) => 0x1D6E2,
(Serif, true, true) => 0x1D71C,
(Sans, _, false) => 0x1D756,
(Sans, _, true) => 0x1D790,
(Cal | Frak | Mono | Bb, _, _) => return c,
},
// Greek lower.
'α'..='ω' => match tuple {
(Serif, false, false) => 0x03B1,
(Serif, true, false) => 0x1D6C2,
(Serif, false, true) => 0x1D6FC,
(Serif, true, true) => 0x1D736,
(Sans, _, false) => 0x1D770,
(Sans, _, true) => 0x1D7AA,
(Cal | Frak | Mono | Bb, _, _) => return c,
},
// Hebrew Alef -> Dalet.
'\u{05D0}'..='\u{05D3}' => 0x2135,
// Numbers.
'0'..='9' => match tuple {
(Serif, false, _) => 0x0030,
(Serif, true, _) => 0x1D7CE,
(Bb, _, _) => 0x1D7D8,
(Sans, false, _) => 0x1D7E2,
(Sans, true, _) => 0x1D7EC,
(Mono, _, _) => 0x1D7F6,
(Cal | Frak, _, _) => return c,
},
_ => unreachable!(),
};
std::char::from_u32(start + (c as u32 - base as u32)).unwrap()
}
fn basic_exception(c: char) -> Option<char> {
Some(match c {
'〈' => '⟨',
'〉' => '⟩',
'《' => '⟪',
'》' => '⟫',
_ => return None,
})
}
fn latin_exception(
c: char,
variant: MathVariant,
bold: bool,
italic: bool,
) -> Option<char> {
use MathVariant::*;
Some(match (c, variant, bold, italic) {
('B', Cal, false, _) => '',
('E', Cal, false, _) => '',
('F', Cal, false, _) => '',
('H', Cal, false, _) => '',
('I', Cal, false, _) => '',
('L', Cal, false, _) => '',
('M', Cal, false, _) => '',
('R', Cal, false, _) => '',
('C', Frak, false, _) => '',
('H', Frak, false, _) => '',
('I', Frak, false, _) => '',
('R', Frak, false, _) => '',
('Z', Frak, false, _) => '',
('C', Bb, ..) => '',
('H', Bb, ..) => '',
('N', Bb, ..) => '',
('P', Bb, ..) => '',
('Q', Bb, ..) => '',
('R', Bb, ..) => '',
('Z', Bb, ..) => '',
('D', Bb, _, true) => '',
('d', Bb, _, true) => '',
('e', Bb, _, true) => '',
('i', Bb, _, true) => '',
('j', Bb, _, true) => '',
('h', Serif, false, true) => '',
('e', Cal, false, _) => '',
('g', Cal, false, _) => '',
('o', Cal, false, _) => '',
('ı', Serif, .., true) => '𝚤',
('ȷ', Serif, .., true) => '𝚥',
_ => return None,
})
}
fn greek_exception(
c: char,
variant: MathVariant,
bold: bool,
italic: bool,
) -> Option<char> {
use MathVariant::*;
let list = match c {
'ϴ' => ['𝚹', '𝛳', '𝜭', '𝝧', '𝞡', 'ϴ'],
'∇' => ['𝛁', '𝛻', '𝜵', '𝝯', '𝞩', '∇'],
'∂' => ['𝛛', '𝜕', '𝝏', '𝞉', '𝟃', '∂'],
'ϵ' => ['𝛜', '𝜖', '𝝐', '𝞊', '𝟄', 'ϵ'],
'ϑ' => ['𝛝', '𝜗', '𝝑', '𝞋', '𝟅', 'ϑ'],
'ϰ' => ['𝛞', '𝜘', '𝝒', '𝞌', '𝟆', 'ϰ'],
'ϕ' => ['𝛟', '𝜙', '𝝓', '𝞍', '𝟇', 'ϕ'],
'ϱ' => ['𝛠', '𝜚', '𝝔', '𝞎', '𝟈', 'ϱ'],
'ϖ' => ['𝛡', '𝜛', '𝝕', '𝞏', '𝟉', 'ϖ'],
'Γ' => ['𝚪', '𝛤', '𝜞', '𝝘', '𝞒', 'ℾ'],
'γ' => ['𝛄', '𝛾', '𝜸', '𝝲', '𝞬', ''],
'Π' => ['𝚷', '𝛱', '𝜫', '𝝥', '𝞟', 'ℿ'],
'π' => ['𝛑', '𝜋', '𝝅', '𝝿', '𝞹', 'ℼ'],
'∑' => ['∑', '∑', '∑', '∑', '∑', '⅀'],
_ => return None,
};
Some(match (variant, bold, italic) {
(Serif, true, false) => list[0],
(Serif, false, true) => list[1],
(Serif, true, true) => list[2],
(Sans, _, false) => list[3],
(Sans, _, true) => list[4],
(Bb, ..) => list[5],
_ => return None,
})
}

View File

@ -0,0 +1,327 @@
use typst_library::diag::SourceResult;
use typst_library::foundations::{Content, Packed, StyleChain};
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, FrameItem, Point, Size};
use typst_library::math::{
OverbraceElem, OverbracketElem, OverlineElem, OverparenElem, OvershellElem,
UnderbraceElem, UnderbracketElem, UnderlineElem, UnderparenElem, UndershellElem,
};
use typst_library::text::TextElem;
use typst_library::visualize::{FixedStroke, Geometry};
use typst_syntax::Span;
use super::{
scaled_font_size, stack, style_cramped, style_for_subscript, style_for_superscript,
FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, MathRun,
};
const BRACE_GAP: Em = Em::new(0.25);
const BRACKET_GAP: Em = Em::new(0.25);
const PAREN_GAP: Em = Em::new(0.25);
const SHELL_GAP: Em = Em::new(0.25);
/// A marker to distinguish under- and overlines.
enum Position {
Under,
Over,
}
/// Lays out an [`UnderlineElem`].
#[typst_macros::time(name = "math.underline", span = elem.span())]
pub fn layout_underline(
elem: &Packed<UnderlineElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Under)
}
/// Lays out an [`OverlineElem`].
#[typst_macros::time(name = "math.overline", span = elem.span())]
pub fn layout_overline(
elem: &Packed<OverlineElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Over)
}
/// Lays out an [`UnderbraceElem`].
#[typst_macros::time(name = "math.underbrace", span = elem.span())]
pub fn layout_underbrace(
elem: &Packed<UnderbraceElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
layout_underoverspreader(
ctx,
styles,
elem.body(),
&elem.annotation(styles),
'⏟',
BRACE_GAP,
Position::Under,
elem.span(),
)
}
/// Lays out an [`OverbraceElem`].
#[typst_macros::time(name = "math.overbrace", span = elem.span())]
pub fn layout_overbrace(
elem: &Packed<OverbraceElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
layout_underoverspreader(
ctx,
styles,
elem.body(),
&elem.annotation(styles),
'⏞',
BRACE_GAP,
Position::Over,
elem.span(),
)
}
/// Lays out an [`UnderbracketElem`].
#[typst_macros::time(name = "math.underbracket", span = elem.span())]
pub fn layout_underbracket(
elem: &Packed<UnderbracketElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
layout_underoverspreader(
ctx,
styles,
elem.body(),
&elem.annotation(styles),
'⎵',
BRACKET_GAP,
Position::Under,
elem.span(),
)
}
/// Lays out an [`OverbracketElem`].
#[typst_macros::time(name = "math.overbracket", span = elem.span())]
pub fn layout_overbracket(
elem: &Packed<OverbracketElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
layout_underoverspreader(
ctx,
styles,
elem.body(),
&elem.annotation(styles),
'⎴',
BRACKET_GAP,
Position::Over,
elem.span(),
)
}
/// Lays out an [`UnderparenElem`].
#[typst_macros::time(name = "math.underparen", span = elem.span())]
pub fn layout_underparen(
elem: &Packed<UnderparenElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
layout_underoverspreader(
ctx,
styles,
elem.body(),
&elem.annotation(styles),
'⏝',
PAREN_GAP,
Position::Under,
elem.span(),
)
}
/// Lays out an [`OverparenElem`].
#[typst_macros::time(name = "math.overparen", span = elem.span())]
pub fn layout_overparen(
elem: &Packed<OverparenElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
layout_underoverspreader(
ctx,
styles,
elem.body(),
&elem.annotation(styles),
'⏜',
PAREN_GAP,
Position::Over,
elem.span(),
)
}
/// Lays out an [`UndershellElem`].
#[typst_macros::time(name = "math.undershell", span = elem.span())]
pub fn layout_undershell(
elem: &Packed<UndershellElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
layout_underoverspreader(
ctx,
styles,
elem.body(),
&elem.annotation(styles),
'⏡',
SHELL_GAP,
Position::Under,
elem.span(),
)
}
/// Lays out an [`OvershellElem`].
#[typst_macros::time(name = "math.overshell", span = elem.span())]
pub fn layout_overshell(
elem: &Packed<OvershellElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
layout_underoverspreader(
ctx,
styles,
elem.body(),
&elem.annotation(styles),
'⏠',
SHELL_GAP,
Position::Over,
elem.span(),
)
}
/// layout under- or overlined content.
fn layout_underoverline(
ctx: &mut MathContext,
styles: StyleChain,
body: &Content,
span: Span,
position: Position,
) -> SourceResult<()> {
let (extra_height, content, line_pos, content_pos, baseline, bar_height, line_adjust);
match position {
Position::Under => {
let sep = scaled!(ctx, styles, underbar_extra_descender);
bar_height = scaled!(ctx, styles, underbar_rule_thickness);
let gap = scaled!(ctx, styles, underbar_vertical_gap);
extra_height = sep + bar_height + gap;
content = ctx.layout_into_fragment(body, styles)?;
line_pos = Point::with_y(content.height() + gap + bar_height / 2.0);
content_pos = Point::zero();
baseline = content.ascent();
line_adjust = -content.italics_correction();
}
Position::Over => {
let sep = scaled!(ctx, styles, overbar_extra_ascender);
bar_height = scaled!(ctx, styles, overbar_rule_thickness);
let gap = scaled!(ctx, styles, overbar_vertical_gap);
extra_height = sep + bar_height + gap;
let cramped = style_cramped();
content = ctx.layout_into_fragment(body, styles.chain(&cramped))?;
line_pos = Point::with_y(sep + bar_height / 2.0);
content_pos = Point::with_y(extra_height);
baseline = content.ascent() + extra_height;
line_adjust = Abs::zero();
}
}
let width = content.width();
let height = content.height() + extra_height;
let size = Size::new(width, height);
let line_width = width + line_adjust;
let content_class = content.class();
let content_is_text_like = content.is_text_like();
let content_italics_correction = content.italics_correction();
let mut frame = Frame::soft(size);
frame.set_baseline(baseline);
frame.push_frame(content_pos, content.into_frame());
frame.push(
line_pos,
FrameItem::Shape(
Geometry::Line(Point::with_x(line_width)).stroked(FixedStroke {
paint: TextElem::fill_in(styles).as_decoration(),
thickness: bar_height,
..FixedStroke::default()
}),
span,
),
);
ctx.push(
FrameFragment::new(ctx, styles, frame)
.with_class(content_class)
.with_text_like(content_is_text_like)
.with_italics_correction(content_italics_correction),
);
Ok(())
}
/// Layout an over- or underbrace-like object.
#[allow(clippy::too_many_arguments)]
fn layout_underoverspreader(
ctx: &mut MathContext,
styles: StyleChain,
body: &Content,
annotation: &Option<Content>,
c: char,
gap: Em,
position: Position,
span: Span,
) -> SourceResult<()> {
let font_size = scaled_font_size(ctx, styles);
let gap = gap.at(font_size);
let body = ctx.layout_into_run(body, styles)?;
let body_class = body.class();
let body = body.into_fragment(ctx, styles);
let glyph = GlyphFragment::new(ctx, styles, c, span);
let stretched = glyph.stretch_horizontal(ctx, body.width(), Abs::zero());
let mut rows = vec![];
let baseline = match position {
Position::Under => {
rows.push(MathRun::new(vec![body]));
rows.push(stretched.into());
if let Some(annotation) = annotation {
let under_style = style_for_subscript(styles);
let annotation_styles = styles.chain(&under_style);
rows.push(ctx.layout_into_run(annotation, annotation_styles)?);
}
0
}
Position::Over => {
if let Some(annotation) = annotation {
let over_style = style_for_superscript(styles);
let annotation_styles = styles.chain(&over_style);
rows.push(ctx.layout_into_run(annotation, annotation_styles)?);
}
rows.push(stretched.into());
rows.push(MathRun::new(vec![body]));
rows.len() - 1
}
};
let frame = stack(
rows,
FixedAlignment::Center,
gap,
baseline,
LeftRightAlternator::Right,
None,
);
ctx.push(FrameFragment::new(ctx, styles, frame).with_class(body_class));
Ok(())
}

View File

@ -1,78 +1,14 @@
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{
elem, Content, NativeElement, Packed, Resolve, Show, StyleChain,
use typst_library::diag::SourceResult;
use typst_library::engine::Engine;
use typst_library::foundations::{Packed, Resolve, StyleChain};
use typst_library::introspection::Locator;
use typst_library::layout::{
Abs, Fragment, Frame, PadElem, Point, Regions, Rel, Sides, Size,
};
use crate::introspection::Locator;
use crate::layout::{
layout_fragment, Abs, BlockElem, Fragment, Frame, Length, Point, Regions, Rel, Sides,
Size,
};
/// Adds spacing around content.
///
/// The spacing can be specified for each side individually, or for all sides at
/// once by specifying a positional argument.
///
/// # Example
/// ```example
/// #set align(center)
///
/// #pad(x: 16pt, image("typing.jpg"))
/// _Typing speeds can be
/// measured in words per minute._
/// ```
#[elem(title = "Padding", Show)]
pub struct PadElem {
/// The padding at the left side.
#[parse(
let all = args.named("rest")?.or(args.find()?);
let x = args.named("x")?.or(all);
let y = args.named("y")?.or(all);
args.named("left")?.or(x)
)]
pub left: Rel<Length>,
/// The padding at the top side.
#[parse(args.named("top")?.or(y))]
pub top: Rel<Length>,
/// The padding at the right side.
#[parse(args.named("right")?.or(x))]
pub right: Rel<Length>,
/// The padding at the bottom side.
#[parse(args.named("bottom")?.or(y))]
pub bottom: Rel<Length>,
/// A shorthand to set `left` and `right` to the same value.
#[external]
pub x: Rel<Length>,
/// A shorthand to set `top` and `bottom` to the same value.
#[external]
pub y: Rel<Length>,
/// A shorthand to set all four sides to the same value.
#[external]
pub rest: Rel<Length>,
/// The content to pad at the sides.
#[required]
pub body: Content,
}
impl Show for Packed<PadElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::multi_layouter(self.clone(), layout_pad)
.pack()
.spanned(self.span()))
}
}
/// Layout the padded content.
#[typst_macros::time(span = elem.span())]
fn layout_pad(
pub fn layout_pad(
elem: &Packed<PadElem>,
engine: &mut Engine,
locator: Locator,
@ -90,7 +26,7 @@ fn layout_pad(
let pod = regions.map(&mut backlog, |size| shrink(size, &padding));
// Layout child into padded regions.
let mut fragment = layout_fragment(engine, &elem.body, locator, styles, pod)?;
let mut fragment = crate::layout_fragment(engine, &elem.body, locator, styles, pod)?;
for frame in &mut fragment {
grow(frame, &padding);
@ -100,13 +36,13 @@ fn layout_pad(
}
/// Shrink a region size by an inset relative to the size itself.
pub(crate) fn shrink(size: Size, inset: &Sides<Rel<Abs>>) -> Size {
pub fn shrink(size: Size, inset: &Sides<Rel<Abs>>) -> Size {
size - inset.sum_by_axis().relative_to(size)
}
/// Shrink the components of possibly multiple `Regions` by an inset relative to
/// the regions themselves.
pub(crate) fn shrink_multiple(
pub fn shrink_multiple(
size: &mut Size,
full: &mut Abs,
backlog: &mut [Abs],
@ -141,7 +77,7 @@ pub(crate) fn shrink_multiple(
/// <=> w - p.rel * w - p.abs = s
/// <=> (1 - p.rel) * w = s + p.abs
/// <=> w = (s + p.abs) / (1 - p.rel)
pub(crate) fn grow(frame: &mut Frame, inset: &Sides<Rel<Abs>>) {
pub fn grow(frame: &mut Frame, inset: &Sides<Rel<Abs>>) {
// Apply the padding inversely such that the grown size padded
// yields the frame's size.
let padded = frame

View File

@ -1,9 +1,9 @@
use std::collections::HashSet;
use crate::foundations::StyleChain;
use crate::introspection::{Locator, SplitLocator, Tag, TagElem};
use crate::layout::{PagebreakElem, Parity};
use crate::realize::Pair;
use typst_library::foundations::StyleChain;
use typst_library::introspection::{Locator, SplitLocator, Tag, TagElem};
use typst_library::layout::{PagebreakElem, Parity};
use typst_library::routines::Pair;
/// An item in page layout.
pub enum Item<'a> {

View File

@ -1,8 +1,9 @@
use typst_library::diag::SourceResult;
use typst_library::engine::Engine;
use typst_library::introspection::{ManualPageCounter, Tag};
use typst_library::layout::{Frame, FrameItem, Page, Point};
use super::LayoutedPage;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::introspection::{ManualPageCounter, Tag};
use crate::layout::{Frame, FrameItem, Page, Point};
/// Piece together the inner page frame and the marginals. We can only do this
/// at the very end because inside/outside margins require knowledge of the

View File

@ -5,25 +5,25 @@ mod finalize;
mod run;
use comemo::{Tracked, TrackedMut};
use typst_library::diag::SourceResult;
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Content, StyleChain};
use typst_library::introspection::{
Introspector, Locator, ManualPageCounter, SplitLocator, TagElem,
};
use typst_library::layout::{FrameItem, Page, Point};
use typst_library::model::{Document, DocumentInfo};
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
use typst_library::World;
use self::collect::{collect, Item};
use self::finalize::finalize;
use self::run::{layout_blank_page, layout_page_run, LayoutedPage};
use crate::diag::SourceResult;
use crate::engine::{Engine, Route, Sink, Traced};
use crate::foundations::{Content, StyleChain};
use crate::introspection::{
Introspector, Locator, ManualPageCounter, SplitLocator, TagElem,
};
use crate::layout::{FrameItem, Page, Point};
use crate::model::{Document, DocumentInfo};
use crate::realize::{realize, Arenas, Pair, RealizationKind};
use crate::World;
/// Layout content into a document.
///
/// This first performs root-level realization and then lays out the resulting
/// elements. In contrast to [`layout_fragment`](crate::layout::layout_fragment),
/// elements. In contrast to [`layout_fragment`](crate::layout_fragment),
/// this does not take regions since the regions are defined by the page
/// configuration in the content and style chain.
#[typst_macros::time(name = "document")]
@ -33,6 +33,7 @@ pub fn layout_document(
styles: StyleChain,
) -> SourceResult<Document> {
layout_document_impl(
engine.routines,
engine.world,
engine.introspector,
engine.traced,
@ -45,7 +46,9 @@ pub fn layout_document(
/// The internal implementation of `layout_document`.
#[comemo::memoize]
#[allow(clippy::too_many_arguments)]
fn layout_document_impl(
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
traced: Tracked<Traced>,
@ -56,6 +59,7 @@ fn layout_document_impl(
) -> SourceResult<Document> {
let mut locator = Locator::root().split();
let mut engine = Engine {
routines,
world,
introspector,
traced,
@ -70,7 +74,7 @@ fn layout_document_impl(
let arenas = Arenas::default();
let mut info = DocumentInfo::default();
let mut children = realize(
let mut children = (engine.routines.realize)(
RealizationKind::Root(&mut info),
&mut engine,
&mut locator,

View File

@ -1,22 +1,25 @@
use comemo::{Track, Tracked, TrackedMut};
use crate::diag::SourceResult;
use crate::engine::{Engine, Route, Sink, Traced};
use crate::foundations::{Content, NativeElement, Resolve, Smart, StyleChain, Styles};
use crate::introspection::{
use typst_library::diag::SourceResult;
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{
Content, NativeElement, Resolve, Smart, StyleChain, Styles,
};
use typst_library::introspection::{
Counter, CounterDisplayElem, CounterKey, Introspector, Locator, LocatorLink, TagElem,
};
use crate::layout::{
layout_flow, layout_frame, Abs, AlignElem, Alignment, Axes, Binding, ColumnsElem,
Dir, Frame, HAlignment, Length, OuterVAlignment, PageElem, Paper, Region, Regions,
Rel, Sides, Size, VAlignment,
use typst_library::layout::{
Abs, AlignElem, Alignment, Axes, Binding, ColumnsElem, Dir, Frame, HAlignment,
Length, OuterVAlignment, PageElem, Paper, Region, Regions, Rel, Sides, Size,
VAlignment,
};
use crate::model::Numbering;
use crate::realize::Pair;
use crate::text::TextElem;
use crate::utils::Numeric;
use crate::visualize::Paint;
use crate::World;
use typst_library::model::Numbering;
use typst_library::routines::{Pair, Routines};
use typst_library::text::TextElem;
use typst_library::visualize::Paint;
use typst_library::World;
use typst_utils::Numeric;
use crate::flow::layout_flow;
/// A mostly finished layout for one page. Needs only knowledge of its exact
/// page number to be finalized into a `Page`. (Because the margins can depend
@ -54,6 +57,7 @@ pub fn layout_page_run(
initial: StyleChain,
) -> SourceResult<Vec<LayoutedPage>> {
layout_page_run_impl(
engine.routines,
engine.world,
engine.introspector,
engine.traced,
@ -69,6 +73,7 @@ pub fn layout_page_run(
#[comemo::memoize]
#[allow(clippy::too_many_arguments)]
fn layout_page_run_impl(
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
traced: Tracked<Traced>,
@ -81,6 +86,7 @@ fn layout_page_run_impl(
let link = LocatorLink::new(locator);
let mut locator = Locator::link(&link).split();
let mut engine = Engine {
routines,
world,
introspector,
traced,
@ -177,7 +183,7 @@ fn layout_page_run_impl(
let mut layout_marginal = |content: &Option<Content>, area, align| {
let Some(content) = content else { return Ok(None) };
let aligned = content.clone().styled(AlignElem::set_alignment(align));
layout_frame(
crate::layout_frame(
&mut engine,
&aligned,
locator.next(&content.span()),

View File

@ -0,0 +1,60 @@
use typst_library::diag::{bail, SourceResult};
use typst_library::engine::Engine;
use typst_library::foundations::{Packed, Resolve, StyleChain};
use typst_library::introspection::Locator;
use typst_library::layout::{
Abs, AlignElem, Axes, Frame, Point, Region, RepeatElem, Size,
};
use typst_utils::Numeric;
/// Layout the repeated content.
#[typst_macros::time(span = elem.span())]
pub fn layout_repeat(
elem: &Packed<RepeatElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let pod = Region::new(region.size, Axes::new(false, false));
let piece = crate::layout_frame(engine, &elem.body, locator, styles, pod)?;
let size = Size::new(region.size.x, piece.height());
if !size.is_finite() {
bail!(elem.span(), "repeat with no size restrictions");
}
let mut frame = Frame::soft(size);
if piece.has_baseline() {
frame.set_baseline(piece.baseline());
}
let mut gap = elem.gap(styles).resolve(styles);
let fill = region.size.x;
let width = piece.width();
// count * width + (count - 1) * gap = fill, but count is an integer so
// we need to round down and get the remainder.
let count = ((fill + gap) / (width + gap)).floor();
let remaining = (fill + gap) % (width + gap);
let justify = elem.justify(styles);
if justify {
gap += remaining / (count - 1.0);
}
let align = AlignElem::alignment_in(styles).resolve(styles);
let mut offset = Abs::zero();
if count == 1.0 || !justify {
offset += align.x.position(remaining);
}
if width > Abs::zero() {
for _ in 0..(count as usize).min(1000) {
frame.push_frame(Point::with_x(offset), piece.clone());
offset += piece.width() + gap;
}
}
Ok(frame)
}

View File

@ -1,469 +1,319 @@
use std::f64::consts::SQRT_2;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{
elem, Cast, Content, NativeElement, Packed, Show, Smart, StyleChain,
use kurbo::ParamCurveExtrema;
use typst_library::diag::{bail, SourceResult};
use typst_library::engine::Engine;
use typst_library::foundations::{Content, Packed, Resolve, Smart, StyleChain};
use typst_library::introspection::Locator;
use typst_library::layout::{
Abs, Axes, Corner, Corners, Frame, FrameItem, Length, Point, Ratio, Region, Rel,
Sides, Size,
};
use crate::introspection::Locator;
use crate::layout::{
layout_frame, Abs, Axes, BlockElem, Corner, Corners, Frame, FrameItem, Length, Point,
Ratio, Region, Rel, Sides, Size, Sizing,
use typst_library::visualize::{
CircleElem, EllipseElem, FillRule, FixedStroke, Geometry, LineElem, Paint, Path,
PathElem, PathVertex, PolygonElem, RectElem, Shape, SquareElem, Stroke,
};
use crate::syntax::Span;
use crate::utils::Get;
use crate::visualize::{FixedStroke, Paint, Path, Stroke};
use typst_syntax::Span;
use typst_utils::{Get, Numeric};
/// A rectangle with optional content.
///
/// # Example
/// ```example
/// // Without content.
/// #rect(width: 35%, height: 30pt)
///
/// // With content.
/// #rect[
/// Automatically sized \
/// to fit the content.
/// ]
/// ```
#[elem(title = "Rectangle", Show)]
pub struct RectElem {
/// The rectangle's width, relative to its parent container.
pub width: Smart<Rel<Length>>,
/// Layout the line.
#[typst_macros::time(span = elem.span())]
pub fn layout_line(
elem: &Packed<LineElem>,
_: &mut Engine,
_: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let resolve = |axes: Axes<Rel<Abs>>| axes.zip_map(region.size, Rel::relative_to);
let start = resolve(elem.start(styles));
let delta = elem.end(styles).map(|end| resolve(end) - start).unwrap_or_else(|| {
let length = elem.length(styles);
let angle = elem.angle(styles);
let x = angle.cos() * length;
let y = angle.sin() * length;
resolve(Axes::new(x, y))
});
/// The rectangle's height, relative to its parent container.
pub height: Sizing,
let stroke = elem.stroke(styles).unwrap_or_default();
let size = start.max(start + delta).max(Size::zero());
/// How to fill the rectangle.
///
/// When setting a fill, the default stroke disappears. To create a
/// rectangle with both fill and stroke, you have to configure both.
///
/// ```example
/// #rect(fill: blue)
/// ```
pub fill: Option<Paint>,
/// How to stroke the rectangle. This can be:
///
/// - `{none}` to disable stroking
/// - `{auto}` for a stroke of `{1pt + black}` if and if only if no fill is
/// given.
/// - Any kind of [stroke]
/// - A dictionary describing the stroke for each side individually. The
/// dictionary can contain the following keys in order of precedence:
/// - `top`: The top stroke.
/// - `right`: The right stroke.
/// - `bottom`: The bottom stroke.
/// - `left`: The left stroke.
/// - `x`: The horizontal stroke.
/// - `y`: The vertical stroke.
/// - `rest`: The stroke on all sides except those for which the
/// dictionary explicitly sets a size.
///
/// ```example
/// #stack(
/// dir: ltr,
/// spacing: 1fr,
/// rect(stroke: red),
/// rect(stroke: 2pt),
/// rect(stroke: 2pt + red),
/// )
/// ```
#[resolve]
#[fold]
pub stroke: Smart<Sides<Option<Option<Stroke>>>>,
/// How much to round the rectangle's corners, relative to the minimum of
/// the width and height divided by two. This can be:
///
/// - A relative length for a uniform corner radius.
/// - A dictionary: With a dictionary, the stroke for each side can be set
/// individually. The dictionary can contain the following keys in order
/// of precedence:
/// - `top-left`: The top-left corner radius.
/// - `top-right`: The top-right corner radius.
/// - `bottom-right`: The bottom-right corner radius.
/// - `bottom-left`: The bottom-left corner radius.
/// - `left`: The top-left and bottom-left corner radii.
/// - `top`: The top-left and top-right corner radii.
/// - `right`: The top-right and bottom-right corner radii.
/// - `bottom`: The bottom-left and bottom-right corner radii.
/// - `rest`: The radii for all corners except those for which the
/// dictionary explicitly sets a size.
///
/// ```example
/// #set rect(stroke: 4pt)
/// #rect(
/// radius: (
/// left: 5pt,
/// top-right: 20pt,
/// bottom-right: 10pt,
/// ),
/// stroke: (
/// left: red,
/// top: yellow,
/// right: green,
/// bottom: blue,
/// ),
/// )
/// ```
#[resolve]
#[fold]
pub radius: Corners<Option<Rel<Length>>>,
/// How much to pad the rectangle's content.
/// See the [box's documentation]($box.outset) for more details.
#[resolve]
#[fold]
#[default(Sides::splat(Some(Abs::pt(5.0).into())))]
pub inset: Sides<Option<Rel<Length>>>,
/// How much to expand the rectangle's size without affecting the layout.
/// See the [box's documentation]($box.outset) for more details.
#[resolve]
#[fold]
pub outset: Sides<Option<Rel<Length>>>,
/// The content to place into the rectangle.
///
/// When this is omitted, the rectangle takes on a default size of at most
/// `{45pt}` by `{30pt}`.
#[positional]
#[borrowed]
pub body: Option<Content>,
}
impl Show for Packed<RectElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(
self.clone(),
|elem, engine, locator, styles, region| {
layout_shape(
engine,
locator,
styles,
region,
ShapeKind::Rect,
elem.body(styles),
elem.fill(styles),
elem.stroke(styles),
elem.inset(styles),
elem.outset(styles),
elem.radius(styles),
elem.span(),
)
},
)
.with_width(self.width(styles))
.with_height(self.height(styles))
.pack()
.spanned(self.span()))
if !size.is_finite() {
bail!(elem.span(), "cannot create line with infinite length");
}
let mut frame = Frame::soft(size);
let shape = Geometry::Line(delta.to_point()).stroked(stroke);
frame.push(start.to_point(), FrameItem::Shape(shape, elem.span()));
Ok(frame)
}
/// A square with optional content.
///
/// # Example
/// ```example
/// // Without content.
/// #square(size: 40pt)
///
/// // With content.
/// #square[
/// Automatically \
/// sized to fit.
/// ]
/// ```
#[elem(Show)]
pub struct SquareElem {
/// The square's side length. This is mutually exclusive with `width` and
/// `height`.
#[external]
pub size: Smart<Length>,
/// Layout the path.
#[typst_macros::time(span = elem.span())]
pub fn layout_path(
elem: &Packed<PathElem>,
_: &mut Engine,
_: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let resolve = |axes: Axes<Rel<Length>>| {
axes.resolve(styles).zip_map(region.size, Rel::relative_to).to_point()
};
/// The square's width. This is mutually exclusive with `size` and `height`.
///
/// In contrast to `size`, this can be relative to the parent container's
/// width.
#[parse(
let size = args.named::<Smart<Length>>("size")?.map(|s| s.map(Rel::from));
match size {
None => args.named("width")?,
size => size,
}
)]
pub width: Smart<Rel<Length>>,
let vertices = elem.vertices();
let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect();
/// The square's height. This is mutually exclusive with `size` and `width`.
///
/// In contrast to `size`, this can be relative to the parent container's
/// height.
#[parse(match size {
None => args.named("height")?,
size => size.map(Into::into),
})]
pub height: Sizing,
/// How to fill the square. See the [rectangle's documentation]($rect.fill)
/// for more details.
pub fill: Option<Paint>,
/// How to stroke the square. See the
/// [rectangle's documentation]($rect.stroke) for more details.
#[resolve]
#[fold]
pub stroke: Smart<Sides<Option<Option<Stroke>>>>,
/// How much to round the square's corners. See the
/// [rectangle's documentation]($rect.radius) for more details.
#[resolve]
#[fold]
pub radius: Corners<Option<Rel<Length>>>,
/// How much to pad the square's content. See the
/// [box's documentation]($box.inset) for more details.
#[resolve]
#[fold]
#[default(Sides::splat(Some(Abs::pt(5.0).into())))]
pub inset: Sides<Option<Rel<Length>>>,
/// How much to expand the square's size without affecting the layout. See
/// the [box's documentation]($box.outset) for more details.
#[resolve]
#[fold]
pub outset: Sides<Option<Rel<Length>>>,
/// The content to place into the square. The square expands to fit this
/// content, keeping the 1-1 aspect ratio.
///
/// When this is omitted, the square takes on a default size of at most
/// `{30pt}`.
#[positional]
#[borrowed]
pub body: Option<Content>,
}
impl Show for Packed<SquareElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(
self.clone(),
|elem, engine, locator, styles, regions| {
layout_shape(
engine,
locator,
styles,
regions,
ShapeKind::Square,
elem.body(styles),
elem.fill(styles),
elem.stroke(styles),
elem.inset(styles),
elem.outset(styles),
elem.radius(styles),
elem.span(),
)
},
)
.with_width(self.width(styles))
.with_height(self.height(styles))
.pack()
.spanned(self.span()))
let mut size = Size::zero();
if points.is_empty() {
return Ok(Frame::soft(size));
}
}
/// An ellipse with optional content.
///
/// # Example
/// ```example
/// // Without content.
/// #ellipse(width: 35%, height: 30pt)
///
/// // With content.
/// #ellipse[
/// #set align(center)
/// Automatically sized \
/// to fit the content.
/// ]
/// ```
#[elem(Show)]
pub struct EllipseElem {
/// The ellipse's width, relative to its parent container.
pub width: Smart<Rel<Length>>,
// Only create a path if there are more than zero points.
// Construct a closed path given all points.
let mut path = Path::new();
path.move_to(points[0]);
/// The ellipse's height, relative to its parent container.
pub height: Sizing,
let mut add_cubic = |from_point: Point,
to_point: Point,
from: PathVertex,
to: PathVertex| {
let from_control_point = resolve(from.control_point_from()) + from_point;
let to_control_point = resolve(to.control_point_to()) + to_point;
path.cubic_to(from_control_point, to_control_point, to_point);
/// How to fill the ellipse. See the [rectangle's documentation]($rect.fill)
/// for more details.
pub fill: Option<Paint>,
let p0 = kurbo::Point::new(from_point.x.to_raw(), from_point.y.to_raw());
let p1 = kurbo::Point::new(
from_control_point.x.to_raw(),
from_control_point.y.to_raw(),
);
let p2 =
kurbo::Point::new(to_control_point.x.to_raw(), to_control_point.y.to_raw());
let p3 = kurbo::Point::new(to_point.x.to_raw(), to_point.y.to_raw());
let extrema = kurbo::CubicBez::new(p0, p1, p2, p3).bounding_box();
size.x.set_max(Abs::raw(extrema.x1));
size.y.set_max(Abs::raw(extrema.y1));
};
/// How to stroke the ellipse. See the
/// [rectangle's documentation]($rect.stroke) for more details.
#[resolve]
#[fold]
pub stroke: Smart<Option<Stroke>>,
for (vertex_window, point_window) in vertices.windows(2).zip(points.windows(2)) {
let from = vertex_window[0];
let to = vertex_window[1];
let from_point = point_window[0];
let to_point = point_window[1];
/// How much to pad the ellipse's content. See the
/// [box's documentation]($box.inset) for more details.
#[resolve]
#[fold]
#[default(Sides::splat(Some(Abs::pt(5.0).into())))]
pub inset: Sides<Option<Rel<Length>>>,
/// How much to expand the ellipse's size without affecting the layout. See
/// the [box's documentation]($box.outset) for more details.
#[resolve]
#[fold]
pub outset: Sides<Option<Rel<Length>>>,
/// The content to place into the ellipse.
///
/// When this is omitted, the ellipse takes on a default size of at most
/// `{45pt}` by `{30pt}`.
#[positional]
#[borrowed]
pub body: Option<Content>,
}
impl Show for Packed<EllipseElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(
self.clone(),
|elem, engine, locator, styles, regions| {
layout_shape(
engine,
locator,
styles,
regions,
ShapeKind::Ellipse,
elem.body(styles),
elem.fill(styles),
elem.stroke(styles).map(|s| Sides::splat(Some(s))),
elem.inset(styles),
elem.outset(styles),
Corners::splat(None),
elem.span(),
)
},
)
.with_width(self.width(styles))
.with_height(self.height(styles))
.pack()
.spanned(self.span()))
add_cubic(from_point, to_point, from, to);
}
if elem.closed(styles) {
let from = *vertices.last().unwrap(); // We checked that we have at least one element.
let to = vertices[0];
let from_point = *points.last().unwrap();
let to_point = points[0];
add_cubic(from_point, to_point, from, to);
path.close_path();
}
// Prepare fill and stroke.
let fill = elem.fill(styles);
let fill_rule = elem.fill_rule(styles);
let stroke = match elem.stroke(styles) {
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
Smart::Auto => None,
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
};
let mut frame = Frame::soft(size);
let shape = Shape {
geometry: Geometry::Path(path),
stroke,
fill,
fill_rule,
};
frame.push(Point::zero(), FrameItem::Shape(shape, elem.span()));
Ok(frame)
}
/// A circle with optional content.
///
/// # Example
/// ```example
/// // Without content.
/// #circle(radius: 25pt)
///
/// // With content.
/// #circle[
/// #set align(center + horizon)
/// Automatically \
/// sized to fit.
/// ]
/// ```
#[elem(Show)]
pub struct CircleElem {
/// The circle's radius. This is mutually exclusive with `width` and
/// `height`.
#[external]
pub radius: Length,
/// Layout the polygon.
#[typst_macros::time(span = elem.span())]
pub fn layout_polygon(
elem: &Packed<PolygonElem>,
_: &mut Engine,
_: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let points: Vec<Point> = elem
.vertices()
.iter()
.map(|c| c.resolve(styles).zip_map(region.size, Rel::relative_to).to_point())
.collect();
/// The circle's width. This is mutually exclusive with `radius` and
/// `height`.
///
/// In contrast to `radius`, this can be relative to the parent container's
/// width.
#[parse(
let size = args
.named::<Smart<Length>>("radius")?
.map(|s| s.map(|r| 2.0 * Rel::from(r)));
match size {
None => args.named("width")?,
size => size,
}
)]
pub width: Smart<Rel<Length>>,
let size = points.iter().fold(Point::zero(), |max, c| c.max(max)).to_size();
if !size.is_finite() {
bail!(elem.span(), "cannot create polygon with infinite size");
}
/// The circle's height. This is mutually exclusive with `radius` and
/// `width`.
///
/// In contrast to `radius`, this can be relative to the parent container's
/// height.
#[parse(match size {
None => args.named("height")?,
size => size.map(Into::into),
})]
pub height: Sizing,
let mut frame = Frame::hard(size);
/// How to fill the circle. See the [rectangle's documentation]($rect.fill)
/// for more details.
pub fill: Option<Paint>,
// Only create a path if there are more than zero points.
if points.is_empty() {
return Ok(frame);
}
/// How to stroke the circle. See the
/// [rectangle's documentation]($rect.stroke) for more details.
#[resolve]
#[fold]
#[default(Smart::Auto)]
pub stroke: Smart<Option<Stroke>>,
// Prepare fill and stroke.
let fill = elem.fill(styles);
let fill_rule = elem.fill_rule(styles);
let stroke = match elem.stroke(styles) {
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
Smart::Auto => None,
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
};
/// How much to pad the circle's content. See the
/// [box's documentation]($box.inset) for more details.
#[resolve]
#[fold]
#[default(Sides::splat(Some(Abs::pt(5.0).into())))]
pub inset: Sides<Option<Rel<Length>>>,
// Construct a closed path given all points.
let mut path = Path::new();
path.move_to(points[0]);
for &point in &points[1..] {
path.line_to(point);
}
path.close_path();
/// How much to expand the circle's size without affecting the layout. See
/// the [box's documentation]($box.outset) for more details.
#[resolve]
#[fold]
pub outset: Sides<Option<Rel<Length>>>,
/// The content to place into the circle. The circle expands to fit this
/// content, keeping the 1-1 aspect ratio.
#[positional]
#[borrowed]
pub body: Option<Content>,
let shape = Shape {
geometry: Geometry::Path(path),
stroke,
fill,
fill_rule,
};
frame.push(Point::zero(), FrameItem::Shape(shape, elem.span()));
Ok(frame)
}
impl Show for Packed<CircleElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(
self.clone(),
|elem, engine, locator, styles, regions| {
layout_shape(
engine,
locator,
styles,
regions,
ShapeKind::Circle,
elem.body(styles),
elem.fill(styles),
elem.stroke(styles).map(|s| Sides::splat(Some(s))),
elem.inset(styles),
elem.outset(styles),
Corners::splat(None),
elem.span(),
)
},
)
.with_width(self.width(styles))
.with_height(self.height(styles))
.pack()
.spanned(self.span()))
/// Lay out the rectangle.
#[typst_macros::time(span = elem.span())]
pub fn layout_rect(
elem: &Packed<RectElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
layout_shape(
engine,
locator,
styles,
region,
ShapeKind::Rect,
elem.body(styles),
elem.fill(styles),
elem.stroke(styles),
elem.inset(styles),
elem.outset(styles),
elem.radius(styles),
elem.span(),
)
}
/// Lay out the square.
#[typst_macros::time(span = elem.span())]
pub fn layout_square(
elem: &Packed<SquareElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
layout_shape(
engine,
locator,
styles,
region,
ShapeKind::Square,
elem.body(styles),
elem.fill(styles),
elem.stroke(styles),
elem.inset(styles),
elem.outset(styles),
elem.radius(styles),
elem.span(),
)
}
/// Lay out the ellipse.
#[typst_macros::time(span = elem.span())]
pub fn layout_ellipse(
elem: &Packed<EllipseElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
layout_shape(
engine,
locator,
styles,
region,
ShapeKind::Ellipse,
elem.body(styles),
elem.fill(styles),
elem.stroke(styles).map(|s| Sides::splat(Some(s))),
elem.inset(styles),
elem.outset(styles),
Corners::splat(None),
elem.span(),
)
}
/// Lay out the circle.
#[typst_macros::time(span = elem.span())]
pub fn layout_circle(
elem: &Packed<CircleElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
layout_shape(
engine,
locator,
styles,
region,
ShapeKind::Circle,
elem.body(styles),
elem.fill(styles),
elem.stroke(styles).map(|s| Sides::splat(Some(s))),
elem.inset(styles),
elem.outset(styles),
Corners::splat(None),
elem.span(),
)
}
/// A category of shape.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
enum ShapeKind {
/// A rectangle with equal side lengths.
Square,
/// A quadrilateral with four right angles.
Rect,
/// An ellipse with coinciding foci.
Circle,
/// A curve around two focal points.
Ellipse,
}
impl ShapeKind {
/// Whether this shape kind is curvy.
fn is_round(self) -> bool {
matches!(self, Self::Circle | Self::Ellipse)
}
/// Whether this shape kind has equal side length.
fn is_quadratic(self) -> bool {
matches!(self, Self::Square | Self::Circle)
}
}
/// Layout a shape.
#[typst_macros::time(span = span)]
#[allow(clippy::too_many_arguments)]
fn layout_shape(
engine: &mut Engine,
@ -491,23 +341,23 @@ fn layout_shape(
// Take the inset, if any, into account.
let mut pod = region;
if has_inset {
pod.size = crate::layout::shrink(region.size, &inset);
pod.size = crate::pad::shrink(region.size, &inset);
}
// Layout the child.
frame = layout_frame(engine, child, locator.relayout(), styles, pod)?;
frame = crate::layout_frame(engine, child, locator.relayout(), styles, pod)?;
// If the child is a square or circle, relayout with full expansion into
// square region to make sure the result is really quadratic.
if kind.is_quadratic() {
let length = frame.size().max_by_side().min(pod.size.min_by_side());
let quad_pod = Region::new(Size::splat(length), Axes::splat(true));
frame = layout_frame(engine, child, locator, styles, quad_pod)?;
frame = crate::layout_frame(engine, child, locator, styles, quad_pod)?;
}
// Apply the inset.
if has_inset {
crate::layout::grow(&mut frame, &inset);
crate::pad::grow(&mut frame, &inset);
}
} else {
// The default size that a shape takes on if it has no child and
@ -535,10 +385,16 @@ fn layout_shape(
let outset = outset.unwrap_or_default().relative_to(frame.size());
let size = frame.size() + outset.sum_by_axis();
let pos = Point::new(-outset.left, -outset.top);
let shape = ellipse(size, fill, stroke.left);
let shape = Shape {
geometry: Geometry::Path(Path::ellipse(size)),
fill,
stroke: stroke.left,
fill_rule: FillRule::default(),
};
frame.prepend(pos, FrameItem::Shape(shape, span));
} else {
frame.fill_and_stroke(
fill_and_stroke(
&mut frame,
fill,
&stroke,
&outset.unwrap_or_default(),
@ -551,128 +407,8 @@ fn layout_shape(
Ok(frame)
}
/// A category of shape.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum ShapeKind {
/// A rectangle with equal side lengths.
Square,
/// A quadrilateral with four right angles.
Rect,
/// An ellipse with coinciding foci.
Circle,
/// A curve around two focal points.
Ellipse,
}
impl ShapeKind {
/// Whether this shape kind is curvy.
fn is_round(self) -> bool {
matches!(self, Self::Circle | Self::Ellipse)
}
/// Whether this shape kind has equal side length.
fn is_quadratic(self) -> bool {
matches!(self, Self::Square | Self::Circle)
}
}
/// A geometric shape with optional fill and stroke.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Shape {
/// The shape's geometry.
pub geometry: Geometry,
/// The shape's background fill.
pub fill: Option<Paint>,
/// The shape's fill rule.
pub fill_rule: FillRule,
/// The shape's border stroke.
pub stroke: Option<FixedStroke>,
}
/// A path filling rule.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum FillRule {
/// Specifies that "inside" is computed by a non-zero sum of signed edge crossings.
#[default]
NonZero,
/// Specifies that "inside" is computed by an odd number of edge crossings.
EvenOdd,
}
/// A shape's geometry.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Geometry {
/// A line to a point (relative to its position).
Line(Point),
/// A rectangle with its origin in the topleft corner.
Rect(Size),
/// A bezier path.
Path(Path),
}
impl Geometry {
/// Fill the geometry without a stroke.
pub fn filled(self, fill: Paint) -> Shape {
Shape {
geometry: self,
fill: Some(fill),
fill_rule: FillRule::default(),
stroke: None,
}
}
/// Stroke the geometry without a fill.
pub fn stroked(self, stroke: FixedStroke) -> Shape {
Shape {
geometry: self,
fill: None,
fill_rule: FillRule::default(),
stroke: Some(stroke),
}
}
/// The bounding box of the geometry.
pub fn bbox_size(&self) -> Size {
match self {
Self::Line(line) => Size::new(line.x, line.y),
Self::Rect(s) => *s,
Self::Path(p) => p.bbox_size(),
}
}
}
/// Produce a shape that approximates an axis-aligned ellipse.
pub(crate) fn ellipse(
size: Size,
fill: Option<Paint>,
stroke: Option<FixedStroke>,
) -> Shape {
// https://stackoverflow.com/a/2007782
let z = Abs::zero();
let rx = size.x / 2.0;
let ry = size.y / 2.0;
let m = 0.551784;
let mx = m * rx;
let my = m * ry;
let point = |x, y| Point::new(x + rx, y + ry);
let mut path = Path::new();
path.move_to(point(-rx, z));
path.cubic_to(point(-rx, -my), point(-mx, -ry), point(z, -ry));
path.cubic_to(point(mx, -ry), point(rx, -my), point(rx, z));
path.cubic_to(point(rx, my), point(mx, ry), point(z, ry));
path.cubic_to(point(-mx, ry), point(-rx, my), point(-rx, z));
Shape {
geometry: Geometry::Path(path),
stroke,
fill,
fill_rule: FillRule::default(),
}
}
/// Creates a new rectangle as a path.
pub(crate) fn clip_rect(
pub fn clip_rect(
size: Size,
radius: &Corners<Rel<Abs>>,
stroke: &Sides<Option<FixedStroke>>,
@ -708,11 +444,30 @@ pub(crate) fn clip_rect(
path
}
/// Add a fill and stroke with optional radius and outset to the frame.
pub fn fill_and_stroke(
frame: &mut Frame,
fill: Option<Paint>,
stroke: &Sides<Option<FixedStroke>>,
outset: &Sides<Rel<Abs>>,
radius: &Corners<Rel<Abs>>,
span: Span,
) {
let outset = outset.relative_to(frame.size());
let size = frame.size() + outset.sum_by_axis();
let pos = Point::new(-outset.left, -outset.top);
frame.prepend_multiple(
styled_rect(size, radius, fill, stroke)
.into_iter()
.map(|x| (pos, FrameItem::Shape(x, span))),
);
}
/// Create a styled rectangle with shapes.
/// - use rect primitive for simple rectangles
/// - stroke sides if possible
/// - use fill for sides for best looks
pub(crate) fn styled_rect(
pub fn styled_rect(
size: Size,
radius: &Corners<Rel<Abs>>,
fill: Option<Paint>,

View File

@ -1,99 +1,17 @@
use std::fmt::{self, Debug, Formatter};
use typst_library::diag::{bail, SourceResult};
use typst_library::engine::Engine;
use typst_library::foundations::{Content, Packed, Resolve, StyleChain, StyledElem};
use typst_library::introspection::{Locator, SplitLocator};
use typst_library::layout::{
Abs, AlignElem, Axes, Axis, Dir, FixedAlignment, Fr, Fragment, Frame, HElem, Point,
Regions, Size, Spacing, StackChild, StackElem, VElem,
};
use typst_syntax::Span;
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, Content, NativeElement, Packed, Resolve, Show, StyleChain, StyledElem,
};
use crate::introspection::{Locator, SplitLocator};
use crate::layout::{
layout_fragment, Abs, AlignElem, Axes, Axis, BlockElem, Dir, FixedAlignment, Fr,
Fragment, Frame, HElem, Point, Regions, Size, Spacing, VElem,
};
use crate::utils::{Get, Numeric};
/// Arranges content and spacing horizontally or vertically.
///
/// The stack places a list of items along an axis, with optional spacing
/// between each item.
///
/// # Example
/// ```example
/// #stack(
/// dir: ttb,
/// rect(width: 40pt),
/// rect(width: 120pt),
/// rect(width: 90pt),
/// )
/// ```
#[elem(Show)]
pub struct StackElem {
/// The direction along which the items are stacked. Possible values are:
///
/// - `{ltr}`: Left to right.
/// - `{rtl}`: Right to left.
/// - `{ttb}`: Top to bottom.
/// - `{btt}`: Bottom to top.
///
/// You can use the `start` and `end` methods to obtain the initial and
/// final points (respectively) of a direction, as `alignment`. You can also
/// use the `axis` method to determine whether a direction is
/// `{"horizontal"}` or `{"vertical"}`. The `inv` method returns a
/// direction's inverse direction.
///
/// For example, `{ttb.start()}` is `top`, `{ttb.end()}` is `bottom`,
/// `{ttb.axis()}` is `{"vertical"}` and `{ttb.inv()}` is equal to `btt`.
#[default(Dir::TTB)]
pub dir: Dir,
/// Spacing to insert between items where no explicit spacing was provided.
pub spacing: Option<Spacing>,
/// The children to stack along the axis.
#[variadic]
pub children: Vec<StackChild>,
}
impl Show for Packed<StackElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::multi_layouter(self.clone(), layout_stack)
.pack()
.spanned(self.span()))
}
}
/// A child of a stack element.
#[derive(Clone, PartialEq, Hash)]
pub enum StackChild {
/// Spacing between other children.
Spacing(Spacing),
/// Arbitrary block-level content.
Block(Content),
}
impl Debug for StackChild {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Spacing(kind) => kind.fmt(f),
Self::Block(block) => block.fmt(f),
}
}
}
cast! {
StackChild,
self => match self {
Self::Spacing(spacing) => spacing.into_value(),
Self::Block(content) => content.into_value(),
},
v: Spacing => Self::Spacing(v),
v: Content => Self::Block(v),
}
use typst_utils::{Get, Numeric};
/// Layout the stack.
#[typst_macros::time(span = elem.span())]
fn layout_stack(
pub fn layout_stack(
elem: &Packed<StackElem>,
engine: &mut Engine,
locator: Locator,
@ -257,7 +175,7 @@ impl<'a> StackLayouter<'a> {
}
.resolve(styles);
let fragment = layout_fragment(
let fragment = crate::layout_fragment(
engine,
block,
self.locator.next(&block.span()),

View File

@ -0,0 +1,246 @@
use once_cell::unsync::Lazy;
use typst_library::diag::{bail, SourceResult};
use typst_library::engine::Engine;
use typst_library::foundations::{Content, Packed, Resolve, Smart, StyleChain};
use typst_library::introspection::Locator;
use typst_library::layout::{
Abs, Axes, FixedAlignment, Frame, MoveElem, Point, Ratio, Region, Rel, RotateElem,
ScaleAmount, ScaleElem, Size, SkewElem, Transform,
};
use typst_utils::Numeric;
/// Layout the moved content.
#[typst_macros::time(span = elem.span())]
pub fn layout_move(
elem: &Packed<MoveElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let mut frame = crate::layout_frame(engine, &elem.body, locator, styles, region)?;
let delta = Axes::new(elem.dx(styles), elem.dy(styles)).resolve(styles);
let delta = delta.zip_map(region.size, Rel::relative_to);
frame.translate(delta.to_point());
Ok(frame)
}
/// Layout the rotated content.
#[typst_macros::time(span = elem.span())]
pub fn layout_rotate(
elem: &Packed<RotateElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let angle = elem.angle(styles);
let align = elem.origin(styles).resolve(styles);
// Compute the new region's approximate size.
let is_finite = region.size.is_finite();
let size = if is_finite {
compute_bounding_box(region.size, Transform::rotate(-angle)).1
} else {
Size::splat(Abs::inf())
};
measure_and_layout(
engine,
locator,
region,
size,
styles,
elem.body(),
Transform::rotate(angle),
align,
elem.reflow(styles),
)
}
/// Layout the scaled content.
#[typst_macros::time(span = elem.span())]
pub fn layout_scale(
elem: &Packed<ScaleElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
// Compute the new region's approximate size.
let scale = resolve_scale(elem, engine, locator.relayout(), region.size, styles)?;
let size = region
.size
.zip_map(scale, |r, s| if r.is_finite() { Ratio::new(1.0 / s).of(r) } else { r })
.map(Abs::abs);
measure_and_layout(
engine,
locator,
region,
size,
styles,
elem.body(),
Transform::scale(scale.x, scale.y),
elem.origin(styles).resolve(styles),
elem.reflow(styles),
)
}
/// Resolves scale parameters, preserving aspect ratio if one of the scales
/// is set to `auto`.
fn resolve_scale(
elem: &Packed<ScaleElem>,
engine: &mut Engine,
locator: Locator,
container: Size,
styles: StyleChain,
) -> SourceResult<Axes<Ratio>> {
fn resolve_axis(
axis: Smart<ScaleAmount>,
body: impl Fn() -> SourceResult<Abs>,
styles: StyleChain,
) -> SourceResult<Smart<Ratio>> {
Ok(match axis {
Smart::Auto => Smart::Auto,
Smart::Custom(amt) => Smart::Custom(match amt {
ScaleAmount::Ratio(ratio) => ratio,
ScaleAmount::Length(length) => {
let length = length.resolve(styles);
Ratio::new(length / body()?)
}
}),
})
}
let size = Lazy::new(|| {
let pod = Region::new(container, Axes::splat(false));
let frame = crate::layout_frame(engine, &elem.body, locator, styles, pod)?;
SourceResult::Ok(frame.size())
});
let x = resolve_axis(
elem.x(styles),
|| size.as_ref().map(|size| size.x).map_err(Clone::clone),
styles,
)?;
let y = resolve_axis(
elem.y(styles),
|| size.as_ref().map(|size| size.y).map_err(Clone::clone),
styles,
)?;
match (x, y) {
(Smart::Auto, Smart::Auto) => {
bail!(elem.span(), "x and y cannot both be auto")
}
(Smart::Custom(x), Smart::Custom(y)) => Ok(Axes::new(x, y)),
(Smart::Auto, Smart::Custom(v)) | (Smart::Custom(v), Smart::Auto) => {
Ok(Axes::splat(v))
}
}
}
/// Layout the skewed content.
#[typst_macros::time(span = elem.span())]
pub fn layout_skew(
elem: &Packed<SkewElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let ax = elem.ax(styles);
let ay = elem.ay(styles);
let align = elem.origin(styles).resolve(styles);
// Compute the new region's approximate size.
let size = if region.size.is_finite() {
compute_bounding_box(region.size, Transform::skew(ax, ay)).1
} else {
Size::splat(Abs::inf())
};
measure_and_layout(
engine,
locator,
region,
size,
styles,
elem.body(),
Transform::skew(ax, ay),
align,
elem.reflow(styles),
)
}
/// Applies a transformation to a frame, reflowing the layout if necessary.
#[allow(clippy::too_many_arguments)]
fn measure_and_layout(
engine: &mut Engine,
locator: Locator,
region: Region,
size: Size,
styles: StyleChain,
body: &Content,
transform: Transform,
align: Axes<FixedAlignment>,
reflow: bool,
) -> SourceResult<Frame> {
if reflow {
// Measure the size of the body.
let pod = Region::new(size, Axes::splat(false));
let frame = crate::layout_frame(engine, body, locator.relayout(), styles, pod)?;
// Actually perform the layout.
let pod = Region::new(frame.size(), Axes::splat(true));
let mut frame = crate::layout_frame(engine, body, locator, styles, pod)?;
let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position);
// Compute the transform.
let ts = Transform::translate(x, y)
.pre_concat(transform)
.pre_concat(Transform::translate(-x, -y));
// Compute the bounding box and offset and wrap in a new frame.
let (offset, size) = compute_bounding_box(frame.size(), ts);
frame.transform(ts);
frame.translate(offset);
frame.set_size(size);
Ok(frame)
} else {
// Layout the body.
let mut frame = crate::layout_frame(engine, body, locator, styles, region)?;
let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position);
// Compute the transform.
let ts = Transform::translate(x, y)
.pre_concat(transform)
.pre_concat(Transform::translate(-x, -y));
// Apply the transform.
frame.transform(ts);
Ok(frame)
}
}
/// Computes the bounding box and offset of a transformed area.
fn compute_bounding_box(size: Size, ts: Transform) -> (Point, Size) {
let top_left = Point::zero().transform_inf(ts);
let top_right = Point::with_x(size.x).transform_inf(ts);
let bottom_left = Point::with_y(size.y).transform_inf(ts);
let bottom_right = size.to_point().transform_inf(ts);
// We first compute the new bounding box of the rotated area.
let min_x = top_left.x.min(top_right.x).min(bottom_left.x).min(bottom_right.x);
let min_y = top_left.y.min(top_right.y).min(bottom_left.y).min(bottom_right.y);
let max_x = top_left.x.max(top_right.x).max(bottom_left.x).max(bottom_right.x);
let max_y = top_left.y.max(top_right.y).max(bottom_left.y).max(bottom_right.y);
// Then we compute the new size of the area.
let width = max_x - min_x;
let height = max_y - min_y;
(Point::new(-min_x, -min_y), Size::new(width.abs(), height.abs()))
}

View File

@ -0,0 +1,72 @@
[package]
name = "typst-library"
description = "Typst's standard library."
version = { workspace = true }
rust-version = { workspace = true }
authors = { workspace = true }
edition = { workspace = true }
homepage = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
categories = { workspace = true }
keywords = { workspace = true }
readme = { workspace = true }
[dependencies]
typst-assets = { workspace = true }
typst-macros = { workspace = true }
typst-syntax = { workspace = true }
typst-timing = { workspace = true }
typst-utils = { workspace = true }
az = { workspace = true }
bitflags = { workspace = true }
bumpalo = { workspace = true }
chinese-number = { workspace = true }
ciborium = { workspace = true }
comemo = { workspace = true }
csv = { workspace = true }
ecow = { workspace = true }
flate2 = { workspace = true }
fontdb = { workspace = true }
hayagriva = { workspace = true }
icu_properties = { workspace = true }
icu_provider = { workspace = true }
icu_provider_blob = { workspace = true }
image = { workspace = true }
indexmap = { workspace = true }
kamadak-exif = { workspace = true }
kurbo = { workspace = true }
lipsum = { workspace = true }
once_cell = { workspace = true }
palette = { workspace = true }
phf = { workspace = true }
png = { workspace = true }
qcms = { workspace = true }
rayon = { workspace = true }
regex = { workspace = true }
roxmltree = { workspace = true }
rust_decimal = { workspace = true }
rustybuzz = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_yaml = { workspace = true }
siphasher = { workspace = true }
smallvec = { workspace = true }
syntect = { workspace = true }
time = { workspace = true }
toml = { workspace = true }
ttf-parser = { workspace = true }
two-face = { workspace = true }
typed-arena = { workspace = true }
unicode-math-class = { workspace = true }
unicode-segmentation = { workspace = true }
unscanny = { workspace = true }
usvg = { workspace = true }
wasmi = { workspace = true }
xmlwriter = { workspace = true }
[dev-dependencies]
typst-dev-assets = { workspace = true }
[lints]
workspace = true

View File

@ -8,9 +8,9 @@ use std::string::FromUtf8Error;
use comemo::Tracked;
use ecow::{eco_vec, EcoVec};
use typst_syntax::package::{PackageSpec, PackageVersion};
use typst_syntax::{Span, Spanned, SyntaxError};
use crate::syntax::package::{PackageSpec, PackageVersion};
use crate::syntax::{Span, Spanned, SyntaxError};
use crate::{World, WorldExt};
/// Early-return with a [`StrResult`] or [`SourceResult`].

View File

@ -6,15 +6,19 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use comemo::{Track, Tracked, TrackedMut, Validate};
use ecow::EcoVec;
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
use typst_syntax::{FileId, Span};
use crate::diag::{bail, HintedStrResult, SourceDiagnostic, SourceResult, StrResult};
use crate::foundations::{Styles, Value};
use crate::introspection::Introspector;
use crate::syntax::{FileId, Span};
use crate::routines::Routines;
use crate::World;
/// Holds all data needed during compilation.
pub struct Engine<'a> {
/// Defines implementation of various Typst compiler routines as a table of
/// function pointers.
pub routines: &'a Routines,
/// The compilation environment.
pub world: Tracked<'a, dyn World + 'a>,
/// Provides access to information about the document.
@ -51,7 +55,9 @@ impl Engine<'_> {
U: Send,
F: Fn(&mut Engine, T) -> U + Send + Sync,
{
let Engine { world, introspector, traced, ref route, .. } = *self;
let Engine {
world, introspector, traced, ref route, routines, ..
} = *self;
// We collect into a vector and then call `into_par_iter` instead of
// using `par_bridge` because it does not retain the ordering.
@ -68,6 +74,7 @@ impl Engine<'_> {
traced,
sink: sink.track_mut(),
route: route.clone(),
routines,
};
(f(&mut engine, value), sink)
})
@ -173,7 +180,7 @@ impl Sink {
/// Add a warning.
pub fn warn(&mut self, warning: SourceDiagnostic) {
// Check if warning is a duplicate.
let hash = crate::utils::hash128(&(&warning.span, &warning.message));
let hash = typst_utils::hash128(&(&warning.span, &warning.message));
if self.warnings_set.insert(hash) {
self.warnings.push(warning);
}

View File

@ -1,12 +1,12 @@
use std::fmt::{self, Debug, Formatter};
use ecow::{eco_format, eco_vec, EcoString, EcoVec};
use typst_syntax::{Span, Spanned};
use crate::diag::{bail, error, At, SourceDiagnostic, SourceResult, StrResult};
use crate::foundations::{
cast, func, repr, scope, ty, Array, Dict, FromValue, IntoValue, Repr, Str, Value,
};
use crate::syntax::{Span, Spanned};
/// Captured arguments to a function.
///

View File

@ -7,15 +7,14 @@ use comemo::Tracked;
use ecow::{eco_format, EcoString, EcoVec};
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use typst_syntax::{Span, Spanned};
use crate::diag::{bail, At, HintedStrResult, SourceDiagnostic, SourceResult, StrResult};
use crate::engine::Engine;
use crate::eval::ops;
use crate::foundations::{
cast, func, repr, scope, ty, Args, Bytes, CastInfo, Context, Dict, FromValue, Func,
IntoValue, Reflect, Repr, Str, Value, Version,
cast, func, ops, repr, scope, ty, Args, Bytes, CastInfo, Context, Dict, FromValue,
Func, IntoValue, Reflect, Repr, Str, Value, Version,
};
use crate::syntax::{Span, Spanned};
/// Create a new [`Array`] from values.
#[macro_export]

View File

@ -1,6 +1,7 @@
use ecow::EcoString;
use std::fmt::{self, Debug, Formatter};
use ecow::EcoString;
use crate::diag::HintedStrResult;
use crate::foundations::{
ty, CastInfo, Fold, FromValue, IntoValue, Reflect, Repr, Resolve, StyleChain, Type,

View File

@ -5,10 +5,10 @@ use std::sync::Arc;
use ecow::{eco_format, EcoString};
use serde::{Serialize, Serializer};
use typst_utils::LazyHash;
use crate::diag::{bail, StrResult};
use crate::foundations::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value};
use crate::utils::LazyHash;
/// A sequence of bytes.
///

View File

@ -4,13 +4,12 @@ use std::cmp;
use std::cmp::Ordering;
use az::SaturatingAs;
use typst_syntax::{Span, Spanned};
use typst_utils::{round_int_with_precision, round_with_precision};
use crate::diag::{bail, At, HintedString, SourceResult, StrResult};
use crate::eval::ops;
use crate::foundations::{cast, func, Decimal, IntoValue, Module, Scope, Value};
use crate::foundations::{cast, func, ops, Decimal, IntoValue, Module, Scope, Value};
use crate::layout::{Angle, Fr, Length, Ratio};
use crate::syntax::{Span, Spanned};
use crate::utils::{round_int_with_precision, round_with_precision};
/// A module with calculation definitions.
pub fn module() -> Module {

View File

@ -1,3 +1,7 @@
#[rustfmt::skip]
#[doc(inline)]
pub use typst_macros::{cast, Cast};
use std::borrow::Cow;
use std::fmt::Write;
use std::hash::Hash;
@ -5,15 +9,11 @@ use std::ops::Add;
use ecow::eco_format;
use smallvec::SmallVec;
use typst_syntax::{Span, Spanned};
use unicode_math_class::MathClass;
use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult};
use crate::foundations::{array, repr, NativeElement, Packed, Repr, Str, Type, Value};
use crate::syntax::{Span, Spanned};
#[rustfmt::skip]
#[doc(inline)]
pub use typst_macros::{cast, Cast};
/// Determine details of a type.
///

View File

@ -10,6 +10,8 @@ use comemo::Tracked;
use ecow::{eco_format, EcoString};
use serde::{Serialize, Serializer};
use smallvec::smallvec;
use typst_syntax::Span;
use typst_utils::{fat, singleton, LazyHash, SmallBitSet};
use crate::diag::{SourceResult, StrResult};
use crate::engine::Engine;
@ -21,9 +23,7 @@ use crate::foundations::{
use crate::introspection::Location;
use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides};
use crate::model::{Destination, EmphElem, LinkElem, StrongElem};
use crate::syntax::Span;
use crate::text::UnderlineElem;
use crate::utils::{fat, singleton, LazyHash, SmallBitSet};
/// A piece of document content.
///

View File

@ -5,10 +5,11 @@ use std::str::FromStr;
use ecow::{eco_format, EcoString};
use rust_decimal::MathematicalOps;
use typst_syntax::{ast, Span, Spanned};
use crate::diag::{warning, At, SourceResult};
use crate::foundations::{cast, func, repr, scope, ty, Engine, Repr, Str};
use crate::syntax::{ast, Span, Spanned};
use crate::engine::Engine;
use crate::foundations::{cast, func, repr, scope, ty, Repr, Str};
use crate::World;
/// A fixed-point decimal number type.
@ -447,8 +448,9 @@ cast! {
mod tests {
use std::str::FromStr;
use typst_utils::hash128;
use super::Decimal;
use crate::utils::hash128;
#[test]
fn test_decimals_with_equal_scales_hash_identically() {

View File

@ -6,13 +6,13 @@ use std::sync::Arc;
use ecow::{eco_format, EcoString};
use indexmap::IndexMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use typst_syntax::is_ident;
use typst_utils::ArcExt;
use crate::diag::{Hint, HintedStrResult, StrResult};
use crate::foundations::{
array, cast, func, repr, scope, ty, Array, Module, Repr, Str, Value,
};
use crate::syntax::is_ident;
use crate::utils::ArcExt;
/// Create a new [`Dict`] from key-value pairs.
#[macro_export]

View File

@ -7,6 +7,9 @@ use std::ptr::NonNull;
use ecow::EcoString;
use once_cell::sync::Lazy;
use smallvec::SmallVec;
#[doc(inline)]
pub use typst_macros::elem;
use typst_utils::Static;
use crate::diag::SourceResult;
use crate::engine::Engine;
@ -15,10 +18,6 @@ use crate::foundations::{
StyleChain, Styles, Value,
};
use crate::text::{Lang, Region};
use crate::utils::Static;
#[doc(inline)]
pub use typst_macros::elem;
/// A document element.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]

View File

@ -2,9 +2,9 @@ use std::num::ParseFloatError;
use ecow::{eco_format, EcoString};
use crate::diag::StrResult;
use crate::diag::{bail, StrResult};
use crate::foundations::{
bail, cast, func, repr, scope, ty, Bytes, Decimal, Endianness, Repr, Str,
cast, func, repr, scope, ty, Bytes, Decimal, Endianness, Repr, Str,
};
use crate::layout::Ratio;

View File

@ -1,9 +1,14 @@
#[doc(inline)]
pub use typst_macros::func;
use std::fmt::{self, Debug, Formatter};
use std::sync::Arc;
use comemo::{Tracked, TrackedMut};
use ecow::{eco_format, EcoString};
use once_cell::sync::Lazy;
use typst_syntax::{ast, Span, SyntaxNode};
use typst_utils::{singleton, LazyHash, Static};
use crate::diag::{bail, SourceResult, StrResult};
use crate::engine::Engine;
@ -11,11 +16,6 @@ use crate::foundations::{
cast, repr, scope, ty, Args, CastInfo, Content, Context, Element, IntoArgs, Scope,
Selector, Type, Value,
};
use crate::syntax::{ast, Span, SyntaxNode};
use crate::utils::{singleton, LazyHash, Static};
#[doc(inline)]
pub use typst_macros::func;
/// A mapping from argument values to a return value.
///
@ -296,9 +296,10 @@ impl Func {
args.finish()?;
Ok(Value::Content(value))
}
Repr::Closure(closure) => crate::eval::call_closure(
Repr::Closure(closure) => (engine.routines.eval_closure)(
self,
closure,
engine.routines,
engine.world,
engine.introspector,
engine.traced,

View File

@ -2,9 +2,9 @@ use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError
use ecow::{eco_format, EcoString};
use crate::diag::StrResult;
use crate::diag::{bail, StrResult};
use crate::foundations::{
bail, cast, func, repr, scope, ty, Bytes, Cast, Decimal, Repr, Str, Value,
cast, func, repr, scope, ty, Bytes, Cast, Decimal, Repr, Str, Value,
};
/// A whole number.
@ -402,13 +402,16 @@ macro_rules! unsigned_int {
($($ty:ty)*) => {
$(cast! {
$ty,
self => if let Ok(int) = i64::try_from(self) {
Value::Int(int)
} else {
// Some u64 are too large to be cast as i64
// In that case, we accept that there may be a
// precision loss, and use a floating point number
Value::Float(self as _)
self => {
#[allow(irrefutable_let_patterns)]
if let Ok(int) = i64::try_from(self) {
Value::Int(int)
} else {
// Some u64 are too large to be cast as i64
// In that case, we accept that there may be a
// precision loss, and use a floating point number
Value::Float(self as _)
}
},
v: i64 => v.try_into().map_err(|_| {
if v < 0 {

View File

@ -1,7 +1,7 @@
use ecow::{eco_format, EcoString};
use typst_utils::PicoStr;
use crate::foundations::{func, scope, ty, Repr};
use crate::utils::PicoStr;
/// A label for an element.
///

View File

@ -1,11 +1,10 @@
//! Foundational types and functions.
pub mod calc;
pub mod ops;
pub mod repr;
pub mod sys;
pub use typst_macros::{scope, ty};
mod args;
mod array;
mod auto;
@ -24,7 +23,6 @@ mod float;
mod func;
mod int;
mod label;
mod methods;
mod module;
mod none;
mod plugin;
@ -32,6 +30,7 @@ mod scope;
mod selector;
mod str;
mod styles;
mod symbol;
mod ty;
mod value;
mod version;
@ -53,7 +52,6 @@ pub use self::float::*;
pub use self::func::*;
pub use self::int::*;
pub use self::label::*;
pub(crate) use self::methods::*;
pub use self::module::*;
pub use self::none::*;
pub use self::plugin::*;
@ -62,9 +60,11 @@ pub use self::scope::*;
pub use self::selector::*;
pub use self::str::*;
pub use self::styles::*;
pub use self::symbol::*;
pub use self::ty::*;
pub use self::value::*;
pub use self::version::*;
pub use typst_macros::{scope, ty};
#[rustfmt::skip]
#[doc(hidden)]
@ -75,11 +75,11 @@ pub use {
};
use ecow::EcoString;
use typst_syntax::Spanned;
use crate::diag::{bail, SourceResult, StrResult};
use crate::engine::Engine;
use crate::eval::EvalMode;
use crate::syntax::Spanned;
use crate::routines::EvalMode;
/// Foundational types and functions.
///
@ -108,6 +108,7 @@ pub(super) fn define(global: &mut Scope, inputs: Dict) {
global.define_type::<Selector>();
global.define_type::<Datetime>();
global.define_type::<Decimal>();
global.define_type::<Symbol>();
global.define_type::<Duration>();
global.define_type::<Version>();
global.define_type::<Plugin>();
@ -297,5 +298,5 @@ pub fn eval(
for (key, value) in dict {
scope.define_spanned(key, value, span);
}
crate::eval::eval_string(engine.world, &text, span, mode, scope)
(engine.routines.eval_string)(engine.routines, engine.world, &text, span, mode, scope)
}

View File

@ -2,10 +2,10 @@ use std::fmt::{self, Debug, Formatter};
use std::sync::Arc;
use ecow::{eco_format, EcoString};
use typst_syntax::FileId;
use crate::diag::StrResult;
use crate::foundations::{repr, ty, Content, Scope, Value};
use crate::syntax::FileId;
/// An evaluated module, either built-in or resulting from a file.
///

Some files were not shown because too many files have changed in this diff Show More