From 12dbb012b19a29612fc863c558901200b4013f5d Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sun, 2 Feb 2025 20:25:58 +0100 Subject: [PATCH 001/172] Revert adding `flatten-text` to `image` (#5789) --- crates/typst-layout/src/image.rs | 1 - .../typst-library/src/visualize/image/mod.rs | 13 ---------- .../typst-library/src/visualize/image/svg.rs | 24 ++----------------- crates/typst-pdf/src/image.rs | 6 +---- 4 files changed, 3 insertions(+), 41 deletions(-) diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index 503c30820..d963ea50d 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -63,7 +63,6 @@ pub fn layout_image( SvgImage::with_fonts( data.clone(), engine.world, - elem.flatten_text(styles), &families(styles).map(|f| f.as_str()).collect::>(), ) .at(span)?, diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 0e5c9e329..07ebdabe2 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -150,12 +150,6 @@ pub struct ImageElem { })] #[borrowed] pub icc: Smart>, - - /// Whether text in SVG images should be converted into curves before - /// embedding. This will result in the text becoming unselectable in the - /// output. - #[default(false)] - pub flatten_text: bool, } #[scope] @@ -199,10 +193,6 @@ impl ImageElem { /// A hint to viewers how they should scale the image. #[named] scaling: Option>, - /// Whether text in SVG images should be converted into curves before - /// embedding. - #[named] - flatten_text: Option, ) -> StrResult { let bytes = data.into_bytes(); let source = Derived::new(DataSource::Bytes(bytes.clone()), bytes); @@ -225,9 +215,6 @@ impl ImageElem { if let Some(scaling) = scaling { elem.push_scaling(scaling); } - if let Some(flatten_text) = flatten_text { - elem.push_flatten_text(flatten_text); - } Ok(elem.pack().spanned(span)) } } diff --git a/crates/typst-library/src/visualize/image/svg.rs b/crates/typst-library/src/visualize/image/svg.rs index dcc55077b..9bf1ead0d 100644 --- a/crates/typst-library/src/visualize/image/svg.rs +++ b/crates/typst-library/src/visualize/image/svg.rs @@ -22,7 +22,6 @@ pub struct SvgImage(Arc); struct Repr { data: Bytes, size: Axes, - flatten_text: bool, font_hash: u128, tree: usvg::Tree, } @@ -34,13 +33,7 @@ impl SvgImage { pub fn new(data: Bytes) -> StrResult { let tree = usvg::Tree::from_data(&data, &base_options()).map_err(format_usvg_error)?; - Ok(Self(Arc::new(Repr { - data, - size: tree_size(&tree), - font_hash: 0, - flatten_text: false, - tree, - }))) + Ok(Self(Arc::new(Repr { data, size: tree_size(&tree), font_hash: 0, tree }))) } /// Decode an SVG image with access to fonts. @@ -49,7 +42,6 @@ impl SvgImage { pub fn with_fonts( data: Bytes, world: Tracked, - flatten_text: bool, families: &[&str], ) -> StrResult { let book = world.book(); @@ -70,13 +62,7 @@ impl SvgImage { ) .map_err(format_usvg_error)?; let font_hash = resolver.into_inner().unwrap().finish(); - Ok(Self(Arc::new(Repr { - data, - size: tree_size(&tree), - font_hash, - flatten_text, - tree, - }))) + Ok(Self(Arc::new(Repr { data, size: tree_size(&tree), font_hash, tree }))) } /// The raw image data. @@ -89,11 +75,6 @@ impl SvgImage { self.0.size.x } - /// Whether the SVG's text should be flattened. - pub fn flatten_text(&self) -> bool { - self.0.flatten_text - } - /// The SVG's height in pixels. pub fn height(&self) -> f64 { self.0.size.y @@ -112,7 +93,6 @@ impl Hash for Repr { // all used fonts gives us something similar. self.data.hash(state); self.font_hash.hash(state); - self.flatten_text.hash(state); } } diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs index 550f60a4b..fa326e3e0 100644 --- a/crates/typst-pdf/src/image.rs +++ b/crates/typst-pdf/src/image.rs @@ -205,11 +205,7 @@ fn encode_svg( ) -> Result<(Chunk, Ref), svg2pdf::ConversionError> { svg2pdf::to_chunk( svg.tree(), - svg2pdf::ConversionOptions { - pdfa, - embed_text: !svg.flatten_text(), - ..Default::default() - }, + svg2pdf::ConversionOptions { pdfa, ..Default::default() }, ) } From eee903b0f8d5c0dfda3539888d7473c6163841b0 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 3 Feb 2025 17:04:54 +0100 Subject: [PATCH 002/172] Refactor `Scope` (#5797) --- crates/typst-eval/src/access.rs | 10 +- crates/typst-eval/src/call.rs | 54 +-- crates/typst-eval/src/code.rs | 2 +- crates/typst-eval/src/import.rs | 18 +- crates/typst-eval/src/math.rs | 2 +- crates/typst-eval/src/vm.rs | 23 +- crates/typst-ide/src/complete.rs | 25 +- crates/typst-ide/src/definition.rs | 4 +- crates/typst-ide/src/matchers.rs | 42 +- crates/typst-ide/src/tooltip.rs | 16 +- crates/typst-ide/src/utils.rs | 2 +- crates/typst-library/src/foundations/dict.rs | 7 +- crates/typst-library/src/foundations/func.rs | 2 +- crates/typst-library/src/foundations/mod.rs | 4 +- .../typst-library/src/foundations/module.rs | 15 +- .../typst-library/src/foundations/plugin.rs | 4 +- crates/typst-library/src/foundations/scope.rs | 395 +++++++++--------- crates/typst-library/src/foundations/ty.rs | 9 +- crates/typst-library/src/html/mod.rs | 2 +- crates/typst-library/src/introspection/mod.rs | 2 +- crates/typst-library/src/layout/mod.rs | 2 +- crates/typst-library/src/lib.rs | 9 +- crates/typst-library/src/loading/mod.rs | 2 +- crates/typst-library/src/math/mod.rs | 2 +- crates/typst-library/src/model/mod.rs | 2 +- crates/typst-library/src/pdf/mod.rs | 2 +- crates/typst-library/src/symbols.rs | 2 +- crates/typst-library/src/text/mod.rs | 2 +- crates/typst-library/src/visualize/mod.rs | 2 +- docs/src/lib.rs | 25 +- docs/src/link.rs | 4 +- 31 files changed, 371 insertions(+), 321 deletions(-) diff --git a/crates/typst-eval/src/access.rs b/crates/typst-eval/src/access.rs index 9bcac4d68..22a6b7f3d 100644 --- a/crates/typst-eval/src/access.rs +++ b/crates/typst-eval/src/access.rs @@ -30,12 +30,14 @@ impl Access for ast::Ident<'_> { fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { let span = self.span(); if vm.inspected == Some(span) { - if let Ok(value) = vm.scopes.get(&self).cloned() { - vm.trace(value); + if let Ok(binding) = vm.scopes.get(&self) { + vm.trace(binding.read().clone()); } } - let value = vm.scopes.get_mut(&self).at(span)?; - Ok(value) + vm.scopes + .get_mut(&self) + .and_then(|b| b.write().map_err(Into::into)) + .at(span) } } diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index 2a2223e15..6f0ec1fc9 100644 --- a/crates/typst-eval/src/call.rs +++ b/crates/typst-eval/src/call.rs @@ -6,8 +6,8 @@ use typst_library::diag::{ }; use typst_library::engine::{Engine, Sink, Traced}; use typst_library::foundations::{ - Arg, Args, Capturer, Closure, Content, Context, Func, NativeElement, Scope, Scopes, - SymbolElem, Value, + Arg, Args, Binding, Capturer, Closure, Content, Context, Func, NativeElement, Scope, + Scopes, SymbolElem, Value, }; use typst_library::introspection::Introspector; use typst_library::math::LrElem; @@ -196,7 +196,7 @@ pub fn eval_closure( // Provide the closure itself for recursive calls. if let Some(name) = name { - vm.define(name, Value::Func(func.clone())); + vm.define(name, func.clone()); } let num_pos_args = args.to_pos().len(); @@ -317,11 +317,11 @@ fn eval_field_call( if let Some(callee) = target.ty().scope().get(&field) { args.insert(0, target_expr.span(), target); - Ok(FieldCall::Normal(callee.clone(), args)) + Ok(FieldCall::Normal(callee.read().clone(), args)) } else if let Value::Content(content) = &target { if let Some(callee) = content.elem().scope().get(&field) { args.insert(0, target_expr.span(), target); - Ok(FieldCall::Normal(callee.clone(), args)) + Ok(FieldCall::Normal(callee.read().clone(), args)) } else { bail!(missing_field_call_error(target, field)) } @@ -458,11 +458,9 @@ impl<'a> CapturesVisitor<'a> { // Identifiers that shouldn't count as captures because they // actually bind a new name are handled below (individually through // the expressions that contain them). - Some(ast::Expr::Ident(ident)) => { - self.capture(ident.get(), ident.span(), Scopes::get) - } + Some(ast::Expr::Ident(ident)) => self.capture(ident.get(), Scopes::get), Some(ast::Expr::MathIdent(ident)) => { - self.capture(ident.get(), ident.span(), Scopes::get_in_math) + self.capture(ident.get(), Scopes::get_in_math) } // Code and content blocks create a scope. @@ -570,32 +568,34 @@ impl<'a> CapturesVisitor<'a> { /// Bind a new internal variable. fn bind(&mut self, ident: ast::Ident) { - self.internal.top.define_ident(ident, Value::None); + // The concrete value does not matter as we only use the scoping + // mechanism of `Scopes`, not the values themselves. + self.internal + .top + .bind(ident.get().clone(), Binding::detached(Value::None)); } /// Capture a variable if it isn't internal. fn capture( &mut self, ident: &EcoString, - span: Span, - getter: impl FnOnce(&'a Scopes<'a>, &str) -> HintedStrResult<&'a Value>, + getter: impl FnOnce(&'a Scopes<'a>, &str) -> HintedStrResult<&'a Binding>, ) { - if self.internal.get(ident).is_err() { - let Some(value) = self - .external - .map(|external| getter(external, ident).ok()) - .unwrap_or(Some(&Value::None)) - else { - return; - }; - - self.captures.define_captured( - ident.clone(), - value.clone(), - self.capturer, - span, - ); + if self.internal.get(ident).is_ok() { + return; } + + let binding = match self.external { + Some(external) => match getter(external, ident) { + Ok(binding) => binding.capture(self.capturer), + Err(_) => return, + }, + // The external scopes are only `None` when we are doing IDE capture + // analysis, in which case the concrete value doesn't matter. + None => Binding::detached(Value::None), + }; + + self.captures.bind(ident.clone(), binding); } } diff --git a/crates/typst-eval/src/code.rs b/crates/typst-eval/src/code.rs index 2baf4ea9e..4ac481865 100644 --- a/crates/typst-eval/src/code.rs +++ b/crates/typst-eval/src/code.rs @@ -154,7 +154,7 @@ impl Eval for ast::Ident<'_> { type Output = Value; fn eval(self, vm: &mut Vm) -> SourceResult { - vm.scopes.get(&self).cloned().at(self.span()) + Ok(vm.scopes.get(&self).at(self.span())?.read().clone()) } } diff --git a/crates/typst-eval/src/import.rs b/crates/typst-eval/src/import.rs index 2bbc7e41c..27b06af41 100644 --- a/crates/typst-eval/src/import.rs +++ b/crates/typst-eval/src/import.rs @@ -4,7 +4,7 @@ use typst_library::diag::{ bail, error, warning, At, FileError, SourceResult, Trace, Tracepoint, }; use typst_library::engine::Engine; -use typst_library::foundations::{Content, Module, Value}; +use typst_library::foundations::{Binding, Content, Module, Value}; use typst_library::World; use typst_syntax::ast::{self, AstNode, BareImportError}; use typst_syntax::package::{PackageManifest, PackageSpec}; @@ -43,7 +43,7 @@ impl Eval for ast::ModuleImport<'_> { } } - // Source itself is imported if there is no import list or a rename. + // If there is a rename, import the source itself under that name. let bare_name = self.bare_name(); let new_name = self.new_name(); if let Some(new_name) = new_name { @@ -57,8 +57,7 @@ impl Eval for ast::ModuleImport<'_> { } } - // Define renamed module on the scope. - vm.scopes.top.define_ident(new_name, source.clone()); + vm.define(new_name, source.clone()); } let scope = source.scope().unwrap(); @@ -76,7 +75,7 @@ impl Eval for ast::ModuleImport<'_> { "this import has no effect", )); } - vm.scopes.top.define_spanned(name, source, source_span); + vm.scopes.top.bind(name, Binding::new(source, source_span)); } Ok(_) | Err(BareImportError::Dynamic) => bail!( source_span, "dynamic import requires an explicit name"; @@ -92,8 +91,8 @@ impl Eval for ast::ModuleImport<'_> { } } Some(ast::Imports::Wildcard) => { - for (var, value, span) in scope.iter() { - vm.scopes.top.define_spanned(var.clone(), value.clone(), span); + for (var, binding) in scope.iter() { + vm.scopes.top.bind(var.clone(), binding.clone()); } } Some(ast::Imports::Items(items)) => { @@ -103,7 +102,7 @@ impl Eval for ast::ModuleImport<'_> { let mut scope = scope; while let Some(component) = &path.next() { - let Some(value) = scope.get(component) else { + let Some(binding) = scope.get(component) else { errors.push(error!(component.span(), "unresolved import")); break; }; @@ -111,6 +110,7 @@ impl Eval for ast::ModuleImport<'_> { if path.peek().is_some() { // Nested import, as this is not the last component. // This must be a submodule. + let value = binding.read(); let Some(submodule) = value.scope() else { let error = if matches!(value, Value::Func(function) if function.scope().is_none()) { @@ -153,7 +153,7 @@ impl Eval for ast::ModuleImport<'_> { } } - vm.define(item.bound_name(), value.clone()); + vm.bind(item.bound_name(), binding.clone()); } } } diff --git a/crates/typst-eval/src/math.rs b/crates/typst-eval/src/math.rs index bfb54aa87..23b293f26 100644 --- a/crates/typst-eval/src/math.rs +++ b/crates/typst-eval/src/math.rs @@ -35,7 +35,7 @@ impl Eval for ast::MathIdent<'_> { type Output = Value; fn eval(self, vm: &mut Vm) -> SourceResult { - vm.scopes.get_in_math(&self).cloned().at(self.span()) + Ok(vm.scopes.get_in_math(&self).at(self.span())?.read().clone()) } } diff --git a/crates/typst-eval/src/vm.rs b/crates/typst-eval/src/vm.rs index a5cbb6fa0..52cfb4b5b 100644 --- a/crates/typst-eval/src/vm.rs +++ b/crates/typst-eval/src/vm.rs @@ -1,7 +1,7 @@ use comemo::Tracked; use typst_library::diag::warning; use typst_library::engine::Engine; -use typst_library::foundations::{Context, IntoValue, Scopes, Value}; +use typst_library::foundations::{Binding, Context, IntoValue, Scopes, Value}; use typst_library::World; use typst_syntax::ast::{self, AstNode}; use typst_syntax::Span; @@ -42,13 +42,23 @@ impl<'a> Vm<'a> { self.engine.world } - /// Define a variable in the current scope. + /// Bind a value to an identifier. + /// + /// This will create a [`Binding`] with the value and the identifier's span. pub fn define(&mut self, var: ast::Ident, value: impl IntoValue) { - let value = value.into_value(); + self.bind(var, Binding::new(value, var.span())); + } + + /// Insert a binding into the current scope. + /// + /// This will insert the value into the top-most scope and make it available + /// for dynamic tracing, assisting IDE functionality. + pub fn bind(&mut self, var: ast::Ident, binding: Binding) { if self.inspected == Some(var.span()) { - self.trace(value.clone()); + self.trace(binding.read().clone()); } - // This will become an error in the parser if 'is' becomes a keyword. + + // This will become an error in the parser if `is` becomes a keyword. if var.get() == "is" { self.engine.sink.warn(warning!( var.span(), @@ -58,7 +68,8 @@ impl<'a> Vm<'a> { hint: "try `is_` instead" )); } - self.scopes.top.define_ident(var, value); + + self.scopes.top.bind(var.get().clone(), binding); } /// Trace a value. diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index 24b76537a..f68c925d4 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -398,13 +398,13 @@ fn field_access_completions( value: &Value, styles: &Option, ) { - for (name, value, _) in value.ty().scope().iter() { - ctx.call_completion(name.clone(), value); + for (name, binding) in value.ty().scope().iter() { + ctx.call_completion(name.clone(), binding.read()); } if let Some(scope) = value.scope() { - for (name, value, _) in scope.iter() { - ctx.call_completion(name.clone(), value); + for (name, binding) in scope.iter() { + ctx.call_completion(name.clone(), binding.read()); } } @@ -541,9 +541,9 @@ fn import_item_completions<'a>( ctx.snippet_completion("*", "*", "Import everything."); } - for (name, value, _) in scope.iter() { + for (name, binding) in scope.iter() { if existing.iter().all(|item| item.original_name().as_str() != name) { - ctx.value_completion(name.clone(), value); + ctx.value_completion(name.clone(), binding.read()); } } } @@ -846,13 +846,11 @@ fn resolve_global_callee<'a>( ) -> Option<&'a Func> { let globals = globals(ctx.world, ctx.leaf); let value = match callee { - ast::Expr::Ident(ident) => globals.get(&ident)?, + ast::Expr::Ident(ident) => globals.get(&ident)?.read(), ast::Expr::FieldAccess(access) => match access.target() { - ast::Expr::Ident(target) => match globals.get(&target)? { - Value::Module(module) => module.field(&access.field()).ok()?, - Value::Func(func) => func.field(&access.field()).ok()?, - _ => return None, - }, + ast::Expr::Ident(target) => { + globals.get(&target)?.read().scope()?.get(&access.field())?.read() + } _ => return None, }, _ => return None, @@ -1464,7 +1462,8 @@ impl<'a> CompletionContext<'a> { } } - for (name, value, _) in globals(self.world, self.leaf).iter() { + for (name, binding) in globals(self.world, self.leaf).iter() { + let value = binding.read(); if filter(value) && !defined.contains_key(name) { self.value_completion_full(Some(name.clone()), value, parens, None, None); } diff --git a/crates/typst-ide/src/definition.rs b/crates/typst-ide/src/definition.rs index 31fb9e34e..69d702b3b 100644 --- a/crates/typst-ide/src/definition.rs +++ b/crates/typst-ide/src/definition.rs @@ -55,8 +55,8 @@ pub fn definition( } } - if let Some(value) = globals(world, &leaf).get(&name) { - return Some(Definition::Std(value.clone())); + if let Some(binding) = globals(world, &leaf).get(&name) { + return Some(Definition::Std(binding.read().clone())); } } diff --git a/crates/typst-ide/src/matchers.rs b/crates/typst-ide/src/matchers.rs index ef8288f2a..270d2f43c 100644 --- a/crates/typst-ide/src/matchers.rs +++ b/crates/typst-ide/src/matchers.rs @@ -76,8 +76,12 @@ pub fn named_items( // ``` Some(ast::Imports::Wildcard) => { if let Some(scope) = source_value.and_then(Value::scope) { - for (name, value, span) in scope.iter() { - let item = NamedItem::Import(name, span, Some(value)); + for (name, binding) in scope.iter() { + let item = NamedItem::Import( + name, + binding.span(), + Some(binding.read()), + ); if let Some(res) = recv(item) { return Some(res); } @@ -89,24 +93,26 @@ pub fn named_items( // ``` Some(ast::Imports::Items(items)) => { for item in items.iter() { + let mut iter = item.path().iter(); + let mut binding = source_value + .and_then(Value::scope) + .zip(iter.next()) + .and_then(|(scope, first)| scope.get(&first)); + + for ident in iter { + binding = binding.and_then(|binding| { + binding.read().scope()?.get(&ident) + }); + } + let bound = item.bound_name(); + let (span, value) = match binding { + Some(binding) => (binding.span(), Some(binding.read())), + None => (bound.span(), None), + }; - let (span, value) = item.path().iter().fold( - (bound.span(), source_value), - |(span, value), path_ident| { - let scope = value.and_then(|v| v.scope()); - let span = scope - .and_then(|s| s.get_span(&path_ident)) - .unwrap_or(Span::detached()) - .or(span); - let value = scope.and_then(|s| s.get(&path_ident)); - (span, value) - }, - ); - - if let Some(res) = - recv(NamedItem::Import(bound.get(), span, value)) - { + let item = NamedItem::Import(bound.get(), span, value); + if let Some(res) = recv(item) { return Some(res); } } diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index 99ae0620b..cfb977733 100644 --- a/crates/typst-ide/src/tooltip.rs +++ b/crates/typst-ide/src/tooltip.rs @@ -3,7 +3,7 @@ use std::fmt::Write; use ecow::{eco_format, EcoString}; use if_chain::if_chain; use typst::engine::Sink; -use typst::foundations::{repr, Capturer, CastInfo, Repr, Value}; +use typst::foundations::{repr, Binding, Capturer, CastInfo, Repr, Value}; use typst::layout::{Length, PagedDocument}; use typst::syntax::ast::AstNode; use typst::syntax::{ast, LinkedNode, Side, Source, SyntaxKind}; @@ -206,7 +206,12 @@ fn named_param_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option { - self.find_iter(module.scope().iter().map(|(_, v, _)| v))?; + self.find_iter(module.scope().iter().map(|(_, b)| b.read()))?; } _ => {} } diff --git a/crates/typst-library/src/foundations/dict.rs b/crates/typst-library/src/foundations/dict.rs index e4ab54e72..c93670c1d 100644 --- a/crates/typst-library/src/foundations/dict.rs +++ b/crates/typst-library/src/foundations/dict.rs @@ -261,7 +261,12 @@ pub struct ToDict(Dict); cast! { ToDict, - v: Module => Self(v.scope().iter().map(|(k, v, _)| (Str::from(k.clone()), v.clone())).collect()), + v: Module => Self(v + .scope() + .iter() + .map(|(k, b)| (Str::from(k.clone()), b.read().clone())) + .collect() + ), } impl Debug for Dict { diff --git a/crates/typst-library/src/foundations/func.rs b/crates/typst-library/src/foundations/func.rs index a05deb1f3..741b66331 100644 --- a/crates/typst-library/src/foundations/func.rs +++ b/crates/typst-library/src/foundations/func.rs @@ -259,7 +259,7 @@ impl Func { let scope = self.scope().ok_or("cannot access fields on user-defined functions")?; match scope.get(field) { - Some(field) => Ok(field), + Some(binding) => Ok(binding.read()), None => match self.name() { Some(name) => bail!("function `{name}` does not contain field `{field}`"), None => bail!("function does not contain field `{field}`"), diff --git a/crates/typst-library/src/foundations/mod.rs b/crates/typst-library/src/foundations/mod.rs index a790da4f4..c335484fa 100644 --- a/crates/typst-library/src/foundations/mod.rs +++ b/crates/typst-library/src/foundations/mod.rs @@ -94,7 +94,7 @@ pub static FOUNDATIONS: Category; /// Hook up all `foundations` definitions. pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) { - global.category(FOUNDATIONS); + global.start_category(FOUNDATIONS); global.define_type::(); global.define_type::(); global.define_type::(); @@ -301,7 +301,7 @@ pub fn eval( let dict = scope; let mut scope = Scope::new(); for (key, value) in dict { - scope.define_spanned(key, value, span); + scope.bind(key.into(), Binding::new(value, span)); } (engine.routines.eval_string)(engine.routines, engine.world, &text, span, mode, scope) } diff --git a/crates/typst-library/src/foundations/module.rs b/crates/typst-library/src/foundations/module.rs index 3ee59c106..3259c17e6 100644 --- a/crates/typst-library/src/foundations/module.rs +++ b/crates/typst-library/src/foundations/module.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use ecow::{eco_format, EcoString}; use typst_syntax::FileId; -use crate::diag::StrResult; +use crate::diag::{bail, StrResult}; use crate::foundations::{repr, ty, Content, Scope, Value}; /// An module of definitions. @@ -118,11 +118,14 @@ impl Module { } /// Try to access a definition in the module. - pub fn field(&self, name: &str) -> StrResult<&Value> { - self.scope().get(name).ok_or_else(|| match &self.name { - Some(module) => eco_format!("module `{module}` does not contain `{name}`"), - None => eco_format!("module does not contain `{name}`"), - }) + pub fn field(&self, field: &str) -> StrResult<&Value> { + match self.scope().get(field) { + Some(binding) => Ok(binding.read()), + None => match &self.name { + Some(name) => bail!("module `{name}` does not contain `{field}`"), + None => bail!("module does not contain `{field}`"), + }, + } } /// Extract the module's content. diff --git a/crates/typst-library/src/foundations/plugin.rs b/crates/typst-library/src/foundations/plugin.rs index cbc0f52de..a33f1cb91 100644 --- a/crates/typst-library/src/foundations/plugin.rs +++ b/crates/typst-library/src/foundations/plugin.rs @@ -8,7 +8,7 @@ use wasmi::Memory; use crate::diag::{bail, At, SourceResult, StrResult}; use crate::engine::Engine; -use crate::foundations::{cast, func, scope, Bytes, Func, Module, Scope, Value}; +use crate::foundations::{cast, func, scope, Binding, Bytes, Func, Module, Scope, Value}; use crate::loading::{DataSource, Load}; /// Loads a WebAssembly module. @@ -369,7 +369,7 @@ impl Plugin { if matches!(export.ty(), wasmi::ExternType::Func(_)) { let name = EcoString::from(export.name()); let func = PluginFunc { plugin: shared.clone(), name: name.clone() }; - scope.define(name, Func::from(func)); + scope.bind(name, Binding::detached(Func::from(func))); } } diff --git a/crates/typst-library/src/foundations/scope.rs b/crates/typst-library/src/foundations/scope.rs index b7b4a6d9d..e73afeacd 100644 --- a/crates/typst-library/src/foundations/scope.rs +++ b/crates/typst-library/src/foundations/scope.rs @@ -5,8 +5,8 @@ use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use ecow::{eco_format, EcoString}; +use indexmap::map::Entry; use indexmap::IndexMap; -use typst_syntax::ast::{self, AstNode}; use typst_syntax::Span; use typst_utils::Static; @@ -46,14 +46,14 @@ impl<'a> Scopes<'a> { self.top = self.scopes.pop().expect("no pushed scope"); } - /// Try to access a variable immutably. - pub fn get(&self, var: &str) -> HintedStrResult<&Value> { + /// Try to access a binding immutably. + pub fn get(&self, var: &str) -> HintedStrResult<&Binding> { std::iter::once(&self.top) .chain(self.scopes.iter().rev()) .find_map(|scope| scope.get(var)) .or_else(|| { self.base.and_then(|base| match base.global.scope().get(var) { - Some(value) => Some(value), + Some(binding) => Some(binding), None if var == "std" => Some(&base.std), None => None, }) @@ -61,14 +61,28 @@ impl<'a> Scopes<'a> { .ok_or_else(|| unknown_variable(var)) } - /// Try to access a variable immutably in math. - pub fn get_in_math(&self, var: &str) -> HintedStrResult<&Value> { + /// Try to access a binding mutably. + pub fn get_mut(&mut self, var: &str) -> HintedStrResult<&mut Binding> { + std::iter::once(&mut self.top) + .chain(&mut self.scopes.iter_mut().rev()) + .find_map(|scope| scope.get_mut(var)) + .ok_or_else(|| { + match self.base.and_then(|base| base.global.scope().get(var)) { + Some(_) => cannot_mutate_constant(var), + _ if var == "std" => cannot_mutate_constant(var), + _ => unknown_variable(var), + } + }) + } + + /// Try to access a binding immutably in math. + pub fn get_in_math(&self, var: &str) -> HintedStrResult<&Binding> { std::iter::once(&self.top) .chain(self.scopes.iter().rev()) .find_map(|scope| scope.get(var)) .or_else(|| { self.base.and_then(|base| match base.math.scope().get(var) { - Some(value) => Some(value), + Some(binding) => Some(binding), None if var == "std" => Some(&base.std), None => None, }) @@ -81,20 +95,6 @@ impl<'a> Scopes<'a> { }) } - /// Try to access a variable mutably. - pub fn get_mut(&mut self, var: &str) -> HintedStrResult<&mut Value> { - std::iter::once(&mut self.top) - .chain(&mut self.scopes.iter_mut().rev()) - .find_map(|scope| scope.get_mut(var)) - .ok_or_else(|| { - match self.base.and_then(|base| base.global.scope().get(var)) { - Some(_) => cannot_mutate_constant(var), - _ if var == "std" => cannot_mutate_constant(var), - _ => unknown_variable(var), - } - })? - } - /// Check if an std variable is shadowed. pub fn check_std_shadowed(&self, var: &str) -> bool { self.base.is_some_and(|base| base.global.scope().get(var).is_some()) @@ -104,84 +104,28 @@ impl<'a> Scopes<'a> { } } -#[cold] -fn cannot_mutate_constant(var: &str) -> HintedString { - eco_format!("cannot mutate a constant: {}", var).into() -} - -/// The error message when a variable is not found. -#[cold] -fn unknown_variable(var: &str) -> HintedString { - let mut res = HintedString::new(eco_format!("unknown variable: {}", var)); - - if var.contains('-') { - res.hint(eco_format!( - "if you meant to use subtraction, try adding spaces around the minus sign{}: `{}`", - if var.matches('-').count() > 1 { "s" } else { "" }, - var.replace('-', " - ") - )); - } - - res -} - -#[cold] -fn unknown_variable_math(var: &str, in_global: bool) -> HintedString { - let mut res = HintedString::new(eco_format!("unknown variable: {}", var)); - - if matches!(var, "none" | "auto" | "false" | "true") { - res.hint(eco_format!( - "if you meant to use a literal, try adding a hash before it: `#{var}`", - )); - } else if in_global { - res.hint(eco_format!( - "`{var}` is not available directly in math, try adding a hash before it: `#{var}`", - )); - } else { - res.hint(eco_format!( - "if you meant to display multiple letters as is, try adding spaces between each letter: `{}`", - var.chars() - .flat_map(|c| [' ', c]) - .skip(1) - .collect::() - )); - res.hint(eco_format!( - "or if you meant to display this as text, try placing it in quotes: `\"{var}\"`" - )); - } - - res -} - /// A map from binding names to values. #[derive(Default, Clone)] pub struct Scope { - map: IndexMap, + map: IndexMap, deduplicate: bool, category: Option, } +/// Scope construction. impl Scope { /// Create a new empty scope. pub fn new() -> Self { Default::default() } - /// Create a new scope with the given capacity. - pub fn with_capacity(capacity: usize) -> Self { - Self { - map: IndexMap::with_capacity(capacity), - ..Default::default() - } - } - /// Create a new scope with duplication prevention. pub fn deduplicating() -> Self { Self { deduplicate: true, ..Default::default() } } /// Enter a new category. - pub fn category(&mut self, category: Category) { + pub fn start_category(&mut self, category: Category) { self.category = Some(category); } @@ -190,102 +134,87 @@ impl Scope { self.category = None; } - /// Bind a value to a name. - #[track_caller] - pub fn define(&mut self, name: impl Into, value: impl IntoValue) { - self.define_spanned(name, value, Span::detached()) - } - - /// Bind a value to a name defined by an identifier. - #[track_caller] - pub fn define_ident(&mut self, ident: ast::Ident, value: impl IntoValue) { - self.define_spanned(ident.get().clone(), value, ident.span()) - } - - /// Bind a value to a name. - #[track_caller] - pub fn define_spanned( - &mut self, - name: impl Into, - value: impl IntoValue, - span: Span, - ) { - let name = name.into(); - - #[cfg(debug_assertions)] - if self.deduplicate && self.map.contains_key(&name) { - panic!("duplicate definition: {name}"); - } - - self.map.insert( - name, - Slot::new(value.into_value(), span, Kind::Normal, self.category), - ); - } - - /// Define a captured, immutable binding. - pub fn define_captured( - &mut self, - name: EcoString, - value: Value, - capturer: Capturer, - span: Span, - ) { - self.map.insert( - name, - Slot::new(value.into_value(), span, Kind::Captured(capturer), self.category), - ); - } - /// Define a native function through a Rust type that shadows the function. - pub fn define_func(&mut self) { + #[track_caller] + pub fn define_func(&mut self) -> &mut Binding { let data = T::data(); - self.define(data.name, Func::from(data)); + self.define(data.name, Func::from(data)) } /// Define a native function with raw function data. - pub fn define_func_with_data(&mut self, data: &'static NativeFuncData) { - self.define(data.name, Func::from(data)); + #[track_caller] + pub fn define_func_with_data( + &mut self, + data: &'static NativeFuncData, + ) -> &mut Binding { + self.define(data.name, Func::from(data)) } /// Define a native type. - pub fn define_type(&mut self) { + #[track_caller] + pub fn define_type(&mut self) -> &mut Binding { let data = T::data(); - self.define(data.name, Type::from(data)); + self.define(data.name, Type::from(data)) } /// Define a native element. - pub fn define_elem(&mut self) { + #[track_caller] + pub fn define_elem(&mut self) -> &mut Binding { let data = T::data(); - self.define(data.name, Element::from(data)); + self.define(data.name, Element::from(data)) } - /// Try to access a variable immutably. - pub fn get(&self, var: &str) -> Option<&Value> { - self.map.get(var).map(Slot::read) + /// Define a built-in with compile-time known name and returns a mutable + /// reference to it. + /// + /// When the name isn't compile-time known, you should instead use: + /// - `Vm::bind` if you already have [`Binding`] + /// - `Vm::define` if you only have a [`Value`] + /// - [`Scope::bind`](Self::bind) if you are not operating in the context of + /// a `Vm` or if you are binding to something that is not an AST + /// identifier (e.g. when constructing a dynamic + /// [`Module`](super::Module)) + #[track_caller] + pub fn define(&mut self, name: &'static str, value: impl IntoValue) -> &mut Binding { + #[cfg(debug_assertions)] + if self.deduplicate && self.map.contains_key(name) { + panic!("duplicate definition: {name}"); + } + + let mut binding = Binding::detached(value); + binding.category = self.category; + self.bind(name.into(), binding) + } +} + +/// Scope manipulation and access. +impl Scope { + /// Inserts a binding into this scope and returns a mutable reference to it. + /// + /// Prefer `Vm::bind` if you are operating in the context of a `Vm`. + pub fn bind(&mut self, name: EcoString, binding: Binding) -> &mut Binding { + match self.map.entry(name) { + Entry::Occupied(mut entry) => { + entry.insert(binding); + entry.into_mut() + } + Entry::Vacant(entry) => entry.insert(binding), + } } - /// Try to access a variable mutably. - pub fn get_mut(&mut self, var: &str) -> Option> { - self.map - .get_mut(var) - .map(Slot::write) - .map(|res| res.map_err(HintedString::from)) + /// Try to access a binding immutably. + pub fn get(&self, var: &str) -> Option<&Binding> { + self.map.get(var) } - /// Get the span of a definition. - pub fn get_span(&self, var: &str) -> Option { - Some(self.map.get(var)?.span) - } - - /// Get the category of a definition. - pub fn get_category(&self, var: &str) -> Option { - self.map.get(var)?.category + /// Try to access a binding mutably. + pub fn get_mut(&mut self, var: &str) -> Option<&mut Binding> { + self.map.get_mut(var) } /// Iterate over all definitions. - pub fn iter(&self) -> impl Iterator { - self.map.iter().map(|(k, v)| (k, v.read(), v.span)) + pub fn iter(&self) -> impl Iterator { + self.map.iter() } } @@ -318,28 +247,85 @@ pub trait NativeScope { fn scope() -> Scope; } -/// A slot where a value is stored. -#[derive(Clone, Hash)] -struct Slot { - /// The stored value. +/// A bound value with metadata. +#[derive(Debug, Clone, Hash)] +pub struct Binding { + /// The bound value. value: Value, - /// The kind of slot, determines how the value can be accessed. - kind: Kind, - /// A span associated with the stored value. + /// The kind of binding, determines how the value can be accessed. + kind: BindingKind, + /// A span associated with the binding. span: Span, - /// The category of the slot. + /// The category of the binding. category: Option, } /// The different kinds of slots. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -enum Kind { +enum BindingKind { /// A normal, mutable binding. Normal, /// A captured copy of another variable. Captured(Capturer), } +impl Binding { + /// Create a new binding with a span marking its definition site. + pub fn new(value: impl IntoValue, span: Span) -> Self { + Self { + value: value.into_value(), + span, + kind: BindingKind::Normal, + category: None, + } + } + + /// Create a binding without a span. + pub fn detached(value: impl IntoValue) -> Self { + Self::new(value, Span::detached()) + } + + /// Read the value. + pub fn read(&self) -> &Value { + &self.value + } + + /// Try to write to the value. + /// + /// This fails if the value is a read-only closure capture. + pub fn write(&mut self) -> StrResult<&mut Value> { + match self.kind { + BindingKind::Normal => Ok(&mut self.value), + BindingKind::Captured(capturer) => bail!( + "variables from outside the {} are \ + read-only and cannot be modified", + match capturer { + Capturer::Function => "function", + Capturer::Context => "context expression", + } + ), + } + } + + /// Create a copy of the binding for closure capturing. + pub fn capture(&self, capturer: Capturer) -> Self { + Self { + kind: BindingKind::Captured(capturer), + ..self.clone() + } + } + + /// A span associated with the stored value. + pub fn span(&self) -> Span { + self.span + } + + /// The category of the value, if any. + pub fn category(&self) -> Option { + self.category + } +} + /// What the variable was captured by. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum Capturer { @@ -349,35 +335,6 @@ pub enum Capturer { Context, } -impl Slot { - /// Create a new slot. - fn new(value: Value, span: Span, kind: Kind, category: Option) -> Self { - Self { value, span, kind, category } - } - - /// Read the value. - fn read(&self) -> &Value { - &self.value - } - - /// Try to write to the value. - fn write(&mut self) -> StrResult<&mut Value> { - match self.kind { - Kind::Normal => Ok(&mut self.value), - Kind::Captured(capturer) => { - bail!( - "variables from outside the {} are \ - read-only and cannot be modified", - match capturer { - Capturer::Function => "function", - Capturer::Context => "context expression", - } - ) - } - } - } -} - /// A group of related definitions. #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub struct Category(Static); @@ -417,3 +374,57 @@ pub struct CategoryData { pub title: &'static str, pub docs: &'static str, } + +/// The error message when trying to mutate a variable from the standard +/// library. +#[cold] +fn cannot_mutate_constant(var: &str) -> HintedString { + eco_format!("cannot mutate a constant: {}", var).into() +} + +/// The error message when a variable wasn't found. +#[cold] +fn unknown_variable(var: &str) -> HintedString { + let mut res = HintedString::new(eco_format!("unknown variable: {}", var)); + + if var.contains('-') { + res.hint(eco_format!( + "if you meant to use subtraction, \ + try adding spaces around the minus sign{}: `{}`", + if var.matches('-').count() > 1 { "s" } else { "" }, + var.replace('-', " - ") + )); + } + + res +} + +/// The error message when a variable wasn't found it math. +#[cold] +fn unknown_variable_math(var: &str, in_global: bool) -> HintedString { + let mut res = HintedString::new(eco_format!("unknown variable: {}", var)); + + if matches!(var, "none" | "auto" | "false" | "true") { + res.hint(eco_format!( + "if you meant to use a literal, \ + try adding a hash before it: `#{var}`", + )); + } else if in_global { + res.hint(eco_format!( + "`{var}` is not available directly in math, \ + try adding a hash before it: `#{var}`", + )); + } else { + res.hint(eco_format!( + "if you meant to display multiple letters as is, \ + try adding spaces between each letter: `{}`", + var.chars().flat_map(|c| [' ', c]).skip(1).collect::() + )); + res.hint(eco_format!( + "or if you meant to display this as text, \ + try placing it in quotes: `\"{var}\"`" + )); + } + + res +} diff --git a/crates/typst-library/src/foundations/ty.rs b/crates/typst-library/src/foundations/ty.rs index 973c1cb61..09f5efa1e 100644 --- a/crates/typst-library/src/foundations/ty.rs +++ b/crates/typst-library/src/foundations/ty.rs @@ -8,7 +8,7 @@ use std::sync::LazyLock; use ecow::{eco_format, EcoString}; use typst_utils::Static; -use crate::diag::StrResult; +use crate::diag::{bail, StrResult}; use crate::foundations::{ cast, func, AutoValue, Func, NativeFuncData, NoneValue, Repr, Scope, Value, }; @@ -95,9 +95,10 @@ impl Type { /// Get a field from this type's scope, if possible. pub fn field(&self, field: &str) -> StrResult<&'static Value> { - self.scope() - .get(field) - .ok_or_else(|| eco_format!("type {self} does not contain field `{field}`")) + match self.scope().get(field) { + Some(binding) => Ok(binding.read()), + None => bail!("type {self} does not contain field `{field}`"), + } } } diff --git a/crates/typst-library/src/html/mod.rs b/crates/typst-library/src/html/mod.rs index ea248172a..c412b4607 100644 --- a/crates/typst-library/src/html/mod.rs +++ b/crates/typst-library/src/html/mod.rs @@ -15,7 +15,7 @@ pub static HTML: Category; /// Create a module with all HTML definitions. pub fn module() -> Module { let mut html = Scope::deduplicating(); - html.category(HTML); + html.start_category(HTML); html.define_elem::(); html.define_elem::(); Module::new("html", html) diff --git a/crates/typst-library/src/introspection/mod.rs b/crates/typst-library/src/introspection/mod.rs index b1ff2e080..d8184330d 100644 --- a/crates/typst-library/src/introspection/mod.rs +++ b/crates/typst-library/src/introspection/mod.rs @@ -42,7 +42,7 @@ pub static INTROSPECTION: Category; /// Hook up all `introspection` definitions. pub fn define(global: &mut Scope) { - global.category(INTROSPECTION); + global.start_category(INTROSPECTION); global.define_type::(); global.define_type::(); global.define_type::(); diff --git a/crates/typst-library/src/layout/mod.rs b/crates/typst-library/src/layout/mod.rs index 574a2830a..57518fe72 100644 --- a/crates/typst-library/src/layout/mod.rs +++ b/crates/typst-library/src/layout/mod.rs @@ -74,7 +74,7 @@ pub static LAYOUT: Category; /// Hook up all `layout` definitions. pub fn define(global: &mut Scope) { - global.category(LAYOUT); + global.start_category(LAYOUT); global.define_type::(); global.define_type::(); global.define_type::(); diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs index 22f3a62a3..460321aa3 100644 --- a/crates/typst-library/src/lib.rs +++ b/crates/typst-library/src/lib.rs @@ -33,7 +33,7 @@ use typst_syntax::{FileId, Source, Span}; use typst_utils::{LazyHash, SmallBitSet}; use crate::diag::FileResult; -use crate::foundations::{Array, Bytes, Datetime, Dict, Module, Scope, Styles, Value}; +use crate::foundations::{Array, Binding, Bytes, Datetime, Dict, Module, Scope, Styles}; use crate::layout::{Alignment, Dir}; use crate::text::{Font, FontBook}; use crate::visualize::Color; @@ -148,7 +148,7 @@ pub struct Library { /// everything else configurable via set and show rules). pub styles: Styles, /// The standard library as a value. Used to provide the `std` variable. - pub std: Value, + pub std: Binding, /// In-development features that were enabled. pub features: Features, } @@ -196,12 +196,11 @@ impl LibraryBuilder { let math = math::module(); let inputs = self.inputs.unwrap_or_default(); let global = global(math.clone(), inputs, &self.features); - let std = Value::Module(global.clone()); Library { - global, + global: global.clone(), math, styles: Styles::new(), - std, + std: Binding::detached(global), features: self.features, } } diff --git a/crates/typst-library/src/loading/mod.rs b/crates/typst-library/src/loading/mod.rs index 171ae651a..c645b691d 100644 --- a/crates/typst-library/src/loading/mod.rs +++ b/crates/typst-library/src/loading/mod.rs @@ -41,7 +41,7 @@ pub static DATA_LOADING: Category; /// Hook up all `data-loading` definitions. pub(super) fn define(global: &mut Scope) { - global.category(DATA_LOADING); + global.start_category(DATA_LOADING); global.define_func::(); global.define_func::(); global.define_func::(); diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs index 3b4b133d9..a97a19b09 100644 --- a/crates/typst-library/src/math/mod.rs +++ b/crates/typst-library/src/math/mod.rs @@ -150,7 +150,7 @@ pub const WIDE: Em = Em::new(2.0); /// Create a module with all math definitions. pub fn module() -> Module { let mut math = Scope::deduplicating(); - math.category(MATH); + math.start_category(MATH); math.define_elem::(); math.define_elem::(); math.define_elem::(); diff --git a/crates/typst-library/src/model/mod.rs b/crates/typst-library/src/model/mod.rs index 7dad51c39..586e10ec1 100644 --- a/crates/typst-library/src/model/mod.rs +++ b/crates/typst-library/src/model/mod.rs @@ -52,7 +52,7 @@ pub static MODEL: Category; /// Hook up all `model` definitions. pub fn define(global: &mut Scope) { - global.category(MODEL); + global.start_category(MODEL); global.define_elem::(); global.define_elem::(); global.define_elem::(); diff --git a/crates/typst-library/src/pdf/mod.rs b/crates/typst-library/src/pdf/mod.rs index ec0754631..3bd3b0c52 100644 --- a/crates/typst-library/src/pdf/mod.rs +++ b/crates/typst-library/src/pdf/mod.rs @@ -12,7 +12,7 @@ pub static PDF: Category; /// Hook up the `pdf` module. pub(super) fn define(global: &mut Scope) { - global.category(PDF); + global.start_category(PDF); global.define("pdf", module()); } diff --git a/crates/typst-library/src/symbols.rs b/crates/typst-library/src/symbols.rs index 1617d3aa8..aee7fb83e 100644 --- a/crates/typst-library/src/symbols.rs +++ b/crates/typst-library/src/symbols.rs @@ -39,7 +39,7 @@ fn extend_scope_from_codex_module(scope: &mut Scope, module: codex::Module) { /// Hook up all `symbol` definitions. pub(super) fn define(global: &mut Scope) { - global.category(SYMBOLS); + global.start_category(SYMBOLS); extend_scope_from_codex_module(global, codex::ROOT); } diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index edbd24139..f506397e1 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -63,7 +63,7 @@ pub static TEXT: Category; /// Hook up all `text` definitions. pub(super) fn define(global: &mut Scope) { - global.category(TEXT); + global.start_category(TEXT); global.define_elem::(); global.define_elem::(); global.define_elem::(); diff --git a/crates/typst-library/src/visualize/mod.rs b/crates/typst-library/src/visualize/mod.rs index 431191491..b0e627af2 100644 --- a/crates/typst-library/src/visualize/mod.rs +++ b/crates/typst-library/src/visualize/mod.rs @@ -36,7 +36,7 @@ pub static VISUALIZE: Category; /// Hook up all visualize definitions. pub(super) fn define(global: &mut Scope) { - global.category(VISUALIZE); + global.start_category(VISUALIZE); global.define_type::(); global.define_type::(); global.define_type::(); diff --git a/docs/src/lib.rs b/docs/src/lib.rs index 2751500e3..004c237c0 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -16,6 +16,7 @@ use serde::Deserialize; use serde_yaml as yaml; use std::sync::LazyLock; use typst::diag::{bail, StrResult}; +use typst::foundations::Binding; use typst::foundations::{ AutoValue, Bytes, CastInfo, Category, Func, Module, NoneValue, ParamInfo, Repr, Scope, Smart, Type, Value, FOUNDATIONS, @@ -47,8 +48,8 @@ static GROUPS: LazyLock> = LazyLock::new(|| { .module() .scope() .iter() - .filter(|(_, v, _)| matches!(v, Value::Func(_))) - .map(|(k, _, _)| k.clone()) + .filter(|(_, b)| matches!(b.read(), Value::Func(_))) + .map(|(k, _)| k.clone()) .collect(); } } @@ -60,7 +61,7 @@ static LIBRARY: LazyLock> = LazyLock::new(|| { let scope = lib.global.scope_mut(); // Add those types, so that they show up in the docs. - scope.category(FOUNDATIONS); + scope.start_category(FOUNDATIONS); scope.define_type::(); scope.define_type::(); @@ -270,8 +271,8 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { // Add values and types. let scope = module.scope(); - for (name, value, _) in scope.iter() { - if scope.get_category(name) != Some(category) { + for (name, binding) in scope.iter() { + if binding.category() != Some(category) { continue; } @@ -279,7 +280,7 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { continue; } - match value { + match binding.read() { Value::Func(func) => { let name = func.name().unwrap(); @@ -476,8 +477,8 @@ fn casts( fn scope_models(resolver: &dyn Resolver, name: &str, scope: &Scope) -> Vec { scope .iter() - .filter_map(|(_, value, _)| { - let Value::Func(func) = value else { return None }; + .filter_map(|(_, binding)| { + let Value::Func(func) = binding.read() else { return None }; Some(func_model(resolver, func, &[name], true)) }) .collect() @@ -554,7 +555,7 @@ fn group_page( let mut outline_items = vec![]; for name in &group.filter { - let value = group.module().scope().get(name).unwrap(); + let value = group.module().scope().get(name).unwrap().read(); let Ok(ref func) = value.clone().cast::() else { panic!("not a function") }; let func = func_model(resolver, func, &path, true); let id_base = urlify(&eco_format!("functions-{}", func.name)); @@ -662,8 +663,8 @@ fn symbols_page(resolver: &dyn Resolver, parent: &str, group: &GroupData) -> Pag /// Produce a symbol list's model. fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel { let mut list = vec![]; - for (name, value, _) in group.module().scope().iter() { - let Value::Symbol(symbol) = value else { continue }; + for (name, binding) in group.module().scope().iter() { + let Value::Symbol(symbol) = binding.read() else { continue }; let complete = |variant: &str| { if variant.is_empty() { name.clone() @@ -703,7 +704,7 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel { /// Extract a module from another module. #[track_caller] fn get_module<'a>(parent: &'a Module, name: &str) -> StrResult<&'a Module> { - match parent.scope().get(name) { + match parent.scope().get(name).map(Binding::read) { Some(Value::Module(module)) => Ok(module), _ => bail!("module doesn't contain module `{name}`"), } diff --git a/docs/src/link.rs b/docs/src/link.rs index 375cc8c2b..c7222b8e1 100644 --- a/docs/src/link.rs +++ b/docs/src/link.rs @@ -1,5 +1,5 @@ use typst::diag::{bail, StrResult}; -use typst::foundations::Func; +use typst::foundations::{Binding, Func}; use crate::{get_module, GROUPS, LIBRARY}; @@ -59,7 +59,7 @@ fn resolve_definition(head: &str, base: &str) -> StrResult { while let Some(name) = parts.peek() { if category.is_none() { - category = focus.scope().get_category(name); + category = focus.scope().get(name).and_then(Binding::category); } let Ok(module) = get_module(focus, name) else { break }; focus = module; From 5b3593e571826ae44a3aeb0e0f6f09face7291ac Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 3 Feb 2025 18:06:45 +0100 Subject: [PATCH 003/172] Enable HTML feature in docs generator (#5800) --- docs/src/lib.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/src/lib.rs b/docs/src/lib.rs index 004c237c0..ff745c9c2 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -21,6 +21,7 @@ use typst::foundations::{ AutoValue, Bytes, CastInfo, Category, Func, Module, NoneValue, ParamInfo, Repr, Scope, Smart, Type, Value, FOUNDATIONS, }; +use typst::html::HTML; use typst::introspection::INTROSPECTION; use typst::layout::{Abs, Margin, PageElem, PagedDocument, LAYOUT}; use typst::loading::DATA_LOADING; @@ -31,7 +32,7 @@ use typst::symbols::SYMBOLS; use typst::text::{Font, FontBook, TEXT}; use typst::utils::LazyHash; use typst::visualize::VISUALIZE; -use typst::Library; +use typst::{Feature, Library, LibraryBuilder}; macro_rules! load { ($path:literal) => { @@ -57,7 +58,9 @@ static GROUPS: LazyLock> = LazyLock::new(|| { }); static LIBRARY: LazyLock> = LazyLock::new(|| { - let mut lib = Library::default(); + let mut lib = LibraryBuilder::default() + .with_features([Feature::Html].into_iter().collect()) + .build(); let scope = lib.global.scope_mut(); // Add those types, so that they show up in the docs. @@ -166,6 +169,7 @@ fn reference_pages(resolver: &dyn Resolver) -> PageModel { category_page(resolver, INTROSPECTION), category_page(resolver, DATA_LOADING), category_page(resolver, PDF), + category_page(resolver, HTML), ]; page } From 50ccd7d60f078f3617bfed5c4e8e1fd7d45ec340 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 4 Feb 2025 10:38:31 +0100 Subject: [PATCH 004/172] Scope deprecations (#5798) --- crates/typst-eval/src/call.rs | 10 +++-- crates/typst-eval/src/code.rs | 11 ++++- crates/typst-eval/src/math.rs | 8 +++- crates/typst-ide/src/complete.rs | 2 +- crates/typst-library/src/diag.rs | 18 ++++++++ crates/typst-library/src/foundations/func.rs | 10 +++-- .../typst-library/src/foundations/module.rs | 6 +-- crates/typst-library/src/foundations/scope.rs | 28 +++++++++++- crates/typst-library/src/foundations/ty.rs | 10 +++-- crates/typst-library/src/foundations/value.rs | 10 ++--- crates/typst-library/src/loading/cbor.rs | 1 + crates/typst-library/src/loading/csv.rs | 1 + crates/typst-library/src/loading/json.rs | 1 + crates/typst-library/src/loading/toml.rs | 1 + crates/typst-library/src/loading/xml.rs | 1 + crates/typst-library/src/loading/yaml.rs | 1 + .../typst-library/src/visualize/image/mod.rs | 1 + crates/typst-library/src/visualize/mod.rs | 12 +++--- crates/typst-library/src/visualize/path.rs | 2 +- crates/typst-macros/src/scope.rs | 43 +++++++++++++------ docs/src/link.rs | 4 +- tests/suite/loading/cbor.typ | 3 ++ tests/suite/loading/csv.typ | 4 ++ tests/suite/loading/json.typ | 4 ++ tests/suite/loading/toml.typ | 4 ++ tests/suite/loading/xml.typ | 4 ++ tests/suite/loading/yaml.typ | 4 ++ tests/suite/visualize/image.typ | 5 +++ tests/suite/visualize/path.typ | 11 +++++ tests/suite/visualize/tiling.typ | 2 + 30 files changed, 179 insertions(+), 43 deletions(-) create mode 100644 tests/suite/loading/cbor.typ diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index 6f0ec1fc9..c68bef963 100644 --- a/crates/typst-eval/src/call.rs +++ b/crates/typst-eval/src/call.rs @@ -315,13 +315,15 @@ fn eval_field_call( (target, args) }; + let field_span = field.span(); + let sink = (&mut vm.engine, field_span); if let Some(callee) = target.ty().scope().get(&field) { args.insert(0, target_expr.span(), target); - Ok(FieldCall::Normal(callee.read().clone(), args)) + Ok(FieldCall::Normal(callee.read_checked(sink).clone(), args)) } else if let Value::Content(content) = &target { if let Some(callee) = content.elem().scope().get(&field) { args.insert(0, target_expr.span(), target); - Ok(FieldCall::Normal(callee.read().clone(), args)) + Ok(FieldCall::Normal(callee.read_checked(sink).clone(), args)) } else { bail!(missing_field_call_error(target, field)) } @@ -331,7 +333,7 @@ fn eval_field_call( ) { // Certain value types may have their own ways to access method fields. // e.g. `$arrow.r(v)$`, `table.cell[..]` - let value = target.field(&field).at(field.span())?; + let value = target.field(&field, sink).at(field_span)?; Ok(FieldCall::Normal(value, args)) } else { // Otherwise we cannot call this field. @@ -364,7 +366,7 @@ fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic { field.as_str(), )); } - _ if target.field(&field).is_ok() => { + _ if target.field(&field, ()).is_ok() => { error.hint(eco_format!( "did you mean to access the field `{}`?", field.as_str(), diff --git a/crates/typst-eval/src/code.rs b/crates/typst-eval/src/code.rs index 4ac481865..a7b6b6f90 100644 --- a/crates/typst-eval/src/code.rs +++ b/crates/typst-eval/src/code.rs @@ -154,7 +154,13 @@ impl Eval for ast::Ident<'_> { type Output = Value; fn eval(self, vm: &mut Vm) -> SourceResult { - Ok(vm.scopes.get(&self).at(self.span())?.read().clone()) + let span = self.span(); + Ok(vm + .scopes + .get(&self) + .at(span)? + .read_checked((&mut vm.engine, span)) + .clone()) } } @@ -310,8 +316,9 @@ impl Eval for ast::FieldAccess<'_> { fn eval(self, vm: &mut Vm) -> SourceResult { let value = self.target().eval(vm)?; let field = self.field(); + let field_span = field.span(); - let err = match value.field(&field).at(field.span()) { + let err = match value.field(&field, (&mut vm.engine, field_span)).at(field_span) { Ok(value) => return Ok(value), Err(err) => err, }; diff --git a/crates/typst-eval/src/math.rs b/crates/typst-eval/src/math.rs index 23b293f26..0e271a089 100644 --- a/crates/typst-eval/src/math.rs +++ b/crates/typst-eval/src/math.rs @@ -35,7 +35,13 @@ impl Eval for ast::MathIdent<'_> { type Output = Value; fn eval(self, vm: &mut Vm) -> SourceResult { - Ok(vm.scopes.get_in_math(&self).at(self.span())?.read().clone()) + let span = self.span(); + Ok(vm + .scopes + .get_in_math(&self) + .at(span)? + .read_checked((&mut vm.engine, span)) + .clone()) } } diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index f68c925d4..c1f08cf09 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -414,7 +414,7 @@ fn field_access_completions( // with method syntax; // 2. We can unwrap the field's value since it's a field belonging to // this value's type, so accessing it should not fail. - ctx.value_completion(field, &value.field(field).unwrap()); + ctx.value_completion(field, &value.field(field, ()).unwrap()); } match value { diff --git a/crates/typst-library/src/diag.rs b/crates/typst-library/src/diag.rs index bd4c90a15..49cbd02c6 100644 --- a/crates/typst-library/src/diag.rs +++ b/crates/typst-library/src/diag.rs @@ -11,6 +11,7 @@ use ecow::{eco_vec, EcoVec}; use typst_syntax::package::{PackageSpec, PackageVersion}; use typst_syntax::{Span, Spanned, SyntaxError}; +use crate::engine::Engine; use crate::{World, WorldExt}; /// Early-return with a [`StrResult`] or [`SourceResult`]. @@ -228,6 +229,23 @@ impl From for SourceDiagnostic { } } +/// Destination for a deprecation message when accessing a deprecated value. +pub trait DeprecationSink { + /// Emits the given deprecation message into this sink. + fn emit(self, message: &str); +} + +impl DeprecationSink for () { + fn emit(self, _: &str) {} +} + +impl DeprecationSink for (&mut Engine<'_>, Span) { + /// Emits the deprecation message as a warning. + fn emit(self, message: &str) { + self.0.sink.warn(SourceDiagnostic::warning(self.1, message)); + } +} + /// A part of a diagnostic's [trace](SourceDiagnostic::trace). #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Tracepoint { diff --git a/crates/typst-library/src/foundations/func.rs b/crates/typst-library/src/foundations/func.rs index 741b66331..3ed1562f6 100644 --- a/crates/typst-library/src/foundations/func.rs +++ b/crates/typst-library/src/foundations/func.rs @@ -9,7 +9,7 @@ use ecow::{eco_format, EcoString}; use typst_syntax::{ast, Span, SyntaxNode}; use typst_utils::{singleton, LazyHash, Static}; -use crate::diag::{bail, At, SourceResult, StrResult}; +use crate::diag::{bail, At, DeprecationSink, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ cast, repr, scope, ty, Args, Bytes, CastInfo, Content, Context, Element, IntoArgs, @@ -255,11 +255,15 @@ impl Func { } /// Get a field from this function's scope, if possible. - pub fn field(&self, field: &str) -> StrResult<&'static Value> { + pub fn field( + &self, + field: &str, + sink: impl DeprecationSink, + ) -> StrResult<&'static Value> { let scope = self.scope().ok_or("cannot access fields on user-defined functions")?; match scope.get(field) { - Some(binding) => Ok(binding.read()), + Some(binding) => Ok(binding.read_checked(sink)), None => match self.name() { Some(name) => bail!("function `{name}` does not contain field `{field}`"), None => bail!("function does not contain field `{field}`"), diff --git a/crates/typst-library/src/foundations/module.rs b/crates/typst-library/src/foundations/module.rs index 3259c17e6..8d9626a1a 100644 --- a/crates/typst-library/src/foundations/module.rs +++ b/crates/typst-library/src/foundations/module.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use ecow::{eco_format, EcoString}; use typst_syntax::FileId; -use crate::diag::{bail, StrResult}; +use crate::diag::{bail, DeprecationSink, StrResult}; use crate::foundations::{repr, ty, Content, Scope, Value}; /// An module of definitions. @@ -118,9 +118,9 @@ impl Module { } /// Try to access a definition in the module. - pub fn field(&self, field: &str) -> StrResult<&Value> { + pub fn field(&self, field: &str, sink: impl DeprecationSink) -> StrResult<&Value> { match self.scope().get(field) { - Some(binding) => Ok(binding.read()), + Some(binding) => Ok(binding.read_checked(sink)), None => match &self.name { Some(name) => bail!("module `{name}` does not contain `{field}`"), None => bail!("module does not contain `{field}`"), diff --git a/crates/typst-library/src/foundations/scope.rs b/crates/typst-library/src/foundations/scope.rs index e73afeacd..d6c5a8d05 100644 --- a/crates/typst-library/src/foundations/scope.rs +++ b/crates/typst-library/src/foundations/scope.rs @@ -10,7 +10,7 @@ use indexmap::IndexMap; use typst_syntax::Span; use typst_utils::Static; -use crate::diag::{bail, HintedStrResult, HintedString, StrResult}; +use crate::diag::{bail, DeprecationSink, HintedStrResult, HintedString, StrResult}; use crate::foundations::{ Element, Func, IntoValue, NativeElement, NativeFunc, NativeFuncData, NativeType, Type, Value, @@ -258,6 +258,8 @@ pub struct Binding { span: Span, /// The category of the binding. category: Option, + /// A deprecation message for the definition. + deprecation: Option<&'static str>, } /// The different kinds of slots. @@ -277,6 +279,7 @@ impl Binding { span, kind: BindingKind::Normal, category: None, + deprecation: None, } } @@ -285,11 +288,29 @@ impl Binding { Self::new(value, Span::detached()) } + /// Marks this binding as deprecated, with the given `message`. + pub fn deprecated(&mut self, message: &'static str) -> &mut Self { + self.deprecation = Some(message); + self + } + /// Read the value. pub fn read(&self) -> &Value { &self.value } + /// Read the value, checking for deprecation. + /// + /// As the `sink` + /// - pass `()` to ignore the message. + /// - pass `(&mut engine, span)` to emit a warning into the engine. + pub fn read_checked(&self, sink: impl DeprecationSink) -> &Value { + if let Some(message) = self.deprecation { + sink.emit(message); + } + &self.value + } + /// Try to write to the value. /// /// This fails if the value is a read-only closure capture. @@ -320,6 +341,11 @@ impl Binding { self.span } + /// A deprecation message for the value, if any. + pub fn deprecation(&self) -> Option<&'static str> { + self.deprecation + } + /// The category of the value, if any. pub fn category(&self) -> Option { self.category diff --git a/crates/typst-library/src/foundations/ty.rs b/crates/typst-library/src/foundations/ty.rs index 09f5efa1e..40f7003c3 100644 --- a/crates/typst-library/src/foundations/ty.rs +++ b/crates/typst-library/src/foundations/ty.rs @@ -8,7 +8,7 @@ use std::sync::LazyLock; use ecow::{eco_format, EcoString}; use typst_utils::Static; -use crate::diag::{bail, StrResult}; +use crate::diag::{bail, DeprecationSink, StrResult}; use crate::foundations::{ cast, func, AutoValue, Func, NativeFuncData, NoneValue, Repr, Scope, Value, }; @@ -94,9 +94,13 @@ impl Type { } /// Get a field from this type's scope, if possible. - pub fn field(&self, field: &str) -> StrResult<&'static Value> { + pub fn field( + &self, + field: &str, + sink: impl DeprecationSink, + ) -> StrResult<&'static Value> { match self.scope().get(field) { - Some(binding) => Ok(binding.read()), + Some(binding) => Ok(binding.read_checked(sink)), None => bail!("type {self} does not contain field `{field}`"), } } diff --git a/crates/typst-library/src/foundations/value.rs b/crates/typst-library/src/foundations/value.rs index 4fa380b46..854c2486e 100644 --- a/crates/typst-library/src/foundations/value.rs +++ b/crates/typst-library/src/foundations/value.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use typst_syntax::{ast, Span}; use typst_utils::ArcExt; -use crate::diag::{HintedStrResult, HintedString, StrResult}; +use crate::diag::{DeprecationSink, HintedStrResult, HintedString, StrResult}; use crate::foundations::{ fields, ops, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime, Decimal, Dict, Duration, Fold, FromValue, Func, IntoValue, Label, Module, @@ -155,15 +155,15 @@ impl Value { } /// Try to access a field on the value. - pub fn field(&self, field: &str) -> StrResult { + pub fn field(&self, field: &str, sink: impl DeprecationSink) -> StrResult { match self { Self::Symbol(symbol) => symbol.clone().modified(field).map(Self::Symbol), Self::Version(version) => version.component(field).map(Self::Int), Self::Dict(dict) => dict.get(field).cloned(), Self::Content(content) => content.field_by_name(field), - Self::Type(ty) => ty.field(field).cloned(), - Self::Func(func) => func.field(field).cloned(), - Self::Module(module) => module.field(field).cloned(), + Self::Type(ty) => ty.field(field, sink).cloned(), + Self::Func(func) => func.field(field, sink).cloned(), + Self::Module(module) => module.field(field, sink).cloned(), _ => fields::field(self, field), } } diff --git a/crates/typst-library/src/loading/cbor.rs b/crates/typst-library/src/loading/cbor.rs index 2bdeb80ef..bd65e8442 100644 --- a/crates/typst-library/src/loading/cbor.rs +++ b/crates/typst-library/src/loading/cbor.rs @@ -38,6 +38,7 @@ impl cbor { /// This function is deprecated. The [`cbor`] function now accepts bytes /// directly. #[func(title = "Decode CBOR")] + #[deprecated = "`cbor.decode` is deprecated, directly pass bytes to `cbor` instead"] pub fn decode( engine: &mut Engine, /// CBOR data. diff --git a/crates/typst-library/src/loading/csv.rs b/crates/typst-library/src/loading/csv.rs index 1cf656ae2..d01d687ba 100644 --- a/crates/typst-library/src/loading/csv.rs +++ b/crates/typst-library/src/loading/csv.rs @@ -100,6 +100,7 @@ impl csv { /// This function is deprecated. The [`csv`] function now accepts bytes /// directly. #[func(title = "Decode CSV")] + #[deprecated = "`csv.decode` is deprecated, directly pass bytes to `csv` instead"] pub fn decode( engine: &mut Engine, /// CSV data. diff --git a/crates/typst-library/src/loading/json.rs b/crates/typst-library/src/loading/json.rs index 035c5e4a7..52c87371f 100644 --- a/crates/typst-library/src/loading/json.rs +++ b/crates/typst-library/src/loading/json.rs @@ -69,6 +69,7 @@ impl json { /// This function is deprecated. The [`json`] function now accepts bytes /// directly. #[func(title = "Decode JSON")] + #[deprecated = "`json.decode` is deprecated, directly pass bytes to `json` instead"] pub fn decode( engine: &mut Engine, /// JSON data. diff --git a/crates/typst-library/src/loading/toml.rs b/crates/typst-library/src/loading/toml.rs index 402207b02..456112463 100644 --- a/crates/typst-library/src/loading/toml.rs +++ b/crates/typst-library/src/loading/toml.rs @@ -48,6 +48,7 @@ impl toml { /// This function is deprecated. The [`toml`] function now accepts bytes /// directly. #[func(title = "Decode TOML")] + #[deprecated = "`toml.decode` is deprecated, directly pass bytes to `toml` instead"] pub fn decode( engine: &mut Engine, /// TOML data. diff --git a/crates/typst-library/src/loading/xml.rs b/crates/typst-library/src/loading/xml.rs index ca467c238..0172071be 100644 --- a/crates/typst-library/src/loading/xml.rs +++ b/crates/typst-library/src/loading/xml.rs @@ -81,6 +81,7 @@ impl xml { /// This function is deprecated. The [`xml`] function now accepts bytes /// directly. #[func(title = "Decode XML")] + #[deprecated = "`xml.decode` is deprecated, directly pass bytes to `xml` instead"] pub fn decode( engine: &mut Engine, /// XML data. diff --git a/crates/typst-library/src/loading/yaml.rs b/crates/typst-library/src/loading/yaml.rs index 5767cb640..511c676cb 100644 --- a/crates/typst-library/src/loading/yaml.rs +++ b/crates/typst-library/src/loading/yaml.rs @@ -59,6 +59,7 @@ impl yaml { /// This function is deprecated. The [`yaml`] function now accepts bytes /// directly. #[func(title = "Decode YAML")] + #[deprecated = "`yaml.decode` is deprecated, directly pass bytes to `yaml` instead"] pub fn decode( engine: &mut Engine, /// YAML data. diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 07ebdabe2..9306eb6f2 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -171,6 +171,7 @@ impl ImageElem { /// #image.decode(changed) /// ``` #[func(title = "Decode Image")] + #[deprecated = "`image.decode` is deprecated, directly pass bytes to `image` instead"] pub fn decode( span: Span, /// The data to decode as an image. Can be a string for SVGs. diff --git a/crates/typst-library/src/visualize/mod.rs b/crates/typst-library/src/visualize/mod.rs index b0e627af2..76849ac86 100644 --- a/crates/typst-library/src/visualize/mod.rs +++ b/crates/typst-library/src/visualize/mod.rs @@ -24,7 +24,7 @@ pub use self::shape::*; pub use self::stroke::*; pub use self::tiling::*; -use crate::foundations::{category, Category, Scope, Type}; +use crate::foundations::{category, Category, Element, Scope, Type}; /// Drawing and data visualization. /// @@ -49,8 +49,10 @@ pub(super) fn define(global: &mut Scope) { global.define_elem::(); global.define_elem::(); global.define_elem::(); - global.define_elem::(); - - // Compatibility. - global.define("pattern", Type::of::()); + global + .define("path", Element::of::()) + .deprecated("the `path` function is deprecated, use `curve` instead"); + global + .define("pattern", Type::of::()) + .deprecated("the name `pattern` is deprecated, use `tiling` instead"); } diff --git a/crates/typst-library/src/visualize/path.rs b/crates/typst-library/src/visualize/path.rs index 6aacb3198..5d3439c08 100644 --- a/crates/typst-library/src/visualize/path.rs +++ b/crates/typst-library/src/visualize/path.rs @@ -23,7 +23,7 @@ use crate::visualize::{FillRule, Paint, Stroke}; /// ``` /// /// # Deprecation -/// This element is deprecated. The [`curve`] element should be used instead. +/// This function is deprecated. The [`curve`] function should be used instead. #[elem(Show)] pub struct PathElem { /// How to fill the path. diff --git a/crates/typst-macros/src/scope.rs b/crates/typst-macros/src/scope.rs index 8a2f1ce61..392ab1a53 100644 --- a/crates/typst-macros/src/scope.rs +++ b/crates/typst-macros/src/scope.rs @@ -31,18 +31,37 @@ pub fn scope(_: TokenStream, item: syn::Item) -> Result { let mut definitions = vec![]; let mut constructor = quote! { None }; for child in &mut item.items { - let def = match child { - syn::ImplItem::Const(item) => handle_const(&self_ty_expr, item)?, - syn::ImplItem::Fn(item) => match handle_fn(self_ty, item)? { - FnKind::Member(tokens) => tokens, - FnKind::Constructor(tokens) => { - constructor = tokens; - continue; - } - }, - syn::ImplItem::Verbatim(item) => handle_type_or_elem(item)?, + let bare: BareType; + let (mut def, attrs) = match child { + syn::ImplItem::Const(item) => { + (handle_const(&self_ty_expr, item)?, &item.attrs) + } + syn::ImplItem::Fn(item) => ( + match handle_fn(self_ty, item)? { + FnKind::Member(tokens) => tokens, + FnKind::Constructor(tokens) => { + constructor = tokens; + continue; + } + }, + &item.attrs, + ), + syn::ImplItem::Verbatim(item) => { + bare = syn::parse2(item.clone())?; + (handle_type_or_elem(&bare)?, &bare.attrs) + } _ => bail!(child, "unexpected item in scope"), }; + + if let Some(message) = attrs.iter().find_map(|attr| match &attr.meta { + syn::Meta::NameValue(pair) if pair.path.is_ident("deprecated") => { + Some(&pair.value) + } + _ => None, + }) { + def = quote! { #def.deprecated(#message) } + } + definitions.push(def); } @@ -61,6 +80,7 @@ pub fn scope(_: TokenStream, item: syn::Item) -> Result { #constructor } + #[allow(deprecated)] fn scope() -> #foundations::Scope { let mut scope = #foundations::Scope::deduplicating(); #(#definitions;)* @@ -78,8 +98,7 @@ fn handle_const(self_ty: &TokenStream, item: &syn::ImplItemConst) -> Result Result { - let item: BareType = syn::parse2(item.clone())?; +fn handle_type_or_elem(item: &BareType) -> Result { let ident = &item.ident; let define = if item.attrs.iter().any(|attr| attr.path().is_ident("elem")) { quote! { define_elem } diff --git a/docs/src/link.rs b/docs/src/link.rs index c7222b8e1..c55261b84 100644 --- a/docs/src/link.rs +++ b/docs/src/link.rs @@ -69,7 +69,7 @@ fn resolve_definition(head: &str, base: &str) -> StrResult { let Some(category) = category else { bail!("{head} has no category") }; let name = parts.next().ok_or("link is missing first part")?; - let value = focus.field(name)?; + let value = focus.field(name, ())?; // Handle grouped functions. if let Some(group) = GROUPS.iter().find(|group| { @@ -88,7 +88,7 @@ fn resolve_definition(head: &str, base: &str) -> StrResult { let mut route = format!("{}reference/{}/{name}", base, category.name()); if let Some(next) = parts.next() { - if let Ok(field) = value.field(next) { + if let Ok(field) = value.field(next, ()) { route.push_str("/#definitions-"); route.push_str(next); if let Some(next) = parts.next() { diff --git a/tests/suite/loading/cbor.typ b/tests/suite/loading/cbor.typ new file mode 100644 index 000000000..4b50bb9c3 --- /dev/null +++ b/tests/suite/loading/cbor.typ @@ -0,0 +1,3 @@ +--- cbor-decode-deprecated --- +// Warning: 15-21 `cbor.decode` is deprecated, directly pass bytes to `cbor` instead +#let _ = cbor.decode diff --git a/tests/suite/loading/csv.typ b/tests/suite/loading/csv.typ index 93545fc49..6f57ec458 100644 --- a/tests/suite/loading/csv.typ +++ b/tests/suite/loading/csv.typ @@ -29,3 +29,7 @@ --- csv-invalid-delimiter --- // Error: 41-51 delimiter must be an ASCII character #csv("/assets/data/zoo.csv", delimiter: "\u{2008}") + +--- csv-decode-deprecated --- +// Warning: 14-20 `csv.decode` is deprecated, directly pass bytes to `csv` instead +#let _ = csv.decode diff --git a/tests/suite/loading/json.typ b/tests/suite/loading/json.typ index 3ebeaf2f7..c8df1ff6e 100644 --- a/tests/suite/loading/json.typ +++ b/tests/suite/loading/json.typ @@ -9,6 +9,10 @@ // Error: 7-30 failed to parse JSON (expected value at line 3 column 14) #json("/assets/data/bad.json") +--- json-decode-deprecated --- +// Warning: 15-21 `json.decode` is deprecated, directly pass bytes to `json` instead +#let _ = json.decode + --- issue-3363-json-large-number --- // Big numbers (larger than what i64 can store) should just lose some precision // but not overflow diff --git a/tests/suite/loading/toml.typ b/tests/suite/loading/toml.typ index 855ca995d..a4318a015 100644 --- a/tests/suite/loading/toml.typ +++ b/tests/suite/loading/toml.typ @@ -39,3 +39,7 @@ --- toml-invalid --- // Error: 7-30 failed to parse TOML (expected `.`, `=` at line 1 column 16) #toml("/assets/data/bad.toml") + +--- toml-decode-deprecated --- +// Warning: 15-21 `toml.decode` is deprecated, directly pass bytes to `toml` instead +#let _ = toml.decode diff --git a/tests/suite/loading/xml.typ b/tests/suite/loading/xml.typ index 41cd20e74..933f3c480 100644 --- a/tests/suite/loading/xml.typ +++ b/tests/suite/loading/xml.typ @@ -26,3 +26,7 @@ --- xml-invalid --- // Error: 6-28 failed to parse XML (found closing tag 'data' instead of 'hello' in line 3) #xml("/assets/data/bad.xml") + +--- xml-decode-deprecated --- +// Warning: 14-20 `xml.decode` is deprecated, directly pass bytes to `xml` instead +#let _ = xml.decode diff --git a/tests/suite/loading/yaml.typ b/tests/suite/loading/yaml.typ index bbfea41cb..a8089052c 100644 --- a/tests/suite/loading/yaml.typ +++ b/tests/suite/loading/yaml.typ @@ -15,3 +15,7 @@ --- yaml-invalid --- // Error: 7-30 failed to parse YAML (did not find expected ',' or ']' at line 2 column 1, while parsing a flow sequence at line 1 column 18) #yaml("/assets/data/bad.yaml") + +--- yaml-decode-deprecated --- +// Warning: 15-21 `yaml.decode` is deprecated, directly pass bytes to `yaml` instead +#let _ = yaml.decode diff --git a/tests/suite/visualize/image.typ b/tests/suite/visualize/image.typ index 6f6e1a157..e37932f28 100644 --- a/tests/suite/visualize/image.typ +++ b/tests/suite/visualize/image.typ @@ -161,22 +161,27 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B --- image-decode-svg --- // Test parsing from svg data +// Warning: 8-14 `image.decode` is deprecated, directly pass bytes to `image` instead #image.decode(``.text, format: "svg") --- image-decode-bad-svg --- // Error: 2-168 failed to parse SVG (missing root node) +// Warning: 8-14 `image.decode` is deprecated, directly pass bytes to `image` instead #image.decode(``.text, format: "svg") --- image-decode-detect-format --- // Test format auto detect +// Warning: 8-14 `image.decode` is deprecated, directly pass bytes to `image` instead #image.decode(read("/assets/images/tiger.jpg", encoding: none), width: 80%) --- image-decode-specify-format --- // Test format manual +// Warning: 8-14 `image.decode` is deprecated, directly pass bytes to `image` instead #image.decode(read("/assets/images/tiger.jpg", encoding: none), format: "jpg", width: 80%) --- image-decode-specify-wrong-format --- // Error: 2-91 failed to decode image (Format error decoding Png: Invalid PNG signature.) +// Warning: 8-14 `image.decode` is deprecated, directly pass bytes to `image` instead #image.decode(read("/assets/images/tiger.jpg", encoding: none), format: "png", width: 80%) --- image-pixmap-empty --- diff --git a/tests/suite/visualize/path.typ b/tests/suite/visualize/path.typ index 55c0f5340..e44b2270e 100644 --- a/tests/suite/visualize/path.typ +++ b/tests/suite/visualize/path.typ @@ -6,6 +6,7 @@ columns: (1fr, 1fr), rows: (1fr, 1fr, 1fr), align: center + horizon, + // Warning: 3-7 the `path` function is deprecated, use `curve` instead path( fill: red, closed: true, @@ -14,6 +15,7 @@ ((0%, 50%), (4%, 4%)), ((50%, 0%), (4%, 4%)), ), + // Warning: 3-7 the `path` function is deprecated, use `curve` instead path( fill: purple, stroke: 1pt, @@ -22,6 +24,7 @@ (0pt, 30pt), (30pt, 0pt), ), + // Warning: 3-7 the `path` function is deprecated, use `curve` instead path( fill: blue, stroke: 1pt, @@ -30,6 +33,7 @@ ((30%, 60%), (-20%, 0%), (0%, 0%)), ((50%, 30%), (60%, -30%), (60%, 0%)), ), + // Warning: 3-7 the `path` function is deprecated, use `curve` instead path( stroke: 5pt, closed: true, @@ -37,6 +41,7 @@ (30pt, 30pt), (15pt, 0pt), ), + // Warning: 3-7 the `path` function is deprecated, use `curve` instead path( fill: red, fill-rule: "non-zero", @@ -47,6 +52,7 @@ (0pt, 20pt), (40pt, 50pt), ), + // Warning: 3-7 the `path` function is deprecated, use `curve` instead path( fill: red, fill-rule: "even-odd", @@ -61,18 +67,22 @@ --- path-bad-vertex --- // Error: 7-9 path vertex must have 1, 2, or 3 points +// Warning: 2-6 the `path` function is deprecated, use `curve` instead #path(()) --- path-bad-point-count --- // Error: 7-47 path vertex must have 1, 2, or 3 points +// Warning: 2-6 the `path` function is deprecated, use `curve` instead #path(((0%, 0%), (0%, 0%), (0%, 0%), (0%, 0%))) --- path-bad-point-array --- // Error: 7-31 point array must contain exactly two entries +// Warning: 2-6 the `path` function is deprecated, use `curve` instead #path(((0%, 0%), (0%, 0%, 0%))) --- path-infinite-length --- // Error: 2-42 cannot create path with infinite length +// Warning: 2-6 the `path` function is deprecated, use `curve` instead #path((0pt, 0pt), (float.inf * 1pt, 0pt)) --- issue-path-in-sized-container --- @@ -82,6 +92,7 @@ fill: aqua, width: 20pt, height: 15pt, + // Warning: 3-7 the `path` function is deprecated, use `curve` instead path( (0pt, 0pt), (10pt, 10pt), diff --git a/tests/suite/visualize/tiling.typ b/tests/suite/visualize/tiling.typ index 5e61aa43a..904133411 100644 --- a/tests/suite/visualize/tiling.typ +++ b/tests/suite/visualize/tiling.typ @@ -159,5 +159,7 @@ --- tiling-pattern-compatibility --- #set page(width: auto, height: auto, margin: 0pt) + +// Warning: 10-17 the name `pattern` is deprecated, use `tiling` instead #let t = pattern(size: (10pt, 10pt), line(stroke: 4pt, start: (0%, 0%), end: (100%, 100%))) #rect(width: 50pt, height: 50pt, fill: t) From b25cf22018e849c7f52ee107789946f7c271e54e Mon Sep 17 00:00:00 2001 From: Ryan Chua <71936834+Toafu@users.noreply.github.com> Date: Tue, 4 Feb 2025 04:40:10 -0500 Subject: [PATCH 005/172] Fix typo in page documentation (#5804) --- crates/typst-library/src/layout/page.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-library/src/layout/page.rs b/crates/typst-library/src/layout/page.rs index 68fd89745..0964dccd2 100644 --- a/crates/typst-library/src/layout/page.rs +++ b/crates/typst-library/src/layout/page.rs @@ -270,7 +270,7 @@ pub struct PageElem { /// margin: (top: 32pt, bottom: 20pt), /// header: [ /// #set text(8pt) - /// #smallcaps[Typst Academcy] + /// #smallcaps[Typst Academy] /// #h(1fr) _Exercise Sheet 3_ /// ], /// ) From 73ffbdef2b3498307328da355b1d933b1ccf206a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:40:28 +0100 Subject: [PATCH 006/172] Bump openssl from 0.10.66 to 0.10.70 (#5802) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d2e410e14..44006cd14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1566,9 +1566,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" dependencies = [ "bitflags 2.6.0", "cfg-if", @@ -1607,9 +1607,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.103" +version = "0.9.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" dependencies = [ "cc", "libc", From 0ea668077d6a47f64ee3875dbed31f9e8d832ae3 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 4 Feb 2025 11:08:43 +0100 Subject: [PATCH 007/172] Bump codex to 0.1.0 (#5805) --- Cargo.lock | 3 +- Cargo.toml | 2 +- crates/typst-library/src/symbols.rs | 46 +++++++++++++++------------ tests/ref/symbol-sect-deprecated.png | Bin 0 -> 391 bytes tests/suite/symbols/symbol.typ | 4 +++ 5 files changed, 32 insertions(+), 23 deletions(-) create mode 100644 tests/ref/symbol-sect-deprecated.png diff --git a/Cargo.lock b/Cargo.lock index 44006cd14..215731282 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -410,7 +410,8 @@ dependencies = [ [[package]] name = "codex" version = "0.1.0" -source = "git+https://github.com/typst/codex?rev=343a9b1#343a9b199430681ba3ca0e2242097c6419492d55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e0ee2092c3513f63588d51c3f81b98e6b1aa8ddcca3b5892b288f093516497d" [[package]] name = "color-print" diff --git a/Cargo.toml b/Cargo.toml index d03bfa6d1..3550963e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] } clap_complete = "4.2.1" clap_mangen = "0.2.10" codespan-reporting = "0.11" -codex = { git = "https://github.com/typst/codex", rev = "343a9b1" } +codex = "0.1.0" color-print = "0.3.6" comemo = "0.4" csv = "1" diff --git a/crates/typst-library/src/symbols.rs b/crates/typst-library/src/symbols.rs index aee7fb83e..777f8172f 100644 --- a/crates/typst-library/src/symbols.rs +++ b/crates/typst-library/src/symbols.rs @@ -10,6 +10,31 @@ use crate::foundations::{category, Category, Module, Scope, Symbol, Value}; #[category] pub static SYMBOLS: Category; +/// Hook up all `symbol` definitions. +pub(super) fn define(global: &mut Scope) { + global.start_category(SYMBOLS); + extend_scope_from_codex_module(global, codex::ROOT); +} + +/// Hook up all math `symbol` definitions, i.e., elements of the `sym` module. +pub(super) fn define_math(math: &mut Scope) { + extend_scope_from_codex_module(math, codex::SYM); +} + +fn extend_scope_from_codex_module(scope: &mut Scope, module: codex::Module) { + for (name, binding) in module.iter() { + let value = match binding.def { + codex::Def::Symbol(s) => Value::Symbol(s.into()), + codex::Def::Module(m) => Value::Module(Module::new(name, m.into())), + }; + + let scope_binding = scope.define(name, value); + if let Some(message) = binding.deprecation { + scope_binding.deprecated(message); + } + } +} + impl From for Scope { fn from(module: codex::Module) -> Scope { let mut scope = Self::new(); @@ -26,24 +51,3 @@ impl From for Symbol { } } } - -fn extend_scope_from_codex_module(scope: &mut Scope, module: codex::Module) { - for (name, definition) in module.iter() { - let value = match definition { - codex::Def::Symbol(s) => Value::Symbol(s.into()), - codex::Def::Module(m) => Value::Module(Module::new(name, m.into())), - }; - scope.define(name, value); - } -} - -/// Hook up all `symbol` definitions. -pub(super) fn define(global: &mut Scope) { - global.start_category(SYMBOLS); - extend_scope_from_codex_module(global, codex::ROOT); -} - -/// Hook up all math `symbol` definitions, i.e., elements of the `sym` module. -pub(super) fn define_math(math: &mut Scope) { - extend_scope_from_codex_module(math, codex::SYM); -} diff --git a/tests/ref/symbol-sect-deprecated.png b/tests/ref/symbol-sect-deprecated.png new file mode 100644 index 0000000000000000000000000000000000000000..da647d5f7254282a95d1ef707ba7bb211c4e60dc GIT binary patch literal 391 zcmV;20eJq2P)8$rI;HctnB4#jYUoPe(=Zbclf0 zt^eOtFqHjttpZVRD{Aw%K}6L5{XZH)zQy|w=zypRkN^L_yb#3ttMUK;$w&-!3s)q9 zsG9Hp|1UoaVqFRN|9^5H*%tqr`%er+cfg5Lc3Udqe*qaUy20QDBm%ab_Jk?<)emA# zP2L>QMSdJlo;X+FCy?&^4 Date: Tue, 4 Feb 2025 16:22:24 +0100 Subject: [PATCH 008/172] Bump dependencies (#5808) --- Cargo.lock | 843 +++++++++++++++++++++++++++-------------------------- Cargo.toml | 12 +- 2 files changed, 428 insertions(+), 427 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 215731282..8b7754aef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,18 +8,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -46,9 +34,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -61,36 +49,37 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "once_cell", + "windows-sys 0.59.0", ] [[package]] @@ -104,9 +93,9 @@ dependencies = [ [[package]] name = "arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" dependencies = [ "derive_arbitrary", ] @@ -192,9 +181,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" dependencies = [ "serde", ] @@ -213,9 +202,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "by_address" @@ -225,9 +214,9 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.18.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" [[package]] name = "byteorder" @@ -243,9 +232,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "cc" -version = "1.1.24" +version = "1.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812acba72f0a070b003d3697490d2b55b837230ae7c6c6497f05cc2ddbb8d938" +checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf" dependencies = [ "jobserver", "libc", @@ -278,14 +267,14 @@ checksum = "7588475145507237ded760e52bf2f1085495245502033756d28ea72ade0e498b" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -333,9 +322,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.19" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" +checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" dependencies = [ "clap_builder", "clap_derive", @@ -343,9 +332,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.19" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstream", "anstyle", @@ -356,18 +345,18 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.32" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74a01f4f9ee6c066d42a1c8dedf0dcddad16c72a8981a309d6398de3a75b0c39" +checksum = "375f9d8255adeeedd51053574fd8d4ba875ea5fa558e86617b07f09f1680c8b6" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" dependencies = [ "heck", "proc-macro2", @@ -377,15 +366,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clap_mangen" -version = "0.2.23" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17415fd4dfbea46e3274fcd8d368284519b358654772afb700dc2e8d2b24eeb" +checksum = "724842fa9b144f9b89b3f3d371a89f3455eea660361d13a554f68f8ae5d6c13a" dependencies = [ "clap", "roff", @@ -415,18 +404,18 @@ checksum = "2e0ee2092c3513f63588d51c3f81b98e6b1aa8ddcca3b5892b288f093516497d" [[package]] name = "color-print" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee543c60ff3888934877a5671f45494dd27ed4ba25c6670b9a7576b7ed7a8c0" +checksum = "3aa954171903797d5623e047d9ab69d91b493657917bdfb8c2c80ecaf9cdb6f4" dependencies = [ "color-print-proc-macro", ] [[package]] name = "color-print-proc-macro" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ff1a80c5f3cb1ca7c06ffdd71b6a6dd6d8f896c42141fbd43f50ed28dcdb93" +checksum = "692186b5ebe54007e45a59aea47ece9eb4108e141326c304cdc91699a7118a22" dependencies = [ "nom", "proc-macro2", @@ -442,9 +431,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "comemo" @@ -455,7 +444,7 @@ dependencies = [ "comemo-macros", "once_cell", "parking_lot", - "siphasher 1.0.1", + "siphasher", ] [[package]] @@ -487,9 +476,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core_maths" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b02505ccb8c50b0aa21ace0fc08c3e53adebd4e58caa18a36152803c7709a3" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" dependencies = [ "libm", ] @@ -505,18 +494,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -533,21 +522,21 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" [[package]] name = "csv" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" dependencies = [ "csv-core", "itoa", @@ -581,9 +570,9 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", @@ -592,23 +581,23 @@ dependencies = [ [[package]] name = "dirs" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -630,9 +619,9 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "ecow" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54bfbb1708988623190a6c4dbedaeaf0f53c20c6395abd6a01feb327b3146f4b" +checksum = "e42fc0a93992b20c58b99e59d61eaf1635a25bfbe49e4275c34ba0aee98119ba" dependencies = [ "serde", ] @@ -693,12 +682,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -719,15 +708,15 @@ checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" [[package]] name = "fastrand" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fdeflate" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8090f921a24b04994d9929e204f50b498a33ea6ba559ffaa05e04f7ee7fb5ab" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" dependencies = [ "simd-adler32", ] @@ -746,9 +735,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", @@ -766,6 +755,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "fontconfig-parser" version = "0.5.7" @@ -851,7 +846,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets", ] [[package]] @@ -880,20 +887,14 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", -] - [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "foldhash", +] [[package]] name = "hayagriva" @@ -904,12 +905,12 @@ dependencies = [ "biblatex", "ciborium", "citationberg", - "indexmap 2.6.0", + "indexmap 2.7.1", "numerals", "paste", "serde", "serde_yaml 0.9.34+deprecated", - "thiserror", + "thiserror 1.0.69", "unic-langid", "unicode-segmentation", "unscanny", @@ -1003,6 +1004,30 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + [[package]] name = "icu_properties" version = "1.5.1" @@ -1107,12 +1132,23 @@ checksum = "f739ee737260d955e330bc83fdeaaf1631f7fb7ed218761d3c04bb13bb7d79df" [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -1165,9 +1201,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -1175,19 +1211,13 @@ dependencies = [ "serde", ] -[[package]] -name = "indexmap-nostd" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" - [[package]] name = "inotify" -version = "0.9.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.8.0", "inotify-sys", "libc", ] @@ -1228,9 +1258,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jobserver" @@ -1243,18 +1273,19 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] [[package]] name = "kamadak-exif" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4fc70d0ab7e5b6bafa30216a6b48705ea964cdfc29c050f2412295eba58077" +checksum = "1130d80c7374efad55a117d715a3af9368f0fa7a2c54573afc15a188cd984837" dependencies = [ "mutate_once", ] @@ -1291,33 +1322,33 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.159" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libdeflate-sys" -version = "1.21.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b14a6afa4e2e1d343fd793a1c0a7e5857a73a2697c2ff2c98ac00d6c4ecc820" +checksum = "413b667c8a795fcbe6287a75a8ce92b1dae928172c716fe95044cb2ec7877941" dependencies = [ "cc", ] [[package]] name = "libdeflater" -version = "1.21.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17fe2badabdaf756f620748311e99ef99a5fdd681562dfd343fdb16ed7d4797" +checksum = "d78376c917eec0550b9c56c858de50e1b7ebf303116487562e624e63ce51453a" dependencies = [ "libdeflate-sys", ] [[package]] name = "libfuzzer-sys" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b9569d2f74e257076d8c6bfa73fb505b46b851e51ddaecc825944aa3bed17fa" +checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" dependencies = [ "arbitrary", "cc", @@ -1325,9 +1356,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libredox" @@ -1335,7 +1366,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "libc", "redox_syscall", ] @@ -1348,9 +1379,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "lipsum" @@ -1364,9 +1395,9 @@ dependencies = [ [[package]] name = "litemap" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" dependencies = [ "serde", ] @@ -1389,9 +1420,9 @@ checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "lzma-sys" @@ -1427,9 +1458,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" dependencies = [ "adler2", "simd-adler32", @@ -1437,14 +1468,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.11" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "log", - "wasi", - "windows-sys 0.48.0", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", ] [[package]] @@ -1461,9 +1492,9 @@ checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" dependencies = [ "libc", "log", @@ -1488,12 +1519,11 @@ dependencies = [ [[package]] name = "notify" -version = "6.1.1" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943" dependencies = [ - "bitflags 2.6.0", - "crossbeam-channel", + "bitflags 2.8.0", "filetime", "fsevent-sys", "inotify", @@ -1501,10 +1531,17 @@ dependencies = [ "libc", "log", "mio", + "notify-types", "walkdir", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + [[package]] name = "num-bigint" version = "0.4.6" @@ -1547,18 +1584,15 @@ checksum = "e25be21376a772d15f97ae789845340a9651d3c4246ff5ebb6a2b35f9c37bd31" [[package]] name = "once_cell" -version = "1.20.1" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" -dependencies = [ - "portable-atomic", -] +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "open" -version = "5.3.0" +version = "5.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3" +checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" dependencies = [ "is-wsl", "libc", @@ -1571,7 +1605,7 @@ version = "0.10.70" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg-if", "foreign-types", "libc", @@ -1593,15 +1627,15 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" -version = "300.3.2+3.3.2" +version = "300.4.1+3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a211a18d945ef7e648cc6e0058f4c548ee46aab922ea203e0d30e966ea23647b" +checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" dependencies = [ "cc", ] @@ -1627,22 +1661,19 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "oxipng" -version = "9.1.2" +version = "9.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec25597808aff9f632f018f0fe8985c6f670598ac5241d220a9f2d32ff46812e" +checksum = "aa3202b10a7ffac89508bb091fe420048c47926b37c5ff84d78dc8af7044fa86" dependencies = [ "bitvec", - "clap", - "clap_mangen", "crossbeam-channel", "filetime", - "indexmap 2.6.0", + "indexmap 2.7.1", "libdeflater", "log", "rayon", "rgb", "rustc-hash", - "rustc_version", "zopfli", ] @@ -1690,7 +1721,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -1701,17 +1732,17 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pathdiff" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "pdf-writer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be17f48d7fbbd22c6efedb58af5d409aa578e407f40b29a0bcb4e66ed84c5c98" +checksum = "5df03c7d216de06f93f398ef06f1385a60f2c597bb96f8195c8d98e08a26b1d5" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "itoa", "memchr", "ryu", @@ -1725,9 +1756,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "phf" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_macros", "phf_shared", @@ -1735,9 +1766,9 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", "rand", @@ -1745,9 +1776,9 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ "phf_generator", "phf_shared", @@ -1758,11 +1789,11 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher 0.3.11", + "siphasher", ] [[package]] @@ -1793,7 +1824,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64", - "indexmap 2.6.0", + "indexmap 2.7.1", "quick-xml 0.32.0", "serde", "time", @@ -1801,9 +1832,9 @@ dependencies = [ [[package]] name = "png" -version = "0.17.14" +version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -1814,15 +1845,15 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" [[package]] name = "postcard" -version = "1.0.10" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7f0a8d620d71c457dd1d47df76bb18960378da56af4527aaa10f515eee732e" +checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8" dependencies = [ "cobs", "embedded-io 0.4.0", @@ -1847,18 +1878,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] [[package]] name = "psm" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa37f80ca58604976033fae9515a8a2989fc13797d953f7c04fb8fa36a11f205" +checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810" dependencies = [ "cc", ] @@ -1869,7 +1900,7 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "getopts", "memchr", "unicase", @@ -1908,9 +1939,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -1968,29 +1999,29 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] name = "redox_users" -version = "0.4.6" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox", - "thiserror", + "thiserror 2.0.11", ] [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -2000,9 +2031,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -2065,37 +2096,28 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "rustybuzz" @@ -2103,7 +2125,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c85d1ccd519e61834798eb52c4e886e8c2d7d698dd3d6ce0b1b47eb8557f1181" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "bytemuck", "core_maths", "log", @@ -2117,9 +2139,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" [[package]] name = "same-file" @@ -2132,9 +2154,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.24" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] @@ -2151,7 +2173,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "core-foundation", "core-foundation-sys", "libc", @@ -2160,9 +2182,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -2181,24 +2203,24 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", @@ -2207,9 +2229,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" dependencies = [ "itoa", "memchr", @@ -2244,7 +2266,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.7.1", "itoa", "ryu", "serde", @@ -2280,19 +2302,13 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "simplecss" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" dependencies = [ "log", ] -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - [[package]] name = "siphasher" version = "1.0.1" @@ -2350,12 +2366,11 @@ dependencies = [ [[package]] name = "string-interner" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c6a0d765f5807e98a091107bae0a56ea3799f66a5de47b2c84c94a39c09974e" +checksum = "1a3275464d7a9f2d4cac57c89c2ef96a8524dba2864c8d6f82e3980baf136f9b" dependencies = [ - "cfg-if", - "hashbrown 0.14.5", + "hashbrown 0.15.2", "serde", ] @@ -2406,7 +2421,7 @@ dependencies = [ "once_cell", "pdf-writer", "resvg", - "siphasher 1.0.1", + "siphasher", "subsetter", "tiny-skia", "ttf-parser", @@ -2415,19 +2430,19 @@ dependencies = [ [[package]] name = "svgtypes" -version = "0.15.2" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "794de53cc48eaabeed0ab6a3404a65f40b3e38c067e4435883a65d2aa4ca000e" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" dependencies = [ "kurbo", - "siphasher 1.0.1", + "siphasher", ] [[package]] name = "syn" -version = "2.0.79" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -2462,7 +2477,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "thiserror", + "thiserror 1.0.69", "walkdir", "yaml-rust", ] @@ -2475,9 +2490,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ff6c40d3aedb5e06b57c6f669ad17ab063dd1e63d977c6a88e7f4dfa4f04020" +checksum = "c65998313f8e17d0d553d28f91a0df93e4dbbbf770279c7bc21ca0f09ea1a1f6" dependencies = [ "filetime", "libc", @@ -2486,12 +2501,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.13.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" dependencies = [ "cfg-if", "fastrand", + "getrandom 0.3.1", "once_cell", "rustix", "windows-sys 0.59.0", @@ -2508,9 +2524,9 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" +checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" dependencies = [ "rustix", "windows-sys 0.59.0", @@ -2524,18 +2540,38 @@ checksum = "a38c90d48152c236a3ab59271da4f4ae63d678c5d7ad6b7714d7cb9760be5e4b" [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", @@ -2544,9 +2580,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -2565,9 +2601,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", @@ -2624,9 +2660,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] @@ -2660,11 +2696,11 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.7.1", "serde", "serde_spanned", "toml_datetime", @@ -2797,7 +2833,7 @@ dependencies = [ "comemo", "ecow", "if_chain", - "indexmap 2.6.0", + "indexmap 2.7.1", "stacker", "toml", "typst-library", @@ -2907,7 +2943,7 @@ name = "typst-library" version = "0.12.0" dependencies = [ "az", - "bitflags 2.6.0", + "bitflags 2.8.0", "bumpalo", "chinese-number", "ciborium", @@ -2922,7 +2958,7 @@ dependencies = [ "icu_provider", "icu_provider_blob", "image", - "indexmap 2.6.0", + "indexmap 2.7.1", "kamadak-exif", "kurbo", "lipsum", @@ -2939,7 +2975,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml 0.9.34+deprecated", - "siphasher 1.0.1", + "siphasher", "smallvec", "syntect", "time", @@ -2981,7 +3017,7 @@ dependencies = [ "comemo", "ecow", "image", - "indexmap 2.6.0", + "indexmap 2.7.1", "miniz_oxide", "pdf-writer", "serde", @@ -3105,7 +3141,7 @@ dependencies = [ "once_cell", "portable-atomic", "rayon", - "siphasher 1.0.1", + "siphasher", "thin-vec", "unicode-math-class", ] @@ -3131,12 +3167,9 @@ dependencies = [ [[package]] name = "unicase" -version = "2.7.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-bidi" @@ -3158,9 +3191,9 @@ checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "unicode-math-class" @@ -3221,9 +3254,9 @@ checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" [[package]] name = "ureq" -version = "2.10.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" dependencies = [ "base64", "flate2", @@ -3237,9 +3270,9 @@ dependencies = [ [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -3264,7 +3297,7 @@ dependencies = [ "roxmltree", "rustybuzz", "simplecss", - "siphasher 1.0.1", + "siphasher", "strict-num", "svgtypes", "tiny-skia-path", @@ -3274,6 +3307,12 @@ dependencies = [ "xmlwriter", ] +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -3315,25 +3354,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasm-bindgen" -version = "0.2.93" +name = "wasi" +version = "0.13.3+wasi-0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn", @@ -3342,9 +3390,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3352,9 +3400,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -3365,15 +3413,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "wasmi" -version = "0.39.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7a1acc721dd73e4fff2dc3796cc3efda6e008369e859a20fdbe058bddeebc3" +checksum = "a19af97fcb96045dd1d6b4d23e2b4abdbbe81723dbc5c9f016eb52145b320063" dependencies = [ "arrayvec", "multi-stash", @@ -3382,23 +3433,23 @@ dependencies = [ "wasmi_collections", "wasmi_core", "wasmi_ir", - "wasmparser-nostd", + "wasmparser", ] [[package]] name = "wasmi_collections" -version = "0.39.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142fda775f9cda587681ff0ec63c7a7e5679dc95da75f3f9b7e3979ce3506a5b" +checksum = "e80d6b275b1c922021939d561574bf376613493ae2b61c6963b15db0e8813562" dependencies = [ "string-interner", ] [[package]] name = "wasmi_core" -version = "0.39.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "281a49ca3c12c8efa052cb67758454fc861d80ab5a03def352e04eb08c20beb2" +checksum = "3a8c51482cc32d31c2c7ff211cd2bedd73c5bd057ba16a2ed0110e7a96097c33" dependencies = [ "downcast-rs", "libm", @@ -3406,27 +3457,28 @@ dependencies = [ [[package]] name = "wasmi_ir" -version = "0.39.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bbadcf529808086a74bacd3ce8aedece444a847292198a56dcde920d1fb213c" +checksum = "6e431a14c186db59212a88516788bd68ed51f87aa1e08d1df742522867b5289a" dependencies = [ "wasmi_core", ] [[package]] -name = "wasmparser-nostd" -version = "0.100.2" +name = "wasmparser" +version = "0.221.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5a015fe95f3504a94bb1462c717aae75253e39b9dd6c3fb1062c934535c64aa" +checksum = "9845c470a2e10b61dd42c385839cdd6496363ed63b5c9e420b5488b77bd22083" dependencies = [ - "indexmap-nostd", + "bitflags 2.8.0", + "indexmap 2.7.1", ] [[package]] name = "web-sys" -version = "0.3.70" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", @@ -3453,16 +3505,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", + "windows-targets", ] [[package]] @@ -3471,7 +3514,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -3480,22 +3523,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-targets", ] [[package]] @@ -3504,46 +3532,28 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3556,48 +3566,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -3606,13 +3592,28 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.20" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f" dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + [[package]] name = "writeable" version = "0.5.5" @@ -3630,9 +3631,9 @@ dependencies = [ [[package]] name = "xattr" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909" dependencies = [ "libc", "linux-raw-sys", @@ -3653,9 +3654,9 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "xmp-writer" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8254499146a4fd0c86e3e99cf4a9f468f595808fb49ff8f3e495f2b117bf4ebc" +checksum = "7eb5954c9ca6dcc869e98d3e42760ed9dab08f3e70212b31d7ab8ae7f3b7a487" [[package]] name = "xz2" @@ -3687,9 +3688,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", @@ -3699,9 +3700,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", @@ -3732,18 +3733,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", @@ -3788,18 +3789,18 @@ dependencies = [ [[package]] name = "zip" -version = "2.2.0" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494" +checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45" dependencies = [ "arbitrary", "crc32fast", "crossbeam-utils", "displaydoc", "flate2", - "indexmap 2.6.0", + "indexmap 2.7.1", "memchr", - "thiserror", + "thiserror 2.0.11", "zopfli", ] @@ -3825,9 +3826,9 @@ checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" [[package]] name = "zune-jpeg" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" +checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" dependencies = [ "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index 3550963e8..d91827ae4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ color-print = "0.3.6" comemo = "0.4" csv = "1" ctrlc = "3.4.1" -dirs = "5" +dirs = "6" ecow = { version = "0.2", features = ["serde"] } env_proxy = "0.4" flate2 = "1" @@ -69,13 +69,13 @@ icu_segmenter = { version = "1.4", features = ["serde"] } if_chain = "1" image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] } indexmap = { version = "2", features = ["serde"] } -kamadak-exif = "0.5" +kamadak-exif = "0.6" kurbo = "0.11" libfuzzer-sys = "0.4" lipsum = "0.9" miniz_oxide = "0.8" native-tls = "0.2" -notify = "6" +notify = "8" once_cell = "1" open = "5.0.1" openssl = "0.10" @@ -83,7 +83,7 @@ oxipng = { version = "9.0", default-features = false, features = ["filetime", "p palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] } parking_lot = "0.12.1" pathdiff = "0.2" -pdf-writer = "0.12" +pdf-writer = "0.12.1" phf = { version = "0.11", features = ["macros"] } pixglyph = "0.5.1" png = "0.17" @@ -133,11 +133,11 @@ unscanny = "0.1" ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] } usvg = { version = "0.43", default-features = false, features = ["text"] } walkdir = "2" -wasmi = "0.39.0" +wasmi = "0.40.0" web-sys = "0.3" xmlparser = "0.13.5" xmlwriter = "0.1.0" -xmp-writer = "0.3" +xmp-writer = "0.3.1" xz2 = { version = "0.1", features = ["static"] } yaml-front-matter = "0.1" zip = { version = "2", default-features = false, features = ["deflate"] } From 85b0318158cc1f71825f45c5fb7915b764f75776 Mon Sep 17 00:00:00 2001 From: Eric Biedert Date: Wed, 5 Feb 2025 13:40:54 +0100 Subject: [PATCH 009/172] Fix small copy-paste oversight (#5811) --- crates/typst-layout/src/math/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs index e5a3d94c9..708a4443d 100644 --- a/crates/typst-layout/src/math/mod.rs +++ b/crates/typst-layout/src/math/mod.rs @@ -644,7 +644,7 @@ fn layout_h( } /// Lays out a [`ClassElem`]. -#[typst_macros::time(name = "math.op", span = elem.span())] +#[typst_macros::time(name = "math.class", span = elem.span())] fn layout_class( elem: &Packed, ctx: &mut MathContext, From 25f6a7ab161b2106c22a9997a68afee60ddb7412 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 5 Feb 2025 13:58:43 +0100 Subject: [PATCH 010/172] Bump more dependencies (#5813) --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 6 +++--- tests/ref/bibliography-before-content.png | Bin 17109 -> 17122 bytes tests/ref/bibliography-indent-par.png | Bin 9087 -> 9096 bytes tests/ref/bibliography-math.png | Bin 4605 -> 4610 bytes tests/ref/cite-footnote.png | Bin 13525 -> 13532 bytes 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8b7754aef..e5daf731f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -312,9 +312,9 @@ dependencies = [ [[package]] name = "citationberg" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92fea693c83bd967604be367dc1e1b4895625eabafec2eec66c51092e18e700e" +checksum = "e4595e03beafb40235070080b5286d3662525efc622cca599585ff1d63f844fa" dependencies = [ "quick-xml 0.36.2", "serde", @@ -398,9 +398,9 @@ dependencies = [ [[package]] name = "codex" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e0ee2092c3513f63588d51c3f81b98e6b1aa8ddcca3b5892b288f093516497d" +checksum = "724d27a0ee38b700e5e164350e79aba601a0db673ac47fce1cb74c3e38864036" [[package]] name = "color-print" @@ -898,9 +898,9 @@ dependencies = [ [[package]] name = "hayagriva" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a3635c2577f77499c9dc3dceeef2e64e6c146e711b1861507a0f15b20641348" +checksum = "954907554bb7fcba29a4f917c2d43e289ec21b69d872ccf97db160eca6caeed8" dependencies = [ "biblatex", "ciborium", @@ -2718,9 +2718,9 @@ dependencies = [ [[package]] name = "two-face" -version = "0.4.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ccd4843ea031c609fe9c16cae00e9657bad8a9f735a3cc2e420955d802b4268" +checksum = "384eda438ddf62e2c6f39a174452d952d9d9df5a8ad5ade22198609f8dcaf852" dependencies = [ "once_cell", "serde", diff --git a/Cargo.toml b/Cargo.toml index d91827ae4..469439d38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] } clap_complete = "4.2.1" clap_mangen = "0.2.10" codespan-reporting = "0.11" -codex = "0.1.0" +codex = "0.1.1" color-print = "0.3.6" comemo = "0.4" csv = "1" @@ -58,7 +58,7 @@ env_proxy = "0.4" flate2 = "1" fontdb = { version = "0.21", default-features = false } fs_extra = "1.3" -hayagriva = "0.8" +hayagriva = "0.8.1" heck = "0.5" hypher = "0.1.4" icu_properties = { version = "1.4", features = ["serde"] } @@ -122,7 +122,7 @@ tiny_http = "0.12" tiny-skia = "0.11" toml = { version = "0.8", default-features = false, features = ["parse", "display"] } ttf-parser = "0.24.1" -two-face = { version = "0.4.0", default-features = false, features = ["syntect-fancy"] } +two-face = { version = "0.4.3", default-features = false, features = ["syntect-fancy"] } typed-arena = "2" unicode-bidi = "0.3.18" unicode-ident = "1.0" diff --git a/tests/ref/bibliography-before-content.png b/tests/ref/bibliography-before-content.png index ea5ece267fce5eafe8071f4f311d1492416457bc..eb9f26d8321256dfb56d2842cc26c7ba8624a312 100644 GIT binary patch delta 17051 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 delta 17021 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 nlmo0&PC39juO5)4#mr?=6-Q6ScxHLQ*&$v7B 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 From 029ae4a5ea7ad1e52112ce26b6d38ce1750dae3f Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 5 Feb 2025 14:24:10 +0100 Subject: [PATCH 011/172] Export target docs (#5812) Co-authored-by: Martin Haug <3874949+reknih@users.noreply.github.com> --- Cargo.lock | 2 + crates/typst-library/src/foundations/mod.rs | 10 +- crates/typst-library/src/foundations/scope.rs | 46 +---- .../typst-library/src/foundations/target.rs | 46 ++++- crates/typst-library/src/html/mod.rs | 50 +++-- crates/typst-library/src/introspection/mod.rs | 18 +- crates/typst-library/src/layout/mod.rs | 11 +- crates/typst-library/src/lib.rs | 52 ++++- crates/typst-library/src/loading/cbor.rs | 3 - crates/typst-library/src/loading/csv.rs | 3 - crates/typst-library/src/loading/json.rs | 3 - crates/typst-library/src/loading/mod.rs | 12 +- crates/typst-library/src/loading/toml.rs | 3 - crates/typst-library/src/loading/xml.rs | 3 - crates/typst-library/src/loading/yaml.rs | 3 - crates/typst-library/src/math/mod.rs | 113 +---------- crates/typst-library/src/model/mod.rs | 13 +- crates/typst-library/src/pdf/mod.rs | 19 +- crates/typst-library/src/symbols.rs | 13 +- crates/typst-library/src/text/mod.rs | 15 +- .../typst-library/src/visualize/image/mod.rs | 25 ++- crates/typst-library/src/visualize/mod.rs | 13 +- crates/typst-library/src/visualize/path.rs | 3 - crates/typst-macros/src/category.rs | 59 ------ crates/typst-macros/src/lib.rs | 10 - docs/Cargo.toml | 4 +- docs/guides/guide-for-latex-users.md | 7 +- docs/reference/export/html.md | 61 ++++++ docs/reference/export/pdf.md | 71 +++++++ docs/reference/export/png.md | 61 ++++++ docs/reference/export/svg.md | 48 +++++ docs/reference/{ => language}/context.md | 0 docs/reference/{ => language}/scripting.md | 0 docs/reference/{ => language}/styling.md | 0 docs/reference/{ => language}/syntax.md | 0 docs/reference/library/data-loading.md | 4 + docs/reference/library/foundations.md | 4 + docs/reference/library/introspection.md | 10 + docs/reference/library/layout.md | 3 + docs/reference/library/math.md | 101 ++++++++++ docs/reference/library/model.md | 5 + docs/reference/library/symbols.md | 5 + docs/reference/library/text.md | 3 + docs/reference/library/visualize.md | 5 + docs/reference/packages.md | 6 - docs/src/html.rs | 13 +- docs/src/lib.rs | 184 ++++++++++++------ docs/src/link.rs | 9 +- docs/src/model.rs | 5 +- 49 files changed, 709 insertions(+), 448 deletions(-) delete mode 100644 crates/typst-macros/src/category.rs create mode 100644 docs/reference/export/html.md create mode 100644 docs/reference/export/pdf.md create mode 100644 docs/reference/export/png.md create mode 100644 docs/reference/export/svg.md rename docs/reference/{ => language}/context.md (100%) rename docs/reference/{ => language}/scripting.md (100%) rename docs/reference/{ => language}/styling.md (100%) rename docs/reference/{ => language}/syntax.md (100%) create mode 100644 docs/reference/library/data-loading.md create mode 100644 docs/reference/library/foundations.md create mode 100644 docs/reference/library/introspection.md create mode 100644 docs/reference/library/layout.md create mode 100644 docs/reference/library/math.md create mode 100644 docs/reference/library/model.md create mode 100644 docs/reference/library/symbols.md create mode 100644 docs/reference/library/text.md create mode 100644 docs/reference/library/visualize.md delete mode 100644 docs/reference/packages.md diff --git a/Cargo.lock b/Cargo.lock index e5daf731f..140dccf74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2822,6 +2822,8 @@ dependencies = [ "typst-assets", "typst-dev-assets", "typst-render", + "typst-utils", + "unicode-math-class", "unscanny", "yaml-front-matter", ] diff --git a/crates/typst-library/src/foundations/mod.rs b/crates/typst-library/src/foundations/mod.rs index c335484fa..8e3aa060d 100644 --- a/crates/typst-library/src/foundations/mod.rs +++ b/crates/typst-library/src/foundations/mod.rs @@ -85,16 +85,9 @@ use crate::engine::Engine; use crate::routines::EvalMode; use crate::{Feature, Features}; -/// Foundational types and functions. -/// -/// Here, you'll find documentation for basic data types like [integers]($int) -/// and [strings]($str) as well as details about core computational functions. -#[category] -pub static FOUNDATIONS: Category; - /// Hook up all `foundations` definitions. pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) { - global.start_category(FOUNDATIONS); + global.start_category(crate::Category::Foundations); global.define_type::(); global.define_type::(); global.define_type::(); @@ -125,6 +118,7 @@ pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) { } global.define("calc", calc::module()); global.define("sys", sys::module(inputs)); + global.reset_category(); } /// Fails with an error. diff --git a/crates/typst-library/src/foundations/scope.rs b/crates/typst-library/src/foundations/scope.rs index d6c5a8d05..e1ce61b8a 100644 --- a/crates/typst-library/src/foundations/scope.rs +++ b/crates/typst-library/src/foundations/scope.rs @@ -1,6 +1,3 @@ -#[doc(inline)] -pub use typst_macros::category; - use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; @@ -8,14 +5,13 @@ use ecow::{eco_format, EcoString}; use indexmap::map::Entry; use indexmap::IndexMap; use typst_syntax::Span; -use typst_utils::Static; use crate::diag::{bail, DeprecationSink, HintedStrResult, HintedString, StrResult}; use crate::foundations::{ Element, Func, IntoValue, NativeElement, NativeFunc, NativeFuncData, NativeType, Type, Value, }; -use crate::Library; +use crate::{Category, Library}; /// A stack of scopes. #[derive(Debug, Default, Clone)] @@ -361,46 +357,6 @@ pub enum Capturer { Context, } -/// A group of related definitions. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct Category(Static); - -impl Category { - /// Create a new category from raw data. - pub const fn from_data(data: &'static CategoryData) -> Self { - Self(Static(data)) - } - - /// The category's name. - pub fn name(&self) -> &'static str { - self.0.name - } - - /// The type's title case name, for use in documentation (e.g. `String`). - pub fn title(&self) -> &'static str { - self.0.title - } - - /// Documentation for the category. - pub fn docs(&self) -> &'static str { - self.0.docs - } -} - -impl Debug for Category { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Category({})", self.name()) - } -} - -/// Defines a category. -#[derive(Debug)] -pub struct CategoryData { - pub name: &'static str, - pub title: &'static str, - pub docs: &'static str, -} - /// The error message when trying to mutate a variable from the standard /// library. #[cold] diff --git a/crates/typst-library/src/foundations/target.rs b/crates/typst-library/src/foundations/target.rs index 5841552e4..2a21fd42b 100644 --- a/crates/typst-library/src/foundations/target.rs +++ b/crates/typst-library/src/foundations/target.rs @@ -3,7 +3,7 @@ use comemo::Tracked; use crate::diag::HintedStrResult; use crate::foundations::{elem, func, Cast, Context}; -/// The compilation target. +/// The export target. #[derive(Debug, Default, Copy, Clone, PartialEq, Hash, Cast)] pub enum Target { /// The target that is used for paged, fully laid-out content. @@ -28,7 +28,49 @@ pub struct TargetElem { pub target: Target, } -/// Returns the current compilation target. +/// Returns the current export target. +/// +/// This function returns either +/// - `{"paged"}` (for PDF, PNG, and SVG export), or +/// - `{"html"}` (for HTML export). +/// +/// The design of this function is not yet finalized and for this reason it is +/// guarded behind the `html` feature. Visit the [HTML documentation +/// page]($html) for more details. +/// +/// # When to use it +/// This function allows you to format your document properly across both HTML +/// and paged export targets. It should primarily be used in templates and show +/// rules, rather than directly in content. This way, the document's contents +/// can be fully agnostic to the export target and content can be shared between +/// PDF and HTML export. +/// +/// # Varying targets +/// This function is [contextual]($context) as the target can vary within a +/// single compilation: When exporting to HTML, the target will be `{"paged"}` +/// while within an [`html.frame`]. +/// +/// # Example +/// ```example +/// #let kbd(it) = context { +/// if target() == "html" { +/// html.elem("kbd", it) +/// } else { +/// set text(fill: rgb("#1f2328")) +/// let r = 3pt +/// box( +/// fill: rgb("#f6f8fa"), +/// stroke: rgb("#d1d9e0b3"), +/// outset: (y: r), +/// inset: (x: r), +/// radius: r, +/// raw(it) +/// ) +/// } +/// } +/// +/// Press #kbd("F1") for help. +/// ``` #[func(contextual)] pub fn target(context: Tracked) -> HintedStrResult { Ok(TargetElem::target_in(context.styles()?)) diff --git a/crates/typst-library/src/html/mod.rs b/crates/typst-library/src/html/mod.rs index c412b4607..1d88781c1 100644 --- a/crates/typst-library/src/html/mod.rs +++ b/crates/typst-library/src/html/mod.rs @@ -6,53 +6,77 @@ pub use self::dom::*; use ecow::EcoString; -use crate::foundations::{category, elem, Category, Content, Module, Scope}; - -/// HTML output. -#[category] -pub static HTML: Category; +use crate::foundations::{elem, Content, Module, Scope}; /// Create a module with all HTML definitions. pub fn module() -> Module { let mut html = Scope::deduplicating(); - html.start_category(HTML); + html.start_category(crate::Category::Html); html.define_elem::(); html.define_elem::(); Module::new("html", html) } -/// A HTML element that can contain Typst content. +/// An HTML element that can contain Typst content. +/// +/// Typst's HTML export automatically generates the appropriate tags for most +/// elements. However, sometimes, it is desirable to retain more control. For +/// example, when using Typst to generate your blog, you could use this function +/// to wrap each article in an `
` tag. +/// +/// Typst is aware of what is valid HTML. A tag and its attributes must form +/// syntactically valid HTML. Some tags, like `meta` do not accept content. +/// Hence, you must not provide a body for them. We may add more checks in the +/// future, so be sure that you are generating valid HTML when using this +/// function. +/// +/// Normally, Typst will generate `html`, `head`, and `body` tags for you. If +/// you instead create them with this function, Typst will omit its own tags. +/// +/// ```typ +/// #html.elem("div", attrs: (style: "background: aqua"))[ +/// A div with _Typst content_ inside! +/// ] +/// ``` #[elem(name = "elem")] pub struct HtmlElem { /// The element's tag. #[required] pub tag: HtmlTag, - /// The element's attributes. + /// The element's HTML attributes. #[borrowed] pub attrs: HtmlAttrs, /// The contents of the HTML element. + /// + /// The body can be arbitrary Typst content. #[positional] #[borrowed] pub body: Option, } impl HtmlElem { - /// Add an atribute to the element. + /// Add an attribute to the element. pub fn with_attr(mut self, attr: HtmlAttr, value: impl Into) -> Self { self.attrs.get_or_insert_with(Default::default).push(attr, value); self } } -/// An element that forces its contents to be laid out. +/// An element that lays out its content as an inline SVG. /// -/// Integrates content that requires layout (e.g. a plot) into HTML output -/// by turning it into an inline SVG. +/// Sometimes, converting Typst content to HTML is not desirable. This can be +/// the case for plots and other content that relies on positioning and styling +/// to convey its message. +/// +/// This function allows you to use the Typst layout engine that would also be +/// used for PDF, SVG, and PNG export to render a part of your document exactly +/// how it would appear when exported in one of these formats. It embeds the +/// content as an inline SVG. #[elem] pub struct FrameElem { - /// The contents that shall be laid out. + /// The content that shall be laid out. #[positional] #[required] pub body: Content, diff --git a/crates/typst-library/src/introspection/mod.rs b/crates/typst-library/src/introspection/mod.rs index d8184330d..995fbd7b5 100644 --- a/crates/typst-library/src/introspection/mod.rs +++ b/crates/typst-library/src/introspection/mod.rs @@ -25,24 +25,11 @@ pub use self::query_::*; pub use self::state::*; pub use self::tag::*; -use crate::foundations::{category, Category, Scope}; - -/// Interactions between document parts. -/// -/// This category is home to Typst's introspection capabilities: With the -/// `counter` function, you can access and manipulate page, section, figure, and -/// equation counters or create custom ones. Meanwhile, the `query` function -/// lets you search for elements in the document to construct things like a list -/// of figures or headers which show the current chapter title. -/// -/// Most of the functions are _contextual._ It is recommended to read the chapter -/// on [context] before continuing here. -#[category] -pub static INTROSPECTION: Category; +use crate::foundations::Scope; /// Hook up all `introspection` definitions. pub fn define(global: &mut Scope) { - global.start_category(INTROSPECTION); + global.start_category(crate::Category::Introspection); global.define_type::(); global.define_type::(); global.define_type::(); @@ -50,4 +37,5 @@ pub fn define(global: &mut Scope) { global.define_func::(); global.define_func::(); global.define_func::(); + global.reset_category(); } diff --git a/crates/typst-library/src/layout/mod.rs b/crates/typst-library/src/layout/mod.rs index 57518fe72..ef1ecdb36 100644 --- a/crates/typst-library/src/layout/mod.rs +++ b/crates/typst-library/src/layout/mod.rs @@ -64,17 +64,11 @@ pub use self::spacing::*; pub use self::stack::*; pub use self::transform::*; -use crate::foundations::{category, Category, Scope}; - -/// Arranging elements on the page in different ways. -/// -/// By combining layout functions, you can create complex and automatic layouts. -#[category] -pub static LAYOUT: Category; +use crate::foundations::Scope; /// Hook up all `layout` definitions. pub fn define(global: &mut Scope) { - global.start_category(LAYOUT); + global.start_category(crate::Category::Layout); global.define_type::(); global.define_type::(); global.define_type::(); @@ -103,4 +97,5 @@ pub fn define(global: &mut Scope) { global.define_elem::(); global.define_func::(); global.define_func::(); + global.reset_category(); } diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs index 460321aa3..c39024f71 100644 --- a/crates/typst-library/src/lib.rs +++ b/crates/typst-library/src/lib.rs @@ -29,6 +29,7 @@ pub mod visualize; use std::ops::{Deref, Range}; +use serde::{Deserialize, Serialize}; use typst_syntax::{FileId, Source, Span}; use typst_utils::{LazyHash, SmallBitSet}; @@ -236,31 +237,72 @@ pub enum Feature { Html, } +/// A group of related standard library definitions. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum Category { + Foundations, + Introspection, + Layout, + DataLoading, + Math, + Model, + Symbols, + Text, + Visualize, + Pdf, + Html, + Svg, + Png, +} + +impl Category { + /// The kebab-case name of the category. + pub fn name(&self) -> &'static str { + match self { + Self::Foundations => "foundations", + Self::Introspection => "introspection", + Self::Layout => "layout", + Self::DataLoading => "data-loading", + Self::Math => "math", + Self::Model => "model", + Self::Symbols => "symbols", + Self::Text => "text", + Self::Visualize => "visualize", + Self::Pdf => "pdf", + Self::Html => "html", + Self::Svg => "svg", + Self::Png => "png", + } + } +} + /// Construct the module with global definitions. fn global(math: Module, inputs: Dict, features: &Features) -> Module { let mut global = Scope::deduplicating(); + self::foundations::define(&mut global, inputs, features); self::model::define(&mut global); self::text::define(&mut global); - global.reset_category(); - global.define("math", math); self::layout::define(&mut global); self::visualize::define(&mut global); self::introspection::define(&mut global); self::loading::define(&mut global); self::symbols::define(&mut global); - self::pdf::define(&mut global); - global.reset_category(); + + global.define("math", math); + global.define("pdf", self::pdf::module()); if features.is_enabled(Feature::Html) { global.define("html", self::html::module()); } + prelude(&mut global); + Module::new("global", global) } /// Defines scoped values that are globally available, too. fn prelude(global: &mut Scope) { - global.reset_category(); global.define("black", Color::BLACK); global.define("gray", Color::GRAY); global.define("silver", Color::SILVER); diff --git a/crates/typst-library/src/loading/cbor.rs b/crates/typst-library/src/loading/cbor.rs index bd65e8442..801ca617a 100644 --- a/crates/typst-library/src/loading/cbor.rs +++ b/crates/typst-library/src/loading/cbor.rs @@ -34,9 +34,6 @@ pub fn cbor( #[scope] impl cbor { /// Reads structured data from CBOR bytes. - /// - /// This function is deprecated. The [`cbor`] function now accepts bytes - /// directly. #[func(title = "Decode CBOR")] #[deprecated = "`cbor.decode` is deprecated, directly pass bytes to `cbor` instead"] pub fn decode( diff --git a/crates/typst-library/src/loading/csv.rs b/crates/typst-library/src/loading/csv.rs index d01d687ba..6fdec4459 100644 --- a/crates/typst-library/src/loading/csv.rs +++ b/crates/typst-library/src/loading/csv.rs @@ -96,9 +96,6 @@ pub fn csv( #[scope] impl csv { /// Reads structured data from a CSV string/bytes. - /// - /// This function is deprecated. The [`csv`] function now accepts bytes - /// directly. #[func(title = "Decode CSV")] #[deprecated = "`csv.decode` is deprecated, directly pass bytes to `csv` instead"] pub fn decode( diff --git a/crates/typst-library/src/loading/json.rs b/crates/typst-library/src/loading/json.rs index 52c87371f..185bac143 100644 --- a/crates/typst-library/src/loading/json.rs +++ b/crates/typst-library/src/loading/json.rs @@ -65,9 +65,6 @@ pub fn json( #[scope] impl json { /// Reads structured data from a JSON string/bytes. - /// - /// This function is deprecated. The [`json`] function now accepts bytes - /// directly. #[func(title = "Decode JSON")] #[deprecated = "`json.decode` is deprecated, directly pass bytes to `json` instead"] pub fn decode( diff --git a/crates/typst-library/src/loading/mod.rs b/crates/typst-library/src/loading/mod.rs index c645b691d..c57e02888 100644 --- a/crates/typst-library/src/loading/mod.rs +++ b/crates/typst-library/src/loading/mod.rs @@ -29,19 +29,12 @@ pub use self::yaml_::*; use crate::diag::{At, SourceResult}; use crate::foundations::OneOrMultiple; -use crate::foundations::{cast, category, Bytes, Category, Scope, Str}; +use crate::foundations::{cast, Bytes, Scope, Str}; use crate::World; -/// Data loading from external files. -/// -/// These functions help you with loading and embedding data, for example from -/// the results of an experiment. -#[category] -pub static DATA_LOADING: Category; - /// Hook up all `data-loading` definitions. pub(super) fn define(global: &mut Scope) { - global.start_category(DATA_LOADING); + global.start_category(crate::Category::DataLoading); global.define_func::(); global.define_func::(); global.define_func::(); @@ -49,6 +42,7 @@ pub(super) fn define(global: &mut Scope) { global.define_func::(); global.define_func::(); global.define_func::(); + global.reset_category(); } /// Something we can retrieve byte data from. diff --git a/crates/typst-library/src/loading/toml.rs b/crates/typst-library/src/loading/toml.rs index 456112463..2660e7e7f 100644 --- a/crates/typst-library/src/loading/toml.rs +++ b/crates/typst-library/src/loading/toml.rs @@ -44,9 +44,6 @@ pub fn toml( #[scope] impl toml { /// Reads structured data from a TOML string/bytes. - /// - /// This function is deprecated. The [`toml`] function now accepts bytes - /// directly. #[func(title = "Decode TOML")] #[deprecated = "`toml.decode` is deprecated, directly pass bytes to `toml` instead"] pub fn decode( diff --git a/crates/typst-library/src/loading/xml.rs b/crates/typst-library/src/loading/xml.rs index 0172071be..32ed6f24b 100644 --- a/crates/typst-library/src/loading/xml.rs +++ b/crates/typst-library/src/loading/xml.rs @@ -77,9 +77,6 @@ pub fn xml( #[scope] impl xml { /// Reads structured data from an XML string/bytes. - /// - /// This function is deprecated. The [`xml`] function now accepts bytes - /// directly. #[func(title = "Decode XML")] #[deprecated = "`xml.decode` is deprecated, directly pass bytes to `xml` instead"] pub fn decode( diff --git a/crates/typst-library/src/loading/yaml.rs b/crates/typst-library/src/loading/yaml.rs index 511c676cb..4eeec28f1 100644 --- a/crates/typst-library/src/loading/yaml.rs +++ b/crates/typst-library/src/loading/yaml.rs @@ -55,9 +55,6 @@ pub fn yaml( #[scope] impl yaml { /// Reads structured data from a YAML string/bytes. - /// - /// This function is deprecated. The [`yaml`] function now accepts bytes - /// directly. #[func(title = "Decode YAML")] #[deprecated = "`yaml.decode` is deprecated, directly pass bytes to `yaml` instead"] pub fn decode( diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs index a97a19b09..2e6d42b13 100644 --- a/crates/typst-library/src/math/mod.rs +++ b/crates/typst-library/src/math/mod.rs @@ -27,119 +27,10 @@ pub use self::underover::*; use typst_utils::singleton; use unicode_math_class::MathClass; -use crate::foundations::{ - category, elem, Category, Content, Module, NativeElement, Scope, -}; +use crate::foundations::{elem, Content, Module, NativeElement, Scope}; use crate::layout::{Em, HElem}; use crate::text::TextElem; -/// Typst has special [syntax]($syntax/#math) and library functions to typeset -/// mathematical formulas. Math formulas can be displayed inline with text or as -/// separate blocks. They will be typeset into their own block if they start and -/// end with at least one space (e.g. `[$ x^2 $]`). -/// -/// # Variables -/// In math, single letters are always displayed as is. Multiple letters, -/// however, are interpreted as variables and functions. To display multiple -/// letters verbatim, you can place them into quotes and to access single letter -/// variables, you can use the [hash syntax]($scripting/#expressions). -/// -/// ```example -/// $ A = pi r^2 $ -/// $ "area" = pi dot "radius"^2 $ -/// $ cal(A) := -/// { x in RR | x "is natural" } $ -/// #let x = 5 -/// $ #x < 17 $ -/// ``` -/// -/// # Symbols -/// Math mode makes a wide selection of [symbols]($category/symbols/sym) like -/// `pi`, `dot`, or `RR` available. Many mathematical symbols are available in -/// different variants. You can select between different variants by applying -/// [modifiers]($symbol) to the symbol. Typst further recognizes a number of -/// shorthand sequences like `=>` that approximate a symbol. When such a -/// shorthand exists, the symbol's documentation lists it. -/// -/// ```example -/// $ x < y => x gt.eq.not y $ -/// ``` -/// -/// # Line Breaks -/// Formulas can also contain line breaks. Each line can contain one or multiple -/// _alignment points_ (`&`) which are then aligned. -/// -/// ```example -/// $ sum_(k=0)^n k -/// &= 1 + ... + n \ -/// &= (n(n+1)) / 2 $ -/// ``` -/// -/// # Function calls -/// Math mode supports special function calls without the hash prefix. In these -/// "math calls", the argument list works a little differently than in code: -/// -/// - Within them, Typst is still in "math mode". Thus, you can write math -/// directly into them, but need to use hash syntax to pass code expressions -/// (except for strings, which are available in the math syntax). -/// - They support positional and named arguments, as well as argument -/// spreading. -/// - They don't support trailing content blocks. -/// - They provide additional syntax for 2-dimensional argument lists. The -/// semicolon (`;`) merges preceding arguments separated by commas into an -/// array argument. -/// -/// ```example -/// $ frac(a^2, 2) $ -/// $ vec(1, 2, delim: "[") $ -/// $ mat(1, 2; 3, 4) $ -/// $ mat(..#range(1, 5).chunks(2)) $ -/// $ lim_x = -/// op("lim", limits: #true)_x $ -/// ``` -/// -/// To write a verbatim comma or semicolon in a math call, escape it with a -/// backslash. The colon on the other hand is only recognized in a special way -/// if directly preceded by an identifier, so to display it verbatim in those -/// cases, you can just insert a space before it. -/// -/// Functions calls preceded by a hash are normal code function calls and not -/// affected by these rules. -/// -/// # Alignment -/// When equations include multiple _alignment points_ (`&`), this creates -/// blocks of alternatingly right- and left-aligned columns. In the example -/// below, the expression `(3x + y) / 7` is right-aligned and `= 9` is -/// left-aligned. The word "given" is also left-aligned because `&&` creates two -/// alignment points in a row, alternating the alignment twice. `& &` and `&&` -/// behave exactly the same way. Meanwhile, "multiply by 7" is right-aligned -/// because just one `&` precedes it. Each alignment point simply alternates -/// between right-aligned/left-aligned. -/// -/// ```example -/// $ (3x + y) / 7 &= 9 && "given" \ -/// 3x + y &= 63 & "multiply by 7" \ -/// 3x &= 63 - y && "subtract y" \ -/// x &= 21 - y/3 & "divide by 3" $ -/// ``` -/// -/// # Math fonts -/// You can set the math font by with a [show-set rule]($styling/#show-rules) as -/// demonstrated below. Note that only special OpenType math fonts are suitable -/// for typesetting maths. -/// -/// ```example -/// #show math.equation: set text(font: "Fira Math") -/// $ sum_(i in NN) 1 + i $ -/// ``` -/// -/// # Math module -/// All math functions are part of the `math` [module]($scripting/#modules), -/// which is available by default in equations. Outside of equations, they can -/// be accessed with the `math.` prefix. -#[category] -pub static MATH: Category; - // Spacings. pub const THIN: Em = Em::new(1.0 / 6.0); pub const MEDIUM: Em = Em::new(2.0 / 9.0); @@ -150,7 +41,7 @@ pub const WIDE: Em = Em::new(2.0); /// Create a module with all math definitions. pub fn module() -> Module { let mut math = Scope::deduplicating(); - math.start_category(MATH); + math.start_category(crate::Category::Math); math.define_elem::(); math.define_elem::(); math.define_elem::(); diff --git a/crates/typst-library/src/model/mod.rs b/crates/typst-library/src/model/mod.rs index 586e10ec1..9bdbf0013 100644 --- a/crates/typst-library/src/model/mod.rs +++ b/crates/typst-library/src/model/mod.rs @@ -40,19 +40,11 @@ pub use self::strong::*; pub use self::table::*; pub use self::terms::*; -use crate::foundations::{category, Category, Scope}; - -/// Document structuring. -/// -/// Here, you can find functions to structure your document and interact with -/// that structure. This includes section headings, figures, bibliography -/// management, cross-referencing and more. -#[category] -pub static MODEL: Category; +use crate::foundations::Scope; /// Hook up all `model` definitions. pub fn define(global: &mut Scope) { - global.start_category(MODEL); + global.start_category(crate::Category::Model); global.define_elem::(); global.define_elem::(); global.define_elem::(); @@ -72,4 +64,5 @@ pub fn define(global: &mut Scope) { global.define_elem::(); global.define_elem::(); global.define_func::(); + global.reset_category(); } diff --git a/crates/typst-library/src/pdf/mod.rs b/crates/typst-library/src/pdf/mod.rs index 3bd3b0c52..786a36372 100644 --- a/crates/typst-library/src/pdf/mod.rs +++ b/crates/typst-library/src/pdf/mod.rs @@ -4,21 +4,12 @@ mod embed; pub use self::embed::*; -use crate::foundations::{category, Category, Module, Scope}; - -/// PDF-specific functionality. -#[category] -pub static PDF: Category; - -/// Hook up the `pdf` module. -pub(super) fn define(global: &mut Scope) { - global.start_category(PDF); - global.define("pdf", module()); -} +use crate::foundations::{Module, Scope}; /// Hook up all `pdf` definitions. pub fn module() -> Module { - let mut scope = Scope::deduplicating(); - scope.define_elem::(); - Module::new("pdf", scope) + let mut pdf = Scope::deduplicating(); + pdf.start_category(crate::Category::Pdf); + pdf.define_elem::(); + Module::new("pdf", pdf) } diff --git a/crates/typst-library/src/symbols.rs b/crates/typst-library/src/symbols.rs index 777f8172f..0588ace95 100644 --- a/crates/typst-library/src/symbols.rs +++ b/crates/typst-library/src/symbols.rs @@ -1,19 +1,12 @@ //! Modifiable symbols. -use crate::foundations::{category, Category, Module, Scope, Symbol, Value}; - -/// These two modules give names to symbols and emoji to make them easy to -/// insert with a normal keyboard. Alternatively, you can also always directly -/// enter Unicode symbols into your text and formulas. In addition to the -/// symbols listed below, math mode defines `dif` and `Dif`. These are not -/// normal symbol values because they also affect spacing and font style. -#[category] -pub static SYMBOLS: Category; +use crate::foundations::{Module, Scope, Symbol, Value}; /// Hook up all `symbol` definitions. pub(super) fn define(global: &mut Scope) { - global.start_category(SYMBOLS); + global.start_category(crate::Category::Symbols); extend_scope_from_codex_module(global, codex::ROOT); + global.reset_category(); } /// Hook up all math `symbol` definitions, i.e., elements of the `sym` module. diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index f506397e1..12f4e4c59 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -45,9 +45,9 @@ use typst_utils::singleton; use crate::diag::{bail, warning, HintedStrResult, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, category, dict, elem, Args, Array, Cast, Category, Construct, Content, Dict, - Fold, IntoValue, NativeElement, Never, NoneValue, Packed, PlainText, Regex, Repr, - Resolve, Scope, Set, Smart, StyleChain, + cast, dict, elem, Args, Array, Cast, Construct, Content, Dict, Fold, IntoValue, + NativeElement, Never, NoneValue, Packed, PlainText, Regex, Repr, Resolve, Scope, Set, + Smart, StyleChain, }; use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel}; use crate::math::{EquationElem, MathSize}; @@ -55,15 +55,9 @@ use crate::model::ParElem; use crate::visualize::{Color, Paint, RelativeTo, Stroke}; use crate::World; -/// Text styling. -/// -/// The [text function]($text) is of particular interest. -#[category] -pub static TEXT: Category; - /// Hook up all `text` definitions. pub(super) fn define(global: &mut Scope) { - global.start_category(TEXT); + global.start_category(crate::Category::Text); global.define_elem::(); global.define_elem::(); global.define_elem::(); @@ -78,6 +72,7 @@ pub(super) fn define(global: &mut Scope) { global.define_func::(); global.define_func::(); global.define_func::(); + global.reset_category(); } /// Customizes the look and layout of text in a variety of ways. diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 9306eb6f2..18d40caa8 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -50,6 +50,17 @@ pub struct ImageElem { /// supported [formats]($image.format). /// /// For more details about paths, see the [Paths section]($syntax/#paths). + /// + /// ```example + /// #let original = read("diagram.svg") + /// #let changed = original.replace( + /// "#2B80FF", // blue + /// green.to-hex(), + /// ) + /// + /// #image(bytes(original)) + /// #image(bytes(changed)) + /// ``` #[required] #[parse( let source = args.expect::>("source")?; @@ -156,20 +167,6 @@ pub struct ImageElem { #[allow(clippy::too_many_arguments)] impl ImageElem { /// Decode a raster or vector graphic from bytes or a string. - /// - /// This function is deprecated. The [`image`] function now accepts bytes - /// directly. - /// - /// ```example - /// #let original = read("diagram.svg") - /// #let changed = original.replace( - /// "#2B80FF", // blue - /// green.to-hex(), - /// ) - /// - /// #image.decode(original) - /// #image.decode(changed) - /// ``` #[func(title = "Decode Image")] #[deprecated = "`image.decode` is deprecated, directly pass bytes to `image` instead"] pub fn decode( diff --git a/crates/typst-library/src/visualize/mod.rs b/crates/typst-library/src/visualize/mod.rs index 76849ac86..72a420657 100644 --- a/crates/typst-library/src/visualize/mod.rs +++ b/crates/typst-library/src/visualize/mod.rs @@ -24,19 +24,11 @@ pub use self::shape::*; pub use self::stroke::*; pub use self::tiling::*; -use crate::foundations::{category, Category, Element, Scope, Type}; - -/// Drawing and data visualization. -/// -/// If you want to create more advanced drawings or plots, also have a look at -/// the [CetZ](https://github.com/johannes-wolf/cetz) package as well as more -/// specialized [packages]($universe) for your use case. -#[category] -pub static VISUALIZE: Category; +use crate::foundations::{Element, Scope, Type}; /// Hook up all visualize definitions. pub(super) fn define(global: &mut Scope) { - global.start_category(VISUALIZE); + global.start_category(crate::Category::Visualize); global.define_type::(); global.define_type::(); global.define_type::(); @@ -55,4 +47,5 @@ pub(super) fn define(global: &mut Scope) { global .define("pattern", Type::of::()) .deprecated("the name `pattern` is deprecated, use `tiling` instead"); + global.reset_category(); } diff --git a/crates/typst-library/src/visualize/path.rs b/crates/typst-library/src/visualize/path.rs index 5d3439c08..c1cfde94a 100644 --- a/crates/typst-library/src/visualize/path.rs +++ b/crates/typst-library/src/visualize/path.rs @@ -21,9 +21,6 @@ use crate::visualize::{FillRule, Paint, Stroke}; /// ((50%, 0pt), (40pt, 0pt)), /// ) /// ``` -/// -/// # Deprecation -/// This function is deprecated. The [`curve`] function should be used instead. #[elem(Show)] pub struct PathElem { /// How to fill the path. diff --git a/crates/typst-macros/src/category.rs b/crates/typst-macros/src/category.rs deleted file mode 100644 index 26ec879cc..000000000 --- a/crates/typst-macros/src/category.rs +++ /dev/null @@ -1,59 +0,0 @@ -use heck::{ToKebabCase, ToTitleCase}; -use proc_macro2::TokenStream; -use quote::quote; -use syn::parse::{Parse, ParseStream}; -use syn::{Attribute, Ident, Result, Token, Type, Visibility}; - -use crate::util::{documentation, foundations}; - -/// Expand the `#[category]` macro. -pub fn category(_: TokenStream, item: syn::Item) -> Result { - let syn::Item::Verbatim(stream) = item else { - bail!(item, "expected bare static"); - }; - - let BareStatic { attrs, vis, ident, ty, .. } = syn::parse2(stream)?; - - let name = ident.to_string().to_kebab_case(); - let title = name.to_title_case(); - let docs = documentation(&attrs); - - Ok(quote! { - #(#attrs)* - #[allow(rustdoc::broken_intra_doc_links)] - #vis static #ident: #ty = { - static DATA: #foundations::CategoryData = #foundations::CategoryData { - name: #name, - title: #title, - docs: #docs, - }; - #foundations::Category::from_data(&DATA) - }; - }) -} - -/// Parse a bare `pub static CATEGORY: Category;` item. -#[allow(dead_code)] -pub struct BareStatic { - pub attrs: Vec, - pub vis: Visibility, - pub static_token: Token![static], - pub ident: Ident, - pub colon_token: Token![:], - pub ty: Type, - pub semi_token: Token![;], -} - -impl Parse for BareStatic { - fn parse(input: ParseStream) -> Result { - Ok(Self { - attrs: input.call(Attribute::parse_outer)?, - vis: input.parse()?, - static_token: input.parse()?, - ident: input.parse()?, - colon_token: input.parse()?, - ty: input.parse()?, - semi_token: input.parse()?, - }) - } -} diff --git a/crates/typst-macros/src/lib.rs b/crates/typst-macros/src/lib.rs index 578389c7f..82e63ddc8 100644 --- a/crates/typst-macros/src/lib.rs +++ b/crates/typst-macros/src/lib.rs @@ -5,7 +5,6 @@ extern crate proc_macro; #[macro_use] mod util; mod cast; -mod category; mod elem; mod func; mod scope; @@ -266,15 +265,6 @@ pub fn scope(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { .into() } -/// Defines a category of definitions. -#[proc_macro_attribute] -pub fn category(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { - let item = syn::parse_macro_input!(item as syn::Item); - category::category(stream.into(), item) - .unwrap_or_else(|err| err.to_compile_error()) - .into() -} - /// Implements `Reflect`, `FromValue`, and `IntoValue` for a type. /// /// - `Reflect` makes Typst's runtime aware of the type's characteristics. diff --git a/docs/Cargo.toml b/docs/Cargo.toml index 41a5645e8..acc551754 100644 --- a/docs/Cargo.toml +++ b/docs/Cargo.toml @@ -17,6 +17,8 @@ cli = ["clap", "typst-render", "serde_json"] [dependencies] typst = { workspace = true } +typst-render = { workspace = true, optional = true } +typst-utils = { workspace = true } typst-assets = { workspace = true, features = ["fonts"] } typst-dev-assets = { workspace = true } clap = { workspace = true, optional = true } @@ -28,7 +30,7 @@ serde_json = { workspace = true, optional = true } serde_yaml = { workspace = true } syntect = { workspace = true, features = ["html"] } typed-arena = { workspace = true } -typst-render = { workspace = true, optional = true } +unicode-math-class = { workspace = true } unscanny = { workspace = true } yaml-front-matter = { workspace = true } diff --git a/docs/guides/guide-for-latex-users.md b/docs/guides/guide-for-latex-users.md index 743afa5a6..5137ae1a9 100644 --- a/docs/guides/guide-for-latex-users.md +++ b/docs/guides/guide-for-latex-users.md @@ -657,7 +657,8 @@ applicable, contains possible workarounds. - **Well-established plotting ecosystem.** LaTeX users often create elaborate charts along with their documents in PGF/TikZ. The Typst ecosystem does not yet offer the same breadth of available options, but the ecosystem around the - [`cetz`](https://github.com/cetz-package/cetz) package is catching up quickly. + [`cetz` package](https://typst.app/universe/package/cetz) is catching up + quickly. - **Change page margins without a pagebreak.** In LaTeX, margins can always be adjusted, even without a pagebreak. To change margins in Typst, you use the @@ -670,4 +671,6 @@ applicable, contains possible workarounds. format, but you can easily convert both into SVG files with [online tools](https://cloudconvert.com/pdf-to-svg) or [Inkscape](https://inkscape.org/). The web app will automatically convert PDF - files to SVG files upon uploading them. + files to SVG files upon uploading them. You can also use the + community-provided [`muchpdf` package](https://typst.app/universe/package/muchpdf) + to embed PDFs. It internally converts PDFs to SVGs on-the-fly. diff --git a/docs/reference/export/html.md b/docs/reference/export/html.md new file mode 100644 index 000000000..330c2e136 --- /dev/null +++ b/docs/reference/export/html.md @@ -0,0 +1,61 @@ +
+ +Typst's HTML export is currently under active development. The feature is still +very incomplete and only available for experimentation behind a feature flag. Do +not use this feature for production use cases. In the CLI, you can experiment +with HTML export by passing `--features html` or setting the `TYPST_FEATURES` +environment variables to `html`. In the web app, HTML export is not available at +this time. Visit the [tracking issue](https://github.com/typst/typst/issues/5512) +to follow progress on HTML export and learn more about planned features. +
+ +HTML files describe a document structurally. The aim of Typst's HTML export is +to capture the structure of an input document and produce semantically rich HTML +that retains this structure. The resulting HTML should be accessible, +human-readable, and editable by hand and downstream tools. + +PDF, PNG, and SVG export, in contrast, all produce _visual_ representations of a +fully-laid out document. This divergence in the formats' intents means that +Typst cannot simply produce perfect HTML for your existing Typst documents. It +cannot always know what the best semantic HTML representation of your content +is. + +Instead, it gives _you_ full control: You can check the current export format +through the [`target`] function and when it is set to HTML, generate [raw HTML +elements]($html.elem). The primary intended use of these elements is in +templates and show rules. This way, the document's contents can be fully +agnostic to the export target and content can be shared between PDF and HTML +export. + +Currently, Typst will always output a single HTML file. Support for outputting +directories with multiple HTML documents and assets, as well as support for +outputting fragments that can be integrated into other HTML documents is +planned. + +Typst currently does not output CSS style sheets, instead focussing on emitting +semantic markup. You can of course write your own CSS styles and still benefit +from sharing your _content_ between PDF and HTML. For the future, we plan to +give you the option of automatically emitting CSS, taking more of your existing +set rules into account. + +# Exporting as HTML +## Command Line +Pass `--format html` to the `compile` or `watch` subcommand or provide an output +file name that ends with `.html`. Note that you must also pass `--features html` +or set `TYPST_FEATURES=html` to enable this experimental export target. + +When using `typst watch`, Typst will spin up a live-reloading HTTP server. You +can configure it as follows: + +- Pass `--port` to change the port. (Defaults to the first free port in the + range 3000-3005.) +- Pass `--no-reload` to disable injection of a live reload script. (The HTML + that is written to disk isn't affected either way.) +- Pass `--no-serve` to disable the server altogether. + +## Web App +Not currently available. + +# HTML-specific functionality +Typst exposes HTML-specific functionality in the global `html` module. See below +for the definitions it contains. diff --git a/docs/reference/export/pdf.md b/docs/reference/export/pdf.md new file mode 100644 index 000000000..b220ae946 --- /dev/null +++ b/docs/reference/export/pdf.md @@ -0,0 +1,71 @@ +PDF files focus on accurately describing documents visually, but also have +facilities for annotating their structure. This hybrid approach makes +them a good fit for document exchange: They render exactly the same on every +device, but also support extraction of a document's content and structure (at +least to an extent). Unlike PNG files, PDFs are not bound to a specific +resolution. Hence, you can view them at any size without incurring a loss of +quality. + +# PDF standards +The International Standards Organization (ISO) has published the base PDF +standard and various standards that extend it to make PDFs more suitable for +specific use-cases. By default, Typst exports PDF 1.7 files. Adobe Acrobat 8 and +later as well as all other commonly used PDF viewers are compatible with this +PDF version. + +## PDF/A +Typst optionally supports emitting PDF/A-conformant files. PDF/A files are +geared towards maximum compatibility with current and future PDF tooling. They +do not rely on difficult-to-implement or proprietary features and contain +exhaustive metadata. This makes them suitable for long-term archival. + +The PDF/A Standard has multiple versions (_parts_ in ISO terminology) and most +parts have multiple profiles that indicate the file's conformance level. +Currently, Typst supports these PDF/A output profiles: + +- PDF/A-2b: The basic conformance level of ISO 19005-2. This version of PDF/A is + based on PDF 1.7 and results in self-contained, archivable PDF files. + +- PDF/A-3b: The basic conformance level of ISO 19005-3. This version of PDF/A is + based on PDF 1.7 and results in archivable PDF files that can contain + arbitrary other related files as [attachments]($pdf.embed). The only + difference between it and PDF/A-2b is the capability to embed + non-PDF/A-conformant files within. + +When choosing between exporting PDF/A and regular PDF, keep in mind that PDF/A +files contain additional metadata, and that some readers will prevent the user +from modifying a PDF/A file. Some features of Typst may be disabled depending on +the PDF standard you choose. + +# Exporting as PDF +## Command Line +PDF is Typst's default export format. Running the `compile` or `watch` +subcommand without specifying a format will create a PDF. When exporting to PDF, +you have the following configuration options: + +- Which PDF standards Typst should enforce conformance with by specifying + `--pdf-standard` followed by one or multiple comma-separated standards. Valid + standards are `1.7`, `a-2b`, and `a-3b`. By default, Typst outputs + PDF-1.7-compliant files. + +- Which pages to export by specifying `--pages` followed by a comma-separated + list of numbers or dash-separated number ranges. Ranges can be half-open. + Example: `2,3,7-9,11-`. + +## Web App +Click the quick download button at the top right to export a PDF with default +settings. For further configuration, click "File" > "Export as" > "PDF" or click +the downwards-facing arrow next to the quick download button and select "Export +as PDF". When exporting to PDF, you have the following configuration options: + +- Which PDF standards Typst should enforce conformance with. By default, Typst + outputs PDF-1.7-compliant files. Valid additional standards are `A-2b` and + `A-3b`. + +- Which pages to export. Valid options are "All pages", "Current page", and + "Custom ranges". Custom ranges are a comma-separated list of numbers or + dash-separated number ranges. Ranges can be half-open. Example: `2,3,7-9,11-`. + +# PDF-specific functionality +Typst exposes PDF-specific functionality in the global `pdf` module. See below +for the definitions it contains. diff --git a/docs/reference/export/png.md b/docs/reference/export/png.md new file mode 100644 index 000000000..fe122f4d3 --- /dev/null +++ b/docs/reference/export/png.md @@ -0,0 +1,61 @@ +Instead of creating a PDF, Typst can also directly render pages to PNG raster +graphics. PNGs are losslessly compressed images that can contain one page at a +time. When exporting a multi-page document, Typst will emit multiple PNGs. PNGs +are a good choice when you want to use Typst's output in an image editing +software or when you can use none of Typst's other export formats. + +In contrast to Typst's other export formats, PNGs are bound to a specific +resolution. When exporting to PNG, you can configure the resolution as pixels +per inch (PPI). If the medium you view the PNG on has a finer resolution than +the PNG you exported, you will notice a loss of quality. Typst calculates the +resolution of your PNGs based on each page's physical dimensions and the PPI. If +you need guidance for choosing a PPI value, consider the following: + +- A DPI value of 300 or 600 is typical for desktop printing. +- Professional prints of detailed graphics can go up to 1200 PPI. +- If your document is only viewed at a distance, e.g. a poster, you may choose a + smaller value than 300. +- If your document is viewed on screens, a typical PPI value for a smartphone is + 400-500. + +Because PNGs only contain a pixel raster, the text within cannot be extracted +automatically (without OCR), for example by copy/paste or a screen reader. If +you need the text to be accessible, export a PDF or HTML file instead. + +PNGs can have transparent backgrounds. By default, Typst will output a PNG with +an opaque white background. You can make the background transparent using +`[#set page(fill: none)]`. Learn more on the +[`page` function's reference page]($page.fill). + +# Exporting as PNG +## Command Line +Pass `--format png` to the `compile` or `watch` subcommand or provide an output +file name that ends with `.png`. + +If your document has more than one page, Typst will create multiple image files. +The output file name must then be a template string containing at least one of +- `[{p}]`, which will be replaced by the page number +- `[{0p}]`, which will be replaced by the zero-padded page number (so that all + numbers have the same length) +- `[{t}]`, which will be replaced by the total number of pages + +When exporting to PNG, you have the following configuration options: + +- Which resolution to render at by specifying `--ppi` followed by a number of + pixels per inch. The default is `144`. + +- Which pages to export by specifying `--pages` followed by a comma-separated + list of numbers or dash-separated number ranges. Ranges can be half-open. + Example: `2,3,7-9,11-`. + +## Web App +Click "File" > "Export as" > "PNG" or click the downwards-facing arrow next to +the quick download button and select "Export as PNG". When exporting to PNG, you +have the following configuration options: + +- The resolution at which the pages should be rendered, as a number of pixels + per inch. The default is `144`. + +- Which pages to export. Valid options are "All pages", "Current page", and + "Custom ranges". Custom ranges are a comma-separated list of numbers or + dash-separated number ranges. Ranges can be half-open. Example: `2,3,7-9,11-`. diff --git a/docs/reference/export/svg.md b/docs/reference/export/svg.md new file mode 100644 index 000000000..630ab8452 --- /dev/null +++ b/docs/reference/export/svg.md @@ -0,0 +1,48 @@ +Instead of creating a PDF, Typst can also directly render pages to scalable +vector graphics (SVGs), which are the preferred format for embedding vector +graphics in web pages. Like PDF files, SVGs display your document exactly how +you have laid it out in Typst. Likewise, they share the benefit of not being +bound to a specific resolution. Hence, you can print or view SVG files on any +device without incurring a loss of quality. (Note that font printing quality may +be better with a PDF.) In contrast to a PDF, an SVG cannot contain multiple +pages. When exporting a multi-page document, Typst will emit multiple SVGs. + +SVGs can represent text in two ways: By embedding the text itself and rendering +it with the fonts available on the viewer's computer or by embedding the shapes +of each glyph in the font used to create the document. To ensure that the SVG +file looks the same across all devices it is viewed on, Typst chooses the latter +method. This means that the text in the SVG cannot be extracted automatically, +for example by copy/paste or a screen reader. If you need the text to be +accessible, export a PDF or HTML file instead. + +SVGs can have transparent backgrounds. By default, Typst will output an SVG with +an opaque white background. You can make the background transparent using +`[#set page(fill: none)]`. Learn more on the +[`page` function's reference page]($page.fill). + +# Exporting as SVG +## Command Line +Pass `--format svg` to the `compile` or `watch` subcommand or provide an output +file name that ends with `.svg`. + +If your document has more than one page, Typst will create multiple image files. +The output file name must then be a template string containing at least one of +- `[{p}]`, which will be replaced by the page number +- `[{0p}]`, which will be replaced by the zero-padded page number (so that all + numbers have the same length) +- `[{t}]`, which will be replaced by the total number of pages + +When exporting to SVG, you have the following configuration options: + +- Which pages to export by specifying `--pages` followed by a comma-separated + list of numbers or dash-separated number ranges. Ranges can be half-open. + Example: `2,3,7-9,11-`. + +## Web App +Click "File" > "Export as" > "SVG" or click the downwards-facing arrow next to +the quick download button and select "Export as SVG". When exporting to SVG, you +have the following configuration options: + +- Which pages to export. Valid options are "All pages", "Current page", and + "Custom ranges". Custom ranges are a comma-separated list of numbers or + dash-separated number ranges. Ranges can be half-open. Example: `2,3,7-9,11-`. diff --git a/docs/reference/context.md b/docs/reference/language/context.md similarity index 100% rename from docs/reference/context.md rename to docs/reference/language/context.md diff --git a/docs/reference/scripting.md b/docs/reference/language/scripting.md similarity index 100% rename from docs/reference/scripting.md rename to docs/reference/language/scripting.md diff --git a/docs/reference/styling.md b/docs/reference/language/styling.md similarity index 100% rename from docs/reference/styling.md rename to docs/reference/language/styling.md diff --git a/docs/reference/syntax.md b/docs/reference/language/syntax.md similarity index 100% rename from docs/reference/syntax.md rename to docs/reference/language/syntax.md diff --git a/docs/reference/library/data-loading.md b/docs/reference/library/data-loading.md new file mode 100644 index 000000000..659a8cccc --- /dev/null +++ b/docs/reference/library/data-loading.md @@ -0,0 +1,4 @@ +Data loading from external files. + +These functions help you with loading and embedding data, for example from the +results of an experiment. diff --git a/docs/reference/library/foundations.md b/docs/reference/library/foundations.md new file mode 100644 index 000000000..738c3789d --- /dev/null +++ b/docs/reference/library/foundations.md @@ -0,0 +1,4 @@ +Foundational types and functions. + +Here, you'll find documentation for basic data types like [integers]($int) and +[strings]($str) as well as details about core computational functions. diff --git a/docs/reference/library/introspection.md b/docs/reference/library/introspection.md new file mode 100644 index 000000000..f48a9937c --- /dev/null +++ b/docs/reference/library/introspection.md @@ -0,0 +1,10 @@ +Interactions between document parts. + +This category is home to Typst's introspection capabilities: With the `counter` +function, you can access and manipulate page, section, figure, and equation +counters or create custom ones. Meanwhile, the `query` function lets you search +for elements in the document to construct things like a list of figures or +headers which show the current chapter title. + +Most of the functions are _contextual._ It is recommended to read the chapter on +[context] before continuing here. diff --git a/docs/reference/library/layout.md b/docs/reference/library/layout.md new file mode 100644 index 000000000..450058d4c --- /dev/null +++ b/docs/reference/library/layout.md @@ -0,0 +1,3 @@ +Arranging elements on the page in different ways. + +By combining layout functions, you can create complex and automatic layouts. diff --git a/docs/reference/library/math.md b/docs/reference/library/math.md new file mode 100644 index 000000000..61f2bb58f --- /dev/null +++ b/docs/reference/library/math.md @@ -0,0 +1,101 @@ +Typst has special [syntax]($syntax/#math) and library functions to typeset +mathematical formulas. Math formulas can be displayed inline with text or as +separate blocks. They will be typeset into their own block if they start and end +with at least one space (e.g. `[$ x^2 $]`). + +# Variables +In math, single letters are always displayed as is. Multiple letters, however, +are interpreted as variables and functions. To display multiple letters +verbatim, you can place them into quotes and to access single letter variables, +you can use the [hash syntax]($scripting/#expressions). + +```example +$ A = pi r^2 $ +$ "area" = pi dot "radius"^2 $ +$ cal(A) := + { x in RR | x "is natural" } $ +#let x = 5 +$ #x < 17 $ +``` + +# Symbols +Math mode makes a wide selection of [symbols]($category/symbols/sym) like `pi`, +`dot`, or `RR` available. Many mathematical symbols are available in different +variants. You can select between different variants by applying +[modifiers]($symbol) to the symbol. Typst further recognizes a number of +shorthand sequences like `=>` that approximate a symbol. When such a shorthand +exists, the symbol's documentation lists it. + +```example +$ x < y => x gt.eq.not y $ +``` + +# Line Breaks +Formulas can also contain line breaks. Each line can contain one or multiple +_alignment points_ (`&`) which are then aligned. + +```example +$ sum_(k=0)^n k + &= 1 + ... + n \ + &= (n(n+1)) / 2 $ +``` + +# Function calls +Math mode supports special function calls without the hash prefix. In these +"math calls", the argument list works a little differently than in code: + +- Within them, Typst is still in "math mode". Thus, you can write math directly + into them, but need to use hash syntax to pass code expressions (except for + strings, which are available in the math syntax). +- They support positional and named arguments, as well as argument spreading. +- They don't support trailing content blocks. +- They provide additional syntax for 2-dimensional argument lists. The semicolon + (`;`) merges preceding arguments separated by commas into an array argument. + +```example +$ frac(a^2, 2) $ +$ vec(1, 2, delim: "[") $ +$ mat(1, 2; 3, 4) $ +$ mat(..#range(1, 5).chunks(2)) $ +$ lim_x = + op("lim", limits: #true)_x $ +``` + +To write a verbatim comma or semicolon in a math call, escape it with a +backslash. The colon on the other hand is only recognized in a special way if +directly preceded by an identifier, so to display it verbatim in those cases, +you can just insert a space before it. + +Functions calls preceded by a hash are normal code function calls and not +affected by these rules. + +# Alignment +When equations include multiple _alignment points_ (`&`), this creates blocks of +alternatingly right- and left-aligned columns. In the example below, the +expression `(3x + y) / 7` is right-aligned and `= 9` is left-aligned. The word +"given" is also left-aligned because `&&` creates two alignment points in a row, +alternating the alignment twice. `& &` and `&&` behave exactly the same way. +Meanwhile, "multiply by 7" is right-aligned because just one `&` precedes it. +Each alignment point simply alternates between right-aligned/left-aligned. + +```example +$ (3x + y) / 7 &= 9 && "given" \ + 3x + y &= 63 & "multiply by 7" \ + 3x &= 63 - y && "subtract y" \ + x &= 21 - y/3 & "divide by 3" $ +``` + +# Math fonts +You can set the math font by with a [show-set rule]($styling/#show-rules) as +demonstrated below. Note that only special OpenType math fonts are suitable for +typesetting maths. + +```example +#show math.equation: set text(font: "Fira Math") +$ sum_(i in NN) 1 + i $ +``` + +# Math module +All math functions are part of the `math` [module]($scripting/#modules), which +is available by default in equations. Outside of equations, they can be accessed +with the `math.` prefix. diff --git a/docs/reference/library/model.md b/docs/reference/library/model.md new file mode 100644 index 000000000..e433ed53b --- /dev/null +++ b/docs/reference/library/model.md @@ -0,0 +1,5 @@ +Document structuring. + +Here, you can find functions to structure your document and interact with that +structure. This includes section headings, figures, bibliography management, +cross-referencing and more. diff --git a/docs/reference/library/symbols.md b/docs/reference/library/symbols.md new file mode 100644 index 000000000..2e6f48cdb --- /dev/null +++ b/docs/reference/library/symbols.md @@ -0,0 +1,5 @@ +These two modules give names to symbols and emoji to make them easy to insert +with a normal keyboard. Alternatively, you can also always directly enter +Unicode symbols into your text and formulas. In addition to the symbols listed +below, math mode defines `dif` and `Dif`. These are not normal symbol values +because they also affect spacing and font style. diff --git a/docs/reference/library/text.md b/docs/reference/library/text.md new file mode 100644 index 000000000..239c0b265 --- /dev/null +++ b/docs/reference/library/text.md @@ -0,0 +1,3 @@ +Text styling. + +The [text function]($text) is of particular interest. diff --git a/docs/reference/library/visualize.md b/docs/reference/library/visualize.md new file mode 100644 index 000000000..9259401f8 --- /dev/null +++ b/docs/reference/library/visualize.md @@ -0,0 +1,5 @@ +Drawing and data visualization. + +If you want to create more advanced drawings or plots, also have a look at the +[CetZ](https://github.com/johannes-wolf/cetz) package as well as more +specialized [packages]($universe) for your use case. diff --git a/docs/reference/packages.md b/docs/reference/packages.md deleted file mode 100644 index bfd1ef580..000000000 --- a/docs/reference/packages.md +++ /dev/null @@ -1,6 +0,0 @@ -Typst [packages]($scripting/#packages) encapsulate reusable building blocks -and make them reusable across projects. Below is a list of Typst packages -created by the community. Due to the early and experimental nature of Typst's -package management, they all live in a `preview` namespace. Click on a package's -name to view its documentation and use the copy button on the right to get a -full import statement for it. diff --git a/docs/src/html.rs b/docs/src/html.rs index 4eb3954c3..9077d5c47 100644 --- a/docs/src/html.rs +++ b/docs/src/html.rs @@ -301,7 +301,10 @@ impl<'a> Handler<'a> { return; } - let default = self.peeked.as_ref().map(|text| text.to_kebab_case()); + let body = self.peeked.as_ref(); + let default = body.map(|text| text.to_kebab_case()); + let has_id = id_slot.is_some(); + let id: &'a str = match (&id_slot, default) { (Some(id), default) => { if Some(*id) == default.as_deref() { @@ -316,10 +319,10 @@ impl<'a> Handler<'a> { *id_slot = (!id.is_empty()).then_some(id); // Special case for things like "v0.3.0". - let name = if id.starts_with('v') && id.contains('.') { - id.into() - } else { - id.to_title_case().into() + let name = match &body { + _ if id.starts_with('v') && id.contains('.') => id.into(), + Some(body) if !has_id => body.as_ref().into(), + _ => id.to_title_case().into(), }; let mut children = &mut self.outline; diff --git a/docs/src/lib.rs b/docs/src/lib.rs index ff745c9c2..f9ee05bbd 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -12,27 +12,20 @@ pub use self::model::*; use std::collections::HashSet; use ecow::{eco_format, EcoString}; +use heck::ToTitleCase; use serde::Deserialize; use serde_yaml as yaml; use std::sync::LazyLock; use typst::diag::{bail, StrResult}; -use typst::foundations::Binding; use typst::foundations::{ - AutoValue, Bytes, CastInfo, Category, Func, Module, NoneValue, ParamInfo, Repr, - Scope, Smart, Type, Value, FOUNDATIONS, + AutoValue, Binding, Bytes, CastInfo, Func, Module, NoneValue, ParamInfo, Repr, Scope, + Smart, Type, Value, }; -use typst::html::HTML; -use typst::introspection::INTROSPECTION; -use typst::layout::{Abs, Margin, PageElem, PagedDocument, LAYOUT}; -use typst::loading::DATA_LOADING; -use typst::math::MATH; -use typst::model::MODEL; -use typst::pdf::PDF; -use typst::symbols::SYMBOLS; -use typst::text::{Font, FontBook, TEXT}; +use typst::layout::{Abs, Margin, PageElem, PagedDocument}; +use typst::text::{Font, FontBook}; use typst::utils::LazyHash; -use typst::visualize::VISUALIZE; -use typst::{Feature, Library, LibraryBuilder}; +use typst::{Category, Feature, Library, LibraryBuilder}; +use unicode_math_class::MathClass; macro_rules! load { ($path:literal) => { @@ -64,9 +57,10 @@ static LIBRARY: LazyLock> = LazyLock::new(|| { let scope = lib.global.scope_mut(); // Add those types, so that they show up in the docs. - scope.start_category(FOUNDATIONS); + scope.start_category(Category::Foundations); scope.define_type::(); scope.define_type::(); + scope.reset_category(); // Adjust the default look. lib.styles @@ -155,21 +149,24 @@ fn reference_pages(resolver: &dyn Resolver) -> PageModel { let mut page = md_page(resolver, resolver.base(), load!("reference/welcome.md")); let base = format!("{}reference/", resolver.base()); page.children = vec![ - md_page(resolver, &base, load!("reference/syntax.md")).with_part("Language"), - md_page(resolver, &base, load!("reference/styling.md")), - md_page(resolver, &base, load!("reference/scripting.md")), - md_page(resolver, &base, load!("reference/context.md")), - category_page(resolver, FOUNDATIONS).with_part("Library"), - category_page(resolver, MODEL), - category_page(resolver, TEXT), - category_page(resolver, MATH), - category_page(resolver, SYMBOLS), - category_page(resolver, LAYOUT), - category_page(resolver, VISUALIZE), - category_page(resolver, INTROSPECTION), - category_page(resolver, DATA_LOADING), - category_page(resolver, PDF), - category_page(resolver, HTML), + md_page(resolver, &base, load!("reference/language/syntax.md")) + .with_part("Language"), + md_page(resolver, &base, load!("reference/language/styling.md")), + md_page(resolver, &base, load!("reference/language/scripting.md")), + md_page(resolver, &base, load!("reference/language/context.md")), + category_page(resolver, Category::Foundations).with_part("Library"), + category_page(resolver, Category::Model), + category_page(resolver, Category::Text), + category_page(resolver, Category::Math), + category_page(resolver, Category::Symbols), + category_page(resolver, Category::Layout), + category_page(resolver, Category::Visualize), + category_page(resolver, Category::Introspection), + category_page(resolver, Category::DataLoading), + category_page(resolver, Category::Pdf).with_part("Export"), + category_page(resolver, Category::Html), + category_page(resolver, Category::Png), + category_page(resolver, Category::Svg), ]; page } @@ -219,14 +216,16 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { let mut markup = vec![]; let mut math = vec![]; - let (module, path): (&Module, &[&str]) = if category == MATH { - (&LIBRARY.math, &["math"]) - } else { - (&LIBRARY.global, &[]) + let docs = category_docs(category); + let (module, path): (&Module, &[&str]) = match category { + Category::Math => (&LIBRARY.math, &["math"]), + Category::Pdf => (get_module(&LIBRARY.global, "pdf").unwrap(), &["pdf"]), + Category::Html => (get_module(&LIBRARY.global, "html").unwrap(), &["html"]), + _ => (&LIBRARY.global, &[]), }; // Add groups. - for group in GROUPS.iter().filter(|g| g.category == category.name()).cloned() { + for group in GROUPS.iter().filter(|g| g.category == category).cloned() { if matches!(group.name.as_str(), "sym" | "emoji") { let subpage = symbols_page(resolver, &route, &group); let BodyModel::Symbols(model) = &subpage.body else { continue }; @@ -243,7 +242,7 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { items.push(CategoryItem { name: group.name.clone(), route: subpage.route.clone(), - oneliner: oneliner(category.docs()).into(), + oneliner: oneliner(docs).into(), code: true, }); children.push(subpage); @@ -256,15 +255,15 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { } // Add symbol pages. These are ordered manually. - if category == SYMBOLS { + if category == Category::Symbols { shorthands = Some(ShorthandsModel { markup, math }); } let mut skip = HashSet::new(); - if category == MATH { + if category == Category::Math { skip = GROUPS .iter() - .filter(|g| g.category == category.name()) + .filter(|g| g.category == category) .flat_map(|g| &g.filter) .map(|s| s.as_str()) .collect(); @@ -273,6 +272,11 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { skip.insert("text"); } + // Tiling would be duplicate otherwise. + if category == Category::Visualize { + skip.insert("pattern"); + } + // Add values and types. let scope = module.scope(); for (name, binding) in scope.iter() { @@ -287,8 +291,8 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { match binding.read() { Value::Func(func) => { let name = func.name().unwrap(); - - let subpage = func_page(resolver, &route, func, path); + let subpage = + func_page(resolver, &route, func, path, binding.deprecation()); items.push(CategoryItem { name: name.into(), route: subpage.route.clone(), @@ -311,31 +315,39 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { } } - if category != SYMBOLS { + if category != Category::Symbols { children.sort_by_cached_key(|child| child.title.clone()); items.sort_by_cached_key(|item| item.name.clone()); } - let name = category.title(); - let details = Html::markdown(resolver, category.docs(), Some(1)); + let title = EcoString::from(match category { + Category::Pdf | Category::Html | Category::Png | Category::Svg => { + category.name().to_uppercase() + } + _ => category.name().to_title_case(), + }); + + let details = Html::markdown(resolver, docs, Some(1)); let mut outline = vec![OutlineItem::from_name("Summary")]; outline.extend(details.outline()); - outline.push(OutlineItem::from_name("Definitions")); + if !items.is_empty() { + outline.push(OutlineItem::from_name("Definitions")); + } if shorthands.is_some() { outline.push(OutlineItem::from_name("Shorthands")); } PageModel { route, - title: name.into(), + title: title.clone(), description: eco_format!( - "Documentation for functions related to {name} in Typst." + "Documentation for functions related to {title} in Typst." ), part: None, outline, body: BodyModel::Category(CategoryModel { name: category.name(), - title: category.title(), + title, details, items, shorthands, @@ -344,14 +356,34 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { } } +/// Retrieve the docs for a category. +fn category_docs(category: Category) -> &'static str { + match category { + Category::Foundations => load!("reference/library/foundations.md"), + Category::Introspection => load!("reference/library/introspection.md"), + Category::Layout => load!("reference/library/layout.md"), + Category::DataLoading => load!("reference/library/data-loading.md"), + Category::Math => load!("reference/library/math.md"), + Category::Model => load!("reference/library/model.md"), + Category::Symbols => load!("reference/library/symbols.md"), + Category::Text => load!("reference/library/text.md"), + Category::Visualize => load!("reference/library/visualize.md"), + Category::Pdf => load!("reference/export/pdf.md"), + Category::Html => load!("reference/export/html.md"), + Category::Svg => load!("reference/export/svg.md"), + Category::Png => load!("reference/export/png.md"), + } +} + /// Create a page for a function. fn func_page( resolver: &dyn Resolver, parent: &str, func: &Func, path: &[&str], + deprecation: Option<&'static str>, ) -> PageModel { - let model = func_model(resolver, func, path, false); + let model = func_model(resolver, func, path, false, deprecation); let name = func.name().unwrap(); PageModel { route: eco_format!("{parent}{}/", urlify(name)), @@ -370,6 +402,7 @@ fn func_model( func: &Func, path: &[&str], nested: bool, + deprecation: Option<&'static str>, ) -> FuncModel { let name = func.name().unwrap(); let scope = func.scope().unwrap(); @@ -383,7 +416,11 @@ fn func_model( } let mut returns = vec![]; - casts(resolver, &mut returns, &mut vec![], func.returns().unwrap()); + let mut strings = vec![]; + casts(resolver, &mut returns, &mut strings, func.returns().unwrap()); + if !strings.is_empty() && !returns.contains(&"str") { + returns.push("str"); + } returns.sort_by_key(|ty| type_index(ty)); if returns == ["none"] { returns.clear(); @@ -401,6 +438,7 @@ fn func_model( oneliner: oneliner(details), element: func.element().is_some(), contextual: func.contextual().unwrap_or(false), + deprecation, details: Html::markdown(resolver, details, nesting), example: example.map(|md| Html::markdown(resolver, md, None)), self_, @@ -483,7 +521,7 @@ fn scope_models(resolver: &dyn Resolver, name: &str, scope: &Scope) -> Vec() else { panic!("not a function") }; - let func = func_model(resolver, func, &path, true); + let binding = group.module().scope().get(name).unwrap(); + let Ok(ref func) = binding.read().clone().cast::() else { + panic!("not a function") + }; + let func = func_model(resolver, func, &path, true, binding.deprecation()); let id_base = urlify(&eco_format!("functions-{}", func.name)); let children = func_outline(&func, &id_base); outline_items.push(OutlineItem { @@ -628,7 +668,7 @@ fn type_model(resolver: &dyn Resolver, ty: &Type) -> TypeModel { constructor: ty .constructor() .ok() - .map(|func| func_model(resolver, &func, &[], true)), + .map(|func| func_model(resolver, &func, &[], true, None)), scope: scope_models(resolver, ty.short_name(), ty.scope()), } } @@ -682,10 +722,19 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel { list.iter().copied().find(|&(_, x)| x == c).map(|(s, _)| s) }; + let name = complete(variant); + let deprecation = match name.as_str() { + "integral.sect" => { + Some("`integral.sect` is deprecated, use `integral.inter` instead") + } + _ => binding.deprecation(), + }; + list.push(SymbolModel { - name: complete(variant), + name, markup_shorthand: shorthand(typst::syntax::ast::Shorthand::LIST), math_shorthand: shorthand(typst::syntax::ast::MathShorthand::LIST), + math_class: typst_utils::default_math_class(c).map(math_class_name), codepoint: c as _, accent: typst::math::Accent::combine(c).is_some(), alternates: symbol @@ -693,6 +742,7 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel { .filter(|(other, _)| other != &variant) .map(|(other, _)| complete(other)) .collect(), + deprecation, }); } } @@ -769,12 +819,32 @@ const TYPE_ORDER: &[&str] = &[ "stroke", ]; +fn math_class_name(class: MathClass) -> &'static str { + match class { + MathClass::Normal => "Normal", + MathClass::Alphabetic => "Alphabetic", + MathClass::Binary => "Binary", + MathClass::Closing => "Closing", + MathClass::Diacritic => "Diacritic", + MathClass::Fence => "Fence", + MathClass::GlyphPart => "Glyph Part", + MathClass::Large => "Large", + MathClass::Opening => "Opening", + MathClass::Punctuation => "Punctuation", + MathClass::Relation => "Relation", + MathClass::Space => "Space", + MathClass::Unary => "Unary", + MathClass::Vary => "Vary", + MathClass::Special => "Special", + } +} + /// Data about a collection of functions. #[derive(Debug, Clone, Deserialize)] struct GroupData { name: EcoString, title: EcoString, - category: EcoString, + category: Category, #[serde(default)] path: Vec, #[serde(default)] diff --git a/docs/src/link.rs b/docs/src/link.rs index c55261b84..2e836b6ce 100644 --- a/docs/src/link.rs +++ b/docs/src/link.rs @@ -44,6 +44,8 @@ fn resolve_known(head: &str, base: &str) -> Option { "$styling" => format!("{base}reference/styling"), "$scripting" => format!("{base}reference/scripting"), "$context" => format!("{base}reference/context"), + "$html" => format!("{base}reference/html"), + "$pdf" => format!("{base}reference/pdf"), "$guides" => format!("{base}guides"), "$changelog" => format!("{base}changelog"), "$universe" => "https://typst.app/universe".into(), @@ -73,11 +75,14 @@ fn resolve_definition(head: &str, base: &str) -> StrResult { // Handle grouped functions. if let Some(group) = GROUPS.iter().find(|group| { - group.category == category.name() && group.filter.iter().any(|func| func == name) + group.category == category && group.filter.iter().any(|func| func == name) }) { let mut route = format!( "{}reference/{}/{}/#functions-{}", - base, group.category, group.name, name + base, + group.category.name(), + group.name, + name ); if let Some(param) = parts.next() { route.push('-'); diff --git a/docs/src/model.rs b/docs/src/model.rs index b222322a7..801c60c7f 100644 --- a/docs/src/model.rs +++ b/docs/src/model.rs @@ -64,7 +64,7 @@ pub enum BodyModel { #[derive(Debug, Serialize)] pub struct CategoryModel { pub name: &'static str, - pub title: &'static str, + pub title: EcoString, pub details: Html, pub items: Vec, pub shorthands: Option, @@ -89,6 +89,7 @@ pub struct FuncModel { pub oneliner: &'static str, pub element: bool, pub contextual: bool, + pub deprecation: Option<&'static str>, pub details: Html, /// This example is only for nested function models. Others can have /// their example directly in their details. @@ -163,6 +164,8 @@ pub struct SymbolModel { pub alternates: Vec, pub markup_shorthand: Option<&'static str>, pub math_shorthand: Option<&'static str>, + pub math_class: Option<&'static str>, + pub deprecation: Option<&'static str>, } /// Shorthands listed on a category page. From 4a9a5d2716fc91f60734769eb001aef32fe15403 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 5 Feb 2025 14:47:32 +0100 Subject: [PATCH 012/172] 0.13 changelog (#5801) --- docs/changelog/0.13.0.md | 324 ++++++++++++++++++++++++++++++++++++++ docs/changelog/welcome.md | 1 + docs/src/lib.rs | 1 + 3 files changed, 326 insertions(+) create mode 100644 docs/changelog/0.13.0.md diff --git a/docs/changelog/0.13.0.md b/docs/changelog/0.13.0.md new file mode 100644 index 000000000..50819f659 --- /dev/null +++ b/docs/changelog/0.13.0.md @@ -0,0 +1,324 @@ +--- +title: Unreleased changes planned for 0.13.0 +description: Changes slated to appear in Typst 0.13.0 +--- + +# Unreleased + +## Highlights +- There is now a distinction between [proper paragraphs]($par) and just + inline-level content. This is important for future work on accessibility and + means that [first line indent]($par.first-line-indent) can now be enabled for + all paragraphs instead of just consecutive ones. +- The [`outline`] has a better out-of-the-box look and is more customizable +- The new [`curve`] function (that supersedes the `path` function) provides a + simpler and more flexible interface for creating Bézier curves +- The `image` function now supports raw [pixel raster formats]($image.format) + for generating images from within Typst +- Functions that accept [file paths]($syntax/#paths) now also accept raw + [bytes] instead, for full flexibility +- WebAssembly [plugins]($plugin) are more flexible and automatically run + multi-threaded +- Fixed a long-standing bug where single-letter strings in math (`[$"a"$]`) + would be displayed in italics +- You can now specify which charset should be [covered]($text.font) by which + font family +- The [`pdf.embed`] function lets you embed arbitrary files in the exported + PDF +- HTML export is currently under active development. The feature is still _very_ + incomplete, but already available for experimentation behind a feature flag. + +## Model +- There is now a distinction between [proper paragraphs]($par) and just + inline-level content **(Breaking change)** + - All text at the root of a document is wrapped in paragraphs. Meanwhile, text + in a container (like a block) is only wrapped in a paragraph if the + container holds any block-level content. If all of the content is + inline-level, no paragraph is created. + - In the laid-out document, it's not immediately visible whether text became + part of a paragraph. However, it is still important for accessibility, HTML + export, and for properties like `first-line-indent`. + - Show rules on `par` now only affect proper paragraphs + - The `first-line-indent` and `hanging-indent` properties also only affect + proper paragraphs + - Creating a `{par[..]}` with body content that is not fully inline-level will + result in a warning + - The default show rules of various built-in elements like lists, quotes, etc. + were adjusted to ensure they produce/don't produce paragraphs as appropriate +- The [`outline`] function was fully reworked to improve its out-of-the-box + behavior **(Breaking change)** + - [Outline entries]($outline.entry) are now [blocks]($block) and are thus + affected by block spacing + - The `{auto}` indentation mode now aligns numberings and titles outline-wide + for a grid-like look + - Automatic indentation now also indents entries without a numbering + - Titles wrapping over multiple lines now have hanging indent + - The page number won't appear alone on its own line anymore + - The link now spans the full entry instead of just the title and page number + - The default spacing between outline leader dots was increased + - The [`fill`]($outline.entry.fill) parameter was moved from `outline` to + `outline.entry` and can thus be configured through show-set rules + - Removed `body` and `page` fields from outline entry + - Added `indented`, `prefix`, `inner`, `body`, and `page` methods on outline + entries to simplify writing of show rules +- Added configuration to [`par.first-line-indent`] for indenting all paragraphs + instead of just consecutive ones +- Added [`form`]($ref.form) parameter to `ref` function. Setting the form to + `{"page"}` will produce a page reference instead of a textual one. +- Added [`document.description`] field, which results in corresponding PDF and + HTML metadata +- Added [`enum.reversed`] parameter +- Added support for Greek [numbering] +- When the [`link`] function wraps around a container like a [block], it will + now generate only one link for the whole block instead of individual links for + all the visible leaf elements. This significantly reduces PDF file sizes when + combining `link` and [`repeat`]. +- The [`link`] function will now only strip one prefix (like `mailto:` or + `tel:`) instead of multiple +- The link function now suppresses hyphenation via a built-in show-set rule + rather than through its default show rule +- Displaying the page counter without a specified numbering will now take the + page numbering into account + +## Visualization +- Added new [`curve`] function that supersedes the [`path`] function and + provides a simpler and more flexible interface. The `path` function is now + deprecated. +- The `image` function now supports raw [pixel raster formats]($image.format). + This can be used to generate images from within Typst without the need for + encoding in an image exchange format. +- Added [`image.scaling`] parameter for configuring how an image is scaled by + PNG export and PDF viewers (smooth or pixelated) +- Added [`image.icc`] parameter for providing or overriding the ICC profile of + an image +- Renamed `pattern` to [`tiling`]. The name `pattern` remains as a deprecated + alias. +- Added [`gradient.center`], [`gradient.radius`], [`gradient.focal-center`], and + [`gradient.focal-radius`] methods +- Fixed interaction of clipping and outset on [`box`] and [`block`] +- Fixed panic with [`path`] of infinite length +- Fixed non-solid (e.g. tiling) text fills in clipped blocks +- Auto-detection of image formats from a raw buffer now has basic support for + SVGs + +## Scripting +- Functions that accept [file paths]($syntax/#paths) now also accept raw + [bytes] + - [`image`], [`cbor`], [`csv`], [`json`], [`toml`], [`xml`], and [`yaml`] now + support a path string or bytes and their `.decode` variants are deprecated + - [`plugin`], [`bibliography`], [`bibliography.style`], [`cite.style`], + [`raw.theme`], and [`raw.syntaxes`] now accept bytes in addition to path + strings. These did not have `.decode` variants, so this adds new + flexibility. + - The `path` argument/field of [`image`] and [`bibliography`] was renamed to + `source` and `sources`, respectively **(Minor breaking change)** +- Improved WebAssembly [plugins]($plugin) + - The `plugin` type is replaced by a [`plugin` function]($plugin) that returns + a [module] containing normal Typst functions. This module can be used with + import syntax. **(Breaking change)** + - Plugins now automatically run in multiple threads without any changes by + plugin authors + - A new [`plugin.transition`] API is introduced which allows plugins to run + impure initialization in a way that doesn't break Typst's purity guarantees +- The variable name bound by a bare import (no renaming, no import list) is now + determined statically and dynamic imports without `{as}` renaming (e.g. + `{import "ot" + "her.typ"}`) are a hard error **(Breaking change)** +- Values of the [`arguments`] type can now be added with `+` and + [joined]($scripting/#blocks) in curly-braced code blocks +- Functions in an element function's scope can now be called with method syntax, + bringing elements and types closer (in anticipation of a future full + unification of the two). Currently, this is only useful for [`outline.entry`] + as no other element function defines methods. +- Added [`calc.norm`] function +- Added support for 32-bit floats in [`float.from-bytes`] and [`float.to-bytes`] +- The [`decimal`] constructor now also accepts decimal values +- Improved `repr` of [symbols]($symbol), [arguments], and [types]($type) +- Duplicate [symbol] variants and modifiers are now a hard error + **(Breaking change)** + +## Math +- Fixed a bug where single letter strings in math (`[$"a"$]`) would be displayed + in italics +- Math function calls can now have hyphenated named arguments and support + [argument spreading]($arguments/#spreading) +- Better looking accents thanks to support for the `flac` (Flattened Accent + Forms) and `dtls` (Dotless Forms) OpenType features +- Added `lcm` [text operator]($math.op) +- The [`bold`]($math.bold) function now works with ϝ and Ϝ +- The [`italic`]($math.italic) function now works with ħ +- Fixed a bug where the extent of a math equation was wrongly affected by + internal metadata +- Fixed interaction of [`lr`]($math.lr) and [context] expressions +- Fixed weak spacing being unconditionally ignored in [`lr`]($math.lr) +- Fixed sub/superscripts sometimes being in the wrong position with + [`lr`]($math.lr) +- Fixed multi-line annotations (e.g. overbrace) changing the math baseline +- Fixed merging of attachments when the base is a nested equation +- Fixed resolving of contextual (em-based) text sizes within math +- Fixed spacing around ⊥ + +## Bibliography +- Prose and author-only citations now use editor names if the author names are + unavailable +- Some non-standard but widely used BibLaTeX `editortype`s like `producer`, + `writer`, `scriptwriter`, and `none` (defined by widespread style + `biblatex-chicago` to mean performers within `music` and `video` entries) are + now recognized +- CSL styles can now render affixes around the bibliography +- For BibTeX entries with `eprinttype = {pubmed}`, the PubMed ID will now be + correctly processed +- Whitespace handling for strings delimiting initialized names has been improved +- Uppercase spelling after apostrophes used as quotation marks is now possible +- Fixed bugs around the handling of CSL delimiting characters +- Fixed a problem with parsing multibyte characters in page ranges that could + prevent Hayagriva from parsing some BibTeX page ranges +- Updated CSL APA style +- Updated CSL locales for Finnish, Swiss German, Austrian German, German, and + Arabic + +## Text +- Added support for specifying which charset should be [covered]($text.font) by + which font family +- Added [`all`]($smallcaps.all) parameter to `smallcaps` function that also + enables small capitals on uppercase letters +- Added basic i18n for Basque and Bulgarian +- [Justification]($par.justify) does not affect [raw] blocks anymore +- [CJK-Latin-spacing]($text.cjk-latin-spacing) does not affect [raw] text + anymore +- Fixed wrong language codes being used for Greek and Ukrainian +- Fixed default quotes for Croatian +- Fixed crash in RTL text handling +- Added support for [`raw`] syntax highlighting for a few new languages: CFML, + NSIS, and WGSL +- New font metadata exception for New Computer Modern Sans Math +- Updated bundled New Computer Modern fonts to version 7.0 + +## Layout +- Fixed various bugs with footnotes + - Fixed footnotes getting lost when multiple footnotes were nested within + another footnote + - Fixed endless loops with empty and overlarge footnotes + - Fixed crash with overlarge footnotes within a floating placement +- Fixed sizing of quadratic shapes ([`square`] and [`circle`]) +- Fixed [`block.sticky`] not working properly at the top of a container +- Fixed crash due to consecutive weak spacing +- Fixed crash when a [block] or text have negative sizes +- Fixed unnecessary hyphenations occurring in rare scenarios due to a bad + interaction between padding and paragraph optimization +- Fixed lone [citations]($cite) in [`align`] not becoming their own paragraph + +## Syntax +- Top-level closing square brackets that do not have a matching opening square + bracket are now a hard error **(Minor breaking change)** +- Adding a space between the identifier and the parentheses in a set rule is not + allowed anymore **(Minor breaking change)** +- Numbers with a unit cannot have a base prefix anymore, e.g. `0b100000pt` is + not allowed anymore. Previously, it was syntactically allowed but always + resolved to a value of zero. **(Minor breaking change)** +- Using `is` as an identifier will now warn as it might become a keyword in the + future +- Fixed minor whitespace handling bugs + - in math mode argument lists + - at the end of headings + - between a term list's term and description +- Fixed parsing of empty single line raw blocks with 3+ backticks and a language + tag +- Fixed minor bug with parentheses parsing in math +- Markup that can only appear at the start of the line (headings, lists) can now + also appear at the start of a list item +- A shebang `#!` at the very start of a file is now ignored + +## PDF export +- Added `pdf.embed` function for embedding arbitrary files in the exported PDF +- Added support for PDF/A-3b export +- The PDF timestamp will now contain the timezone by default + +## HTML export +**Note:** HTML export is currently under active development. The feature is +still _very_ incomplete, but already available for experimentation behind a +feature flag. + +- Added HTML output support for some (but not all) of the built-in elements +- Added [`html.elem`] function for outputting an arbitrary HTML element +- Added [`html.frame`] function for integrating content that requires layout + into HTML (by embedding an SVG) +- Added [`target`] function which returns either `{"paged"}` or `{"html"}` + depending on the export target + +## Tooling and Diagnostics +- Autocompletion improvements + - Added autocompletion for file paths + - Smarter autocompletion of variables: Completing `{rect(fill: |)}` will now + only show variables which contain a valid fill (either directly or nested, + e.g. a dictionary containing a valid fill) + - Different functions will now autocomplete with different brackets (round vs + square) depending on which kind is more useful + - Positional parameters which are already provided aren't autocompleted again + anymore + - Fixed variable autocompletion not considering parameters + - Added autocompletion snippets for common figure usages + - Fixed autocompletion after half-completed import item + - Fixed autocompletion for `cite` function +- Added warning when an unconditional return in a code block discards joined + content +- Fixed error message when accessing non-existent label +- Fixed handling of nested imports in IDE functionality + +## Command Line Interface +- Added `--features` argument and `TYPST_FEATURES` environment variable for + opting into experimental features. The only feature so far is `html`. +- Added a live reloading HTTP server to `typst watch` when targeting HTML +- Fixed self-update not being aware about certain target architectures +- Fixed crash when piping `typst fonts` output to another command + +## Symbols +- New + - `inter`, `inter.and`, `inter.big`, `inter.dot`, `inter.double`, `inter.sq`, + `inter.sq.big`, `inter.sq.double`, `integral.inter` + - `asymp`, `asymp.not` + - `mapsto`, `mapsto.long` + - `divides.not.rev`, `divides.struck` + - `interleave`, `interleave.big`, `interleave.struck` + - `eq.triple.not`, `eq.dots`, `eq.dots.down`, `eq.dots.up` + - `smt`, `smt.eq`, `lat`, `lat.eq` + - `colon.tri`, `colon.tri.op` + - `dagger.triple`, `dagger.l`, `dagger.r`, `dagger.inv` + - `hourglass.stroked`, `hourglass.filled` + - `die.six`, `die.five`, `die.four`, `die.three`, `die.two`, `die.one` + - `errorbar.square.stroked`, `errorbar.square.filled`, + `errorbar.diamond.stroked`, `errorbar.diamond.filled`, + `errorbar.circle.stroked`, `errorbar.circle.filled` + - `numero` + - `Omega.inv` +- Renamed + - `ohm.inv` to `Omega.inv` +- Changed codepoint + - `angle.l.double` from `《` to `⟪` + - `angle.r.double` from `》` to `⟫` + - `angstrom` from U+212B (`Å`) to U+00C5 (`Å`) +- Deprecated + - `sect` and all its variants in favor of `inter` + - `integral.sect` in favor of `integral.inter` +- Removed + - `degree.c` in favor of `°C` (`[$upright(°C)$]` or `[$upright(degree C)$]` in math) + - `degree.f` in favor of `°F` (`[$upright(°F)$]` or `[$upright(degree F)$]` in math) + - `kelvin` in favor of just K (`[$upright(K)$]` in math) + +## Deprecations +- The [`path`] function in favor of the [`curve`] function +- The name `pattern` for tiling patterns in favor of the new name [`tiling`] +- [`image.decode`], [`cbor.decode`], [`csv.decode`], [`json.decode`], + [`toml.decode`], [`xml.decode`], [`yaml.decode`] in favor of the top-level + functions directly accepting both paths and bytes +- The `sect` and its variants in favor of `inter`, and `integral.sect` in favor + of `integral.inter` +- Fully removed type/str compatibility behavior (e.g. `{int == "integer"}`) + which was temporarily introduced in Typst 0.8 **(Breaking change)** + +## Development +- The `typst::compile` function is now generic and can return either a + `PagedDocument` or an `HtmlDocument` +- `typst-timing` now supports WebAssembly targets via `web-sys` when the `wasm` + feature is enabled +- Increased minimum supported Rust version to 1.80 +- Fixed linux/arm64 Docker image diff --git a/docs/changelog/welcome.md b/docs/changelog/welcome.md index 12b6b896b..bb245eb01 100644 --- a/docs/changelog/welcome.md +++ b/docs/changelog/welcome.md @@ -10,6 +10,7 @@ forward. This section documents all changes to Typst since its initial public release. ## Versions +- [Unreleased changes planned for Typst 0.13.0]($changelog/0.13.0) - [Typst 0.12.0]($changelog/0.12.0) - [Typst 0.11.1]($changelog/0.11.1) - [Typst 0.11.0]($changelog/0.11.0) diff --git a/docs/src/lib.rs b/docs/src/lib.rs index f9ee05bbd..fae74e0fc 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -188,6 +188,7 @@ fn changelog_pages(resolver: &dyn Resolver) -> PageModel { let mut page = md_page(resolver, resolver.base(), load!("changelog/welcome.md")); let base = format!("{}changelog/", resolver.base()); page.children = vec![ + md_page(resolver, &base, load!("changelog/0.13.0.md")), md_page(resolver, &base, load!("changelog/0.12.0.md")), md_page(resolver, &base, load!("changelog/0.11.1.md")), md_page(resolver, &base, load!("changelog/0.11.0.md")), From d897ab5e7d2e941494df8ba137a1f92f8aada03a Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 6 Feb 2025 10:34:28 +0100 Subject: [PATCH 013/172] Autocomplete content methods (#5822) --- crates/typst-ide/src/complete.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index c1f08cf09..7df788dc3 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -398,7 +398,17 @@ fn field_access_completions( value: &Value, styles: &Option, ) { - for (name, binding) in value.ty().scope().iter() { + let scopes = { + let ty = value.ty().scope(); + let elem = match value { + Value::Content(content) => Some(content.elem().scope()), + _ => None, + }; + elem.into_iter().chain(Some(ty)) + }; + + // Autocomplete methods from the element's or type's scope. + for (name, binding) in scopes.flat_map(|scope| scope.iter()) { ctx.call_completion(name.clone(), binding.read()); } @@ -1747,4 +1757,15 @@ mod tests { .must_include(["this", "that"]) .must_exclude(["*", "figure"]); } + + #[test] + fn test_autocomplete_type_methods() { + test("#\"hello\".", -1).must_include(["len", "contains"]); + } + + #[test] + fn test_autocomplete_content_methods() { + test("#show outline.entry: it => it.\n#outline()\n= Hi", 30) + .must_include(["indented", "body", "page"]); + } } From ca702c7f82ef8e027e559228dc9c469e1a65ac6f Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Thu, 6 Feb 2025 11:18:10 +0100 Subject: [PATCH 014/172] Documentation fixes and improvements (#5816) --- crates/typst-layout/src/shapes.rs | 4 ++-- crates/typst-library/src/foundations/plugin.rs | 4 +--- crates/typst-library/src/loading/cbor.rs | 4 +--- crates/typst-library/src/loading/csv.rs | 4 +--- crates/typst-library/src/loading/json.rs | 4 +--- crates/typst-library/src/loading/toml.rs | 4 +--- crates/typst-library/src/loading/xml.rs | 4 +--- crates/typst-library/src/loading/yaml.rs | 4 +--- crates/typst-library/src/model/outline.rs | 2 +- crates/typst-library/src/pdf/embed.rs | 4 +--- crates/typst-library/src/visualize/curve.rs | 18 +++++++++--------- .../typst-library/src/visualize/image/mod.rs | 7 ++++--- crates/typst-library/src/visualize/path.rs | 6 +++--- crates/typst-library/src/visualize/shape.rs | 2 +- docs/changelog/0.13.0.md | 6 +++--- docs/reference/export/png.md | 2 +- 16 files changed, 32 insertions(+), 47 deletions(-) diff --git a/crates/typst-layout/src/shapes.rs b/crates/typst-layout/src/shapes.rs index eb665f06a..21d0a518f 100644 --- a/crates/typst-layout/src/shapes.rs +++ b/crates/typst-layout/src/shapes.rs @@ -1281,7 +1281,7 @@ impl ControlPoints { } } -/// Helper to draw arcs with bezier curves. +/// Helper to draw arcs with Bézier curves. trait CurveExt { fn arc(&mut self, start: Point, center: Point, end: Point); fn arc_move(&mut self, start: Point, center: Point, end: Point); @@ -1305,7 +1305,7 @@ impl CurveExt for Curve { } } -/// Get the control points for a bezier curve that approximates a circular arc for +/// Get the control points for a Bézier curve that approximates a circular arc for /// a start point, an end point and a center of the circle whose arc connects /// the two. fn bezier_arc_control(start: Point, center: Point, end: Point) -> [Point; 2] { diff --git a/crates/typst-library/src/foundations/plugin.rs b/crates/typst-library/src/foundations/plugin.rs index a33f1cb91..31f8cd732 100644 --- a/crates/typst-library/src/foundations/plugin.rs +++ b/crates/typst-library/src/foundations/plugin.rs @@ -148,9 +148,7 @@ use crate::loading::{DataSource, Load}; #[func(scope)] pub fn plugin( engine: &mut Engine, - /// A path to a WebAssembly file or raw WebAssembly bytes. - /// - /// For more details about paths, see the [Paths section]($syntax/#paths). + /// A [path]($syntax/#paths) to a WebAssembly file or raw WebAssembly bytes. source: Spanned, ) -> SourceResult { let data = source.load(engine.world)?; diff --git a/crates/typst-library/src/loading/cbor.rs b/crates/typst-library/src/loading/cbor.rs index 801ca617a..aa14c5c77 100644 --- a/crates/typst-library/src/loading/cbor.rs +++ b/crates/typst-library/src/loading/cbor.rs @@ -20,9 +20,7 @@ use crate::loading::{DataSource, Load}; #[func(scope, title = "CBOR")] pub fn cbor( engine: &mut Engine, - /// A path to a CBOR file or raw CBOR bytes. - /// - /// For more details about paths, see the [Paths section]($syntax/#paths). + /// A [path]($syntax/#paths) to a CBOR file or raw CBOR bytes. source: Spanned, ) -> SourceResult { let data = source.load(engine.world)?; diff --git a/crates/typst-library/src/loading/csv.rs b/crates/typst-library/src/loading/csv.rs index 6fdec4459..6afb5baeb 100644 --- a/crates/typst-library/src/loading/csv.rs +++ b/crates/typst-library/src/loading/csv.rs @@ -26,9 +26,7 @@ use crate::loading::{DataSource, Load, Readable}; #[func(scope, title = "CSV")] pub fn csv( engine: &mut Engine, - /// Path to a CSV file or raw CSV bytes. - /// - /// For more details about paths, see the [Paths section]($syntax/#paths). + /// A [path]($syntax/#paths) to a CSV file or raw CSV bytes. source: Spanned, /// The delimiter that separates columns in the CSV file. /// Must be a single ASCII character. diff --git a/crates/typst-library/src/loading/json.rs b/crates/typst-library/src/loading/json.rs index 185bac143..aa908cca4 100644 --- a/crates/typst-library/src/loading/json.rs +++ b/crates/typst-library/src/loading/json.rs @@ -51,9 +51,7 @@ use crate::loading::{DataSource, Load, Readable}; #[func(scope, title = "JSON")] pub fn json( engine: &mut Engine, - /// Path to a JSON file or raw JSON bytes. - /// - /// For more details about paths, see the [Paths section]($syntax/#paths). + /// A [path]($syntax/#paths) to a JSON file or raw JSON bytes. source: Spanned, ) -> SourceResult { let data = source.load(engine.world)?; diff --git a/crates/typst-library/src/loading/toml.rs b/crates/typst-library/src/loading/toml.rs index 2660e7e7f..f04b2e746 100644 --- a/crates/typst-library/src/loading/toml.rs +++ b/crates/typst-library/src/loading/toml.rs @@ -29,9 +29,7 @@ use crate::loading::{DataSource, Load, Readable}; #[func(scope, title = "TOML")] pub fn toml( engine: &mut Engine, - /// A path to a TOML file or raw TOML bytes. - /// - /// For more details about paths, see the [Paths section]($syntax/#paths). + /// A [path]($syntax/#paths) to a TOML file or raw TOML bytes. source: Spanned, ) -> SourceResult { let data = source.load(engine.world)?; diff --git a/crates/typst-library/src/loading/xml.rs b/crates/typst-library/src/loading/xml.rs index 32ed6f24b..daccd02fc 100644 --- a/crates/typst-library/src/loading/xml.rs +++ b/crates/typst-library/src/loading/xml.rs @@ -58,9 +58,7 @@ use crate::loading::{DataSource, Load, Readable}; #[func(scope, title = "XML")] pub fn xml( engine: &mut Engine, - /// A path to an XML file or raw XML bytes. - /// - /// For more details about paths, see the [Paths section]($syntax/#paths). + /// A [path]($syntax/#paths) to an XML file or raw XML bytes. source: Spanned, ) -> SourceResult { let data = source.load(engine.world)?; diff --git a/crates/typst-library/src/loading/yaml.rs b/crates/typst-library/src/loading/yaml.rs index 4eeec28f1..3f48113e8 100644 --- a/crates/typst-library/src/loading/yaml.rs +++ b/crates/typst-library/src/loading/yaml.rs @@ -41,9 +41,7 @@ use crate::loading::{DataSource, Load, Readable}; #[func(scope, title = "YAML")] pub fn yaml( engine: &mut Engine, - /// A path to a YAML file or raw YAML bytes. - /// - /// For more details about paths, see the [Paths section]($syntax/#paths). + /// A [path]($syntax/#paths) to a YAML file or raw YAML bytes. source: Spanned, ) -> SourceResult { let data = source.load(engine.world)?; diff --git a/crates/typst-library/src/model/outline.rs b/crates/typst-library/src/model/outline.rs index f413189ba..7ceb530f8 100644 --- a/crates/typst-library/src/model/outline.rs +++ b/crates/typst-library/src/model/outline.rs @@ -623,7 +623,7 @@ impl OutlineEntry { /// The content which is displayed in place of the referred element at its /// entry in the outline. For a heading, this is its - /// [`body`]($heading.body), for a figure a caption, and for equations it is + /// [`body`]($heading.body); for a figure a caption and for equations, it is /// empty. #[func] pub fn body(&self) -> StrResult { diff --git a/crates/typst-library/src/pdf/embed.rs b/crates/typst-library/src/pdf/embed.rs index f9ca3ca09..001078e5e 100644 --- a/crates/typst-library/src/pdf/embed.rs +++ b/crates/typst-library/src/pdf/embed.rs @@ -32,12 +32,10 @@ use crate::World; /// embedded file conforms to PDF/A-1 or PDF/A-2. #[elem(Show, Locatable)] pub struct EmbedElem { - /// Path of the file to be embedded. + /// The [path]($syntax/#paths) of the file to be embedded. /// /// Must always be specified, but is only read from if no data is provided /// in the following argument. - /// - /// For more details about paths, see the [Paths section]($syntax/#paths). #[required] #[parse( let Spanned { v: path, span } = diff --git a/crates/typst-library/src/visualize/curve.rs b/crates/typst-library/src/visualize/curve.rs index 607d92ab1..fb5151e8f 100644 --- a/crates/typst-library/src/visualize/curve.rs +++ b/crates/typst-library/src/visualize/curve.rs @@ -10,12 +10,12 @@ use crate::foundations::{ use crate::layout::{Abs, Axes, BlockElem, Length, Point, Rel, Size}; use crate::visualize::{FillRule, Paint, Stroke}; -/// A curve consisting of movements, lines, and Beziér segments. +/// A curve consisting of movements, lines, and Bézier segments. /// /// At any point in time, there is a conceptual pen or cursor. /// - Move elements move the cursor without drawing. /// - Line/Quadratic/Cubic elements draw a segment from the cursor to a new -/// position, potentially with control point for a Beziér curve. +/// position, potentially with control point for a Bézier curve. /// - Close elements draw a straight or smooth line back to the start of the /// curve or the latest preceding move segment. /// @@ -26,7 +26,7 @@ use crate::visualize::{FillRule, Paint, Stroke}; /// or relative to the current pen/cursor position, that is, the position where /// the previous segment ended. /// -/// Beziér curve control points can be skipped by passing `{none}` or +/// Bézier curve control points can be skipped by passing `{none}` or /// automatically mirrored from the preceding segment by passing `{auto}`. /// /// # Example @@ -88,7 +88,7 @@ pub struct CurveElem { #[fold] pub stroke: Smart>, - /// The components of the curve, in the form of moves, line and Beziér + /// The components of the curve, in the form of moves, line and Bézier /// segment, and closes. #[variadic] pub components: Vec, @@ -225,7 +225,7 @@ pub struct CurveLine { pub relative: bool, } -/// Adds a quadratic Beziér curve segment from the last point to `end`, using +/// Adds a quadratic Bézier curve segment from the last point to `end`, using /// `control` as the control point. /// /// ```example @@ -245,9 +245,9 @@ pub struct CurveLine { /// ``` #[elem(name = "quad", title = "Curve Quadratic Segment")] pub struct CurveQuad { - /// The control point of the quadratic Beziér curve. + /// The control point of the quadratic Bézier curve. /// - /// - If `{auto}` and this segment follows another quadratic Beziér curve, + /// - If `{auto}` and this segment follows another quadratic Bézier curve, /// the previous control point will be mirrored. /// - If `{none}`, the control point defaults to `end`, and the curve will /// be a straight line. @@ -272,7 +272,7 @@ pub struct CurveQuad { pub relative: bool, } -/// Adds a cubic Beziér curve segment from the last point to `end`, using +/// Adds a cubic Bézier curve segment from the last point to `end`, using /// `control-start` and `control-end` as the control points. /// /// ```example @@ -388,7 +388,7 @@ pub enum CloseMode { Straight, } -/// A curve consisting of movements, lines, and Beziér segments. +/// A curve consisting of movements, lines, and Bézier segments. #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] pub struct Curve(pub Vec); diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 18d40caa8..97189e22d 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -46,10 +46,11 @@ use crate::text::LocalName; /// ``` #[elem(scope, Show, LocalName, Figurable)] pub struct ImageElem { - /// A path to an image file or raw bytes making up an image in one of the - /// supported [formats]($image.format). + /// A [path]($syntax/#paths) to an image file or raw bytes making up an + /// image in one of the supported [formats]($image.format). /// - /// For more details about paths, see the [Paths section]($syntax/#paths). + /// Bytes can be used to specify raw pixel data in a row-major, + /// left-to-right, top-to-bottom format. /// /// ```example /// #let original = read("diagram.svg") diff --git a/crates/typst-library/src/visualize/path.rs b/crates/typst-library/src/visualize/path.rs index c1cfde94a..968146cda 100644 --- a/crates/typst-library/src/visualize/path.rs +++ b/crates/typst-library/src/visualize/path.rs @@ -8,7 +8,7 @@ use crate::foundations::{ use crate::layout::{Axes, BlockElem, Length, Rel}; use crate::visualize::{FillRule, Paint, Stroke}; -/// A path through a list of points, connected by Bezier curves. +/// A path through a list of points, connected by Bézier curves. /// /// # Example /// ```example @@ -59,8 +59,8 @@ pub struct PathElem { #[fold] pub stroke: Smart>, - /// Whether to close this path with one last bezier curve. This curve will - /// takes into account the adjacent control points. If you want to close + /// Whether to close this path with one last Bézier curve. This curve will + /// take into account the adjacent control points. If you want to close /// with a straight line, simply add one last point that's the same as the /// start point. #[default(false)] diff --git a/crates/typst-library/src/visualize/shape.rs b/crates/typst-library/src/visualize/shape.rs index 3c62b210f..439b4cd98 100644 --- a/crates/typst-library/src/visualize/shape.rs +++ b/crates/typst-library/src/visualize/shape.rs @@ -412,7 +412,7 @@ pub enum Geometry { Line(Point), /// A rectangle with its origin in the topleft corner. Rect(Size), - /// A curve consisting of movements, lines, and Bezier segments. + /// A curve consisting of movements, lines, and Bézier segments. Curve(Curve), } diff --git a/docs/changelog/0.13.0.md b/docs/changelog/0.13.0.md index 50819f659..4e4dd0c2d 100644 --- a/docs/changelog/0.13.0.md +++ b/docs/changelog/0.13.0.md @@ -16,7 +16,7 @@ description: Changes slated to appear in Typst 0.13.0 - The `image` function now supports raw [pixel raster formats]($image.format) for generating images from within Typst - Functions that accept [file paths]($syntax/#paths) now also accept raw - [bytes] instead, for full flexibility + [bytes], for full flexibility - WebAssembly [plugins]($plugin) are more flexible and automatically run multi-threaded - Fixed a long-standing bug where single-letter strings in math (`[$"a"$]`) @@ -155,7 +155,7 @@ description: Changes slated to appear in Typst 0.13.0 - Fixed multi-line annotations (e.g. overbrace) changing the math baseline - Fixed merging of attachments when the base is a nested equation - Fixed resolving of contextual (em-based) text sizes within math -- Fixed spacing around ⊥ +- Fixed spacing around up tacks (⊥) ## Bibliography - Prose and author-only citations now use editor names if the author names are @@ -229,7 +229,7 @@ description: Changes slated to appear in Typst 0.13.0 - A shebang `#!` at the very start of a file is now ignored ## PDF export -- Added `pdf.embed` function for embedding arbitrary files in the exported PDF +- Added [`pdf.embed`] function for embedding arbitrary files in the exported PDF - Added support for PDF/A-3b export - The PDF timestamp will now contain the timezone by default diff --git a/docs/reference/export/png.md b/docs/reference/export/png.md index fe122f4d3..0e817e0f1 100644 --- a/docs/reference/export/png.md +++ b/docs/reference/export/png.md @@ -11,7 +11,7 @@ the PNG you exported, you will notice a loss of quality. Typst calculates the resolution of your PNGs based on each page's physical dimensions and the PPI. If you need guidance for choosing a PPI value, consider the following: -- A DPI value of 300 or 600 is typical for desktop printing. +- A value of 300 or 600 is typical for desktop printing. - Professional prints of detailed graphics can go up to 1200 PPI. - If your document is only viewed at a distance, e.g. a poster, you may choose a smaller value than 300. From d61f57365b931e7cd57ed0a88b21c79f3042e3f5 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 6 Feb 2025 11:18:35 +0100 Subject: [PATCH 015/172] Fix docs outline for nested definitions (#5823) --- docs/src/lib.rs | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/src/lib.rs b/docs/src/lib.rs index fae74e0fc..e9771738d 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -550,8 +550,6 @@ fn func_outline(model: &FuncModel, id_base: &str) -> Vec { .collect(), }); } - - outline.extend(scope_outline(&model.scope)); } else { outline.extend(model.params.iter().map(|param| OutlineItem { id: eco_format!("{id_base}-{}", urlify(param.name)), @@ -560,27 +558,30 @@ fn func_outline(model: &FuncModel, id_base: &str) -> Vec { })); } + outline.extend(scope_outline(&model.scope, id_base)); + outline } /// Produce an outline for a function scope. -fn scope_outline(scope: &[FuncModel]) -> Option { +fn scope_outline(scope: &[FuncModel], id_base: &str) -> Option { if scope.is_empty() { return None; } - Some(OutlineItem { - id: "definitions".into(), - name: "Definitions".into(), - children: scope - .iter() - .map(|func| { - let id = urlify(&eco_format!("definitions-{}", func.name)); - let children = func_outline(func, &id); - OutlineItem { id, name: func.title.into(), children } - }) - .collect(), - }) + let dash = if id_base.is_empty() { "" } else { "-" }; + let id = eco_format!("{id_base}{dash}definitions"); + + let children = scope + .iter() + .map(|func| { + let id = urlify(&eco_format!("{id}-{}", func.name)); + let children = func_outline(func, &id); + OutlineItem { id, name: func.title.into(), children } + }) + .collect(); + + Some(OutlineItem { id, name: "Definitions".into(), children }) } /// Create a page for a group of functions. @@ -687,7 +688,7 @@ fn type_outline(model: &TypeModel) -> Vec { }); } - outline.extend(scope_outline(&model.scope)); + outline.extend(scope_outline(&model.scope, "")); outline } From a1c73b41b862eb95f609f18ee99bdb6da039f478 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 6 Feb 2025 21:57:46 +0100 Subject: [PATCH 016/172] Document removals in changelog (#5827) --- docs/changelog/0.13.0.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/changelog/0.13.0.md b/docs/changelog/0.13.0.md index 4e4dd0c2d..2caace723 100644 --- a/docs/changelog/0.13.0.md +++ b/docs/changelog/0.13.0.md @@ -45,6 +45,7 @@ description: Changes slated to appear in Typst 0.13.0 result in a warning - The default show rules of various built-in elements like lists, quotes, etc. were adjusted to ensure they produce/don't produce paragraphs as appropriate + - Removed support for booleans and content in [`outline.indent`] - The [`outline`] function was fully reworked to improve its out-of-the-box behavior **(Breaking change)** - [Outline entries]($outline.entry) are now [blocks]($block) and are thus @@ -312,8 +313,20 @@ feature flag. functions directly accepting both paths and bytes - The `sect` and its variants in favor of `inter`, and `integral.sect` in favor of `integral.inter` -- Fully removed type/str compatibility behavior (e.g. `{int == "integer"}`) - which was temporarily introduced in Typst 0.8 **(Breaking change)** + +## Removals +- Removed `style` function and `styles` argument of [`measure`], use a [context] + expression instead **(Breaking change)** +- Removed `state.display` function, use [`state.get`] instead + **(Breaking change)** +- Removed `location` argument of [`state.at`], [`counter.at`], and [`query`] + **(Breaking change)** +- Removed compatibility behavior where [`counter.display`] worked without + [context] **(Breaking change)** +- Removed compatibility behavior of [`locate`] **(Breaking change)** +- Removed compatibility behavior of type/str comparisons + (e.g. `{int == "integer"}`) which was temporarily introduced in Typst 0.8 + **(Breaking change)** ## Development - The `typst::compile` function is now generic and can return either a From e4f8e57c534db8a31d51e0342c46b913a7e22422 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 6 Feb 2025 22:10:43 +0100 Subject: [PATCH 017/172] Fix unnecessary import rename warning (#5828) --- crates/typst-eval/src/import.rs | 6 +++--- tests/suite/scripting/import.typ | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/typst-eval/src/import.rs b/crates/typst-eval/src/import.rs index 27b06af41..1b1641487 100644 --- a/crates/typst-eval/src/import.rs +++ b/crates/typst-eval/src/import.rs @@ -44,11 +44,10 @@ impl Eval for ast::ModuleImport<'_> { } // If there is a rename, import the source itself under that name. - let bare_name = self.bare_name(); let new_name = self.new_name(); if let Some(new_name) = new_name { - if let Ok(source_name) = &bare_name { - if source_name == new_name.as_str() { + if let ast::Expr::Ident(ident) = self.source() { + if ident.as_str() == new_name.as_str() { // Warn on `import x as x` vm.engine.sink.warn(warning!( new_name.span(), @@ -57,6 +56,7 @@ impl Eval for ast::ModuleImport<'_> { } } + // Define renamed module on the scope. vm.define(new_name, source.clone()); } diff --git a/tests/suite/scripting/import.typ b/tests/suite/scripting/import.typ index 03e2efc6b..49b66ee56 100644 --- a/tests/suite/scripting/import.typ +++ b/tests/suite/scripting/import.typ @@ -255,6 +255,10 @@ // Warning: 17-21 unnecessary import rename to same name #import enum as enum +--- import-rename-necessary --- +#import "module.typ" as module: a +#test(module.a, a) + --- import-rename-unnecessary-mixed --- // Warning: 17-21 unnecessary import rename to same name #import enum as enum: item @@ -263,10 +267,6 @@ // Warning: 31-35 unnecessary import rename to same name #import enum as enum: item as item ---- import-item-rename-unnecessary-string --- -// Warning: 25-31 unnecessary import rename to same name -#import "module.typ" as module - --- import-item-rename-unnecessary-but-ok --- #import "modul" + "e.typ" as module #test(module.b, 1) From 3fba256405c4aae9f121a07ddaa29cc10b825fc9 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Mon, 10 Feb 2025 07:39:04 -0300 Subject: [PATCH 018/172] Don't crash on image with zero DPI (#5835) --- crates/typst-layout/src/image.rs | 2 ++ crates/typst-library/src/visualize/image/raster.rs | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index d963ea50d..3e5b7d8bd 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -95,6 +95,8 @@ pub fn layout_image( } else { // If neither is forced, take the natural image size at the image's // DPI bounded by the available space. + // + // Division by DPI is fine since it's guaranteed to be positive. let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI); let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi)); Size::new( diff --git a/crates/typst-library/src/visualize/image/raster.rs b/crates/typst-library/src/visualize/image/raster.rs index d43b15486..0883fe71d 100644 --- a/crates/typst-library/src/visualize/image/raster.rs +++ b/crates/typst-library/src/visualize/image/raster.rs @@ -160,6 +160,8 @@ impl RasterImage { } /// The image's pixel density in pixels per inch, if known. + /// + /// This is guaranteed to be positive. pub fn dpi(&self) -> Option { self.0.dpi } @@ -334,6 +336,9 @@ fn apply_rotation(image: &mut DynamicImage, rotation: u32) { } /// Try to determine the DPI (dots per inch) of the image. +/// +/// This is guaranteed to be a positive value, or `None` if invalid or +/// unspecified. fn determine_dpi(data: &[u8], exif: Option<&exif::Exif>) -> Option { // Try to extract the DPI from the EXIF metadata. If that doesn't yield // anything, fall back to specialized procedures for extracting JPEG or PNG @@ -341,6 +346,7 @@ fn determine_dpi(data: &[u8], exif: Option<&exif::Exif>) -> Option { exif.and_then(exif_dpi) .or_else(|| jpeg_dpi(data)) .or_else(|| png_dpi(data)) + .filter(|&dpi| dpi > 0.0) } /// Try to get the DPI from the EXIF metadata. From 25e27169e1413c9e14184267be57fdbbb09e7c34 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Mon, 10 Feb 2025 11:39:32 +0100 Subject: [PATCH 019/172] Add warning for `pdf.embed` elem used with HTML (#5829) --- crates/typst-library/src/pdf/embed.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/pdf/embed.rs b/crates/typst-library/src/pdf/embed.rs index 001078e5e..f902e7f14 100644 --- a/crates/typst-library/src/pdf/embed.rs +++ b/crates/typst-library/src/pdf/embed.rs @@ -1,9 +1,12 @@ use ecow::EcoString; +use typst_library::foundations::Target; use typst_syntax::Spanned; -use crate::diag::{At, SourceResult}; +use crate::diag::{warning, At, SourceResult}; use crate::engine::Engine; -use crate::foundations::{elem, Bytes, Cast, Content, Derived, Packed, Show, StyleChain}; +use crate::foundations::{ + elem, Bytes, Cast, Content, Derived, Packed, Show, StyleChain, TargetElem, +}; use crate::introspection::Locatable; use crate::World; @@ -78,7 +81,12 @@ pub struct EmbedElem { } impl Show for Packed { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + if TargetElem::target_in(styles) == Target::Html { + engine + .sink + .warn(warning!(self.span(), "embed was ignored during HTML export")); + } Ok(Content::empty()) } } From ee47cb846924235be6eae968a7853ea7860ccc51 Mon Sep 17 00:00:00 2001 From: TwoF1nger <140991913+TwoF1nger@users.noreply.github.com> Date: Mon, 10 Feb 2025 10:42:16 +0000 Subject: [PATCH 020/172] Add smart quotes for Bulgarian (#5807) --- crates/typst-library/src/text/smartquote.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/typst-library/src/text/smartquote.rs b/crates/typst-library/src/text/smartquote.rs index 2f89fe298..f457a6371 100644 --- a/crates/typst-library/src/text/smartquote.rs +++ b/crates/typst-library/src/text/smartquote.rs @@ -251,6 +251,7 @@ impl<'s> SmartQuotes<'s> { "el" => ("‘", "’", "«", "»"), "he" => ("’", "’", "”", "”"), "hr" => ("‘", "’", "„", "”"), + "bg" => ("’", "’", "„", "“"), _ if lang.dir() == Dir::RTL => ("’", "‘", "”", "“"), _ => default, }; From 89e71acecd4a3a06943d0bd4443fc80a9b8f41e4 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 10 Feb 2025 15:37:19 +0100 Subject: [PATCH 021/172] Respect `par` constructor arguments (#5842) --- crates/typst-layout/src/flow/collect.rs | 13 +- crates/typst-layout/src/flow/mod.rs | 77 ++++--- crates/typst-layout/src/inline/collect.rs | 54 +---- crates/typst-layout/src/inline/finalize.rs | 10 +- crates/typst-layout/src/inline/line.rs | 51 +++-- crates/typst-layout/src/inline/linebreak.rs | 44 ++-- crates/typst-layout/src/inline/mod.rs | 191 +++++++++++++++++- crates/typst-layout/src/inline/prepare.rs | 73 +------ crates/typst-layout/src/math/text.rs | 1 - crates/typst-library/src/model/link.rs | 4 +- crates/typst-library/src/text/mod.rs | 25 +-- crates/typst-library/src/text/raw.rs | 6 +- tests/ref/issue-5831-par-constructor-args.png | Bin 0 -> 1356 bytes tests/suite/model/par.typ | 14 ++ 14 files changed, 314 insertions(+), 249 deletions(-) create mode 100644 tests/ref/issue-5831-par-constructor-args.png diff --git a/crates/typst-layout/src/flow/collect.rs b/crates/typst-layout/src/flow/collect.rs index 34362a6c5..2c14f7a37 100644 --- a/crates/typst-layout/src/flow/collect.rs +++ b/crates/typst-layout/src/flow/collect.rs @@ -124,7 +124,6 @@ impl<'a> Collector<'a, '_, '_> { styles, self.base, self.expand, - None, )? .into_frames(); @@ -133,7 +132,8 @@ impl<'a> Collector<'a, '_, '_> { self.output.push(Child::Tag(&elem.tag)); } - self.lines(lines, styles); + let leading = ParElem::leading_in(styles); + self.lines(lines, leading, styles); for (c, _) in &self.children[end..] { let elem = c.to_packed::().unwrap(); @@ -169,10 +169,12 @@ impl<'a> Collector<'a, '_, '_> { )? .into_frames(); - let spacing = ParElem::spacing_in(styles); + let spacing = elem.spacing(styles); + let leading = elem.leading(styles); + self.output.push(Child::Rel(spacing.into(), 4)); - self.lines(lines, styles); + self.lines(lines, leading, styles); self.output.push(Child::Rel(spacing.into(), 4)); self.par_situation = ParSituation::Consecutive; @@ -181,9 +183,8 @@ impl<'a> Collector<'a, '_, '_> { } /// Collect laid-out lines. - fn lines(&mut self, lines: Vec, styles: StyleChain<'a>) { + fn lines(&mut self, lines: Vec, leading: Abs, styles: StyleChain<'a>) { let align = AlignElem::alignment_in(styles).resolve(styles); - let leading = ParElem::leading_in(styles); let costs = TextElem::costs_in(styles); // Determine whether to prevent widow and orphans. diff --git a/crates/typst-layout/src/flow/mod.rs b/crates/typst-layout/src/flow/mod.rs index 2acbbcef3..cba228bcd 100644 --- a/crates/typst-layout/src/flow/mod.rs +++ b/crates/typst-layout/src/flow/mod.rs @@ -197,7 +197,50 @@ pub fn layout_flow<'a>( mode: FlowMode, ) -> SourceResult { // Prepare configuration that is shared across the whole flow. - let config = Config { + let config = configuration(shared, regions, columns, column_gutter, mode); + + // Collect the elements into pre-processed children. These are much easier + // to handle than the raw elements. + let bump = Bump::new(); + let children = collect( + engine, + &bump, + children, + locator.next(&()), + Size::new(config.columns.width, regions.full), + regions.expand.x, + mode, + )?; + + let mut work = Work::new(&children); + let mut finished = vec![]; + + // This loop runs once per region produced by the flow layout. + loop { + let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?; + finished.push(frame); + + // Terminate the loop when everything is processed, though draining the + // backlog if necessary. + if work.done() && (!regions.expand.y || regions.backlog.is_empty()) { + break; + } + + regions.next(); + } + + Ok(Fragment::frames(finished)) +} + +/// Determine the flow's configuration. +fn configuration<'x>( + shared: StyleChain<'x>, + regions: Regions, + columns: NonZeroUsize, + column_gutter: Rel, + mode: FlowMode, +) -> Config<'x> { + Config { mode, shared, columns: { @@ -235,39 +278,7 @@ pub fn layout_flow<'a>( ) }, }), - }; - - // Collect the elements into pre-processed children. These are much easier - // to handle than the raw elements. - let bump = Bump::new(); - let children = collect( - engine, - &bump, - children, - locator.next(&()), - Size::new(config.columns.width, regions.full), - regions.expand.x, - mode, - )?; - - let mut work = Work::new(&children); - let mut finished = vec![]; - - // This loop runs once per region produced by the flow layout. - loop { - let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?; - finished.push(frame); - - // Terminate the loop when everything is processed, though draining the - // backlog if necessary. - if work.done() && (!regions.expand.y || regions.backlog.is_empty()) { - break; - } - - regions.next(); } - - Ok(Fragment::frames(finished)) } /// The work that is left to do by flow layout. diff --git a/crates/typst-layout/src/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs index 14cf2e3b8..5a1b7b4fc 100644 --- a/crates/typst-layout/src/inline/collect.rs +++ b/crates/typst-layout/src/inline/collect.rs @@ -2,10 +2,8 @@ use typst_library::diag::warning; 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, + Abs, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, Spacing, }; -use typst_library::model::{EnumElem, ListElem, TermsElem}; use typst_library::routines::Pair; use typst_library::text::{ is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes, @@ -123,40 +121,20 @@ pub fn collect<'a>( children: &[Pair<'a>], engine: &mut Engine<'_>, locator: &mut SplitLocator<'a>, - styles: StyleChain<'a>, + config: &Config, region: Size, - situation: Option, ) -> SourceResult<(String, Vec>, SpanMapper)> { let mut collector = Collector::new(2 + children.len()); let mut quoter = SmartQuoter::new(); - let outer_dir = TextElem::dir_in(styles); + if !config.first_line_indent.is_zero() { + collector.push_item(Item::Absolute(config.first_line_indent, false)); + collector.spans.push(1, Span::detached()); + } - if let Some(situation) = situation { - let first_line_indent = ParElem::first_line_indent_in(styles); - if !first_line_indent.amount.is_zero() - && match situation { - // First-line indent for the first paragraph after a list bullet - // just looks bad. - ParSituation::First => first_line_indent.all && !in_list(styles), - ParSituation::Consecutive => true, - ParSituation::Other => first_line_indent.all, - } - && AlignElem::alignment_in(styles).resolve(styles).x - == outer_dir.start().into() - { - collector.push_item(Item::Absolute( - first_line_indent.amount.resolve(styles), - false, - )); - collector.spans.push(1, Span::detached()); - } - - let hang = ParElem::hanging_indent_in(styles); - if !hang.is_zero() { - collector.push_item(Item::Absolute(-hang, false)); - collector.spans.push(1, Span::detached()); - } + if !config.hanging_indent.is_zero() { + collector.push_item(Item::Absolute(-config.hanging_indent, false)); + collector.spans.push(1, Span::detached()); } for &(child, styles) in children { @@ -167,7 +145,7 @@ pub fn collect<'a>( } else if let Some(elem) = child.to_packed::() { collector.build_text(styles, |full| { let dir = TextElem::dir_in(styles); - if dir != outer_dir { + if dir != config.dir { // Insert "Explicit Directional Embedding". match dir { Dir::LTR => full.push_str(LTR_EMBEDDING), @@ -182,7 +160,7 @@ pub fn collect<'a>( full.push_str(&elem.text); } - if dir != outer_dir { + if dir != config.dir { // Insert "Pop Directional Formatting". full.push_str(POP_EMBEDDING); } @@ -265,16 +243,6 @@ pub fn collect<'a>( Ok((collector.full, collector.segments, collector.spans)) } -/// Whether we have a list ancestor. -/// -/// When we support some kind of more general ancestry mechanism, this can -/// become more elegant. -fn in_list(styles: StyleChain) -> bool { - ListElem::depth_in(styles).0 > 0 - || !EnumElem::parents_in(styles).is_empty() - || TermsElem::within_in(styles) -} - /// Collects segments. struct Collector<'a> { full: String, diff --git a/crates/typst-layout/src/inline/finalize.rs b/crates/typst-layout/src/inline/finalize.rs index 7ad287c45..c9de0085e 100644 --- a/crates/typst-layout/src/inline/finalize.rs +++ b/crates/typst-layout/src/inline/finalize.rs @@ -9,7 +9,6 @@ pub fn finalize( engine: &mut Engine, p: &Preparation, lines: &[Line], - styles: StyleChain, region: Size, expand: bool, locator: &mut SplitLocator<'_>, @@ -19,9 +18,10 @@ pub fn finalize( let width = if !region.x.is_finite() || (!expand && lines.iter().all(|line| line.fr().is_zero())) { - region - .x - .min(p.hang + lines.iter().map(|line| line.width).max().unwrap_or_default()) + region.x.min( + p.config.hanging_indent + + lines.iter().map(|line| line.width).max().unwrap_or_default(), + ) } else { region.x }; @@ -29,7 +29,7 @@ pub fn finalize( // Stack the lines into one frame per region. lines .iter() - .map(|line| commit(engine, p, line, width, region.y, locator, styles)) + .map(|line| commit(engine, p, line, width, region.y, locator)) .collect::>() .map(Fragment::frames) } diff --git a/crates/typst-layout/src/inline/line.rs b/crates/typst-layout/src/inline/line.rs index 9f6973807..bd08f30ef 100644 --- a/crates/typst-layout/src/inline/line.rs +++ b/crates/typst-layout/src/inline/line.rs @@ -2,10 +2,9 @@ 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::model::ParLineMarker; use typst_library::text::{Lang, TextElem}; use typst_utils::Numeric; @@ -135,7 +134,7 @@ pub fn line<'a>( // Whether the line is justified. let justify = full.ends_with(LINE_SEPARATOR) - || (p.justify && breakpoint != Breakpoint::Mandatory); + || (p.config.justify && breakpoint != Breakpoint::Mandatory); // Process dashes. let dash = if breakpoint.is_hyphen() || full.ends_with(SHY) { @@ -157,14 +156,14 @@ pub fn line<'a>( // Add a hyphen at the line start, if a previous dash should be repeated. if pred.map_or(false, |pred| should_repeat_hyphen(pred, full)) { if let Some(shaped) = items.first_text_mut() { - shaped.prepend_hyphen(engine, p.fallback); + shaped.prepend_hyphen(engine, p.config.fallback); } } // Add a hyphen at the line end, if we ended on a soft hyphen. if dash == Some(Dash::Soft) { if let Some(shaped) = items.last_text_mut() { - shaped.push_hyphen(engine, p.fallback); + shaped.push_hyphen(engine, p.config.fallback); } } @@ -234,13 +233,13 @@ where { // If there is nothing bidirectional going on, skip reordering. let Some(bidi) = &p.bidi else { - f(range, p.dir == Dir::RTL); + f(range, p.config.dir == Dir::RTL); return; }; // The bidi crate panics for empty lines. if range.is_empty() { - f(range, p.dir == Dir::RTL); + f(range, p.config.dir == Dir::RTL); return; } @@ -308,13 +307,13 @@ fn collect_range<'a>( /// punctuation marks at line start or line end. fn adjust_cj_at_line_boundaries(p: &Preparation, text: &str, items: &mut Items) { if text.starts_with(BEGIN_PUNCT_PAT) - || (p.cjk_latin_spacing && text.starts_with(is_of_cj_script)) + || (p.config.cjk_latin_spacing && text.starts_with(is_of_cj_script)) { adjust_cj_at_line_start(p, items); } if text.ends_with(END_PUNCT_PAT) - || (p.cjk_latin_spacing && text.ends_with(is_of_cj_script)) + || (p.config.cjk_latin_spacing && text.ends_with(is_of_cj_script)) { adjust_cj_at_line_end(p, items); } @@ -332,7 +331,10 @@ fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) { let shrink = glyph.shrinkability().0; glyph.shrink_left(shrink); shaped.width -= shrink.at(shaped.size); - } else if p.cjk_latin_spacing && glyph.is_cj_script() && glyph.x_offset > Em::zero() { + } else if p.config.cjk_latin_spacing + && glyph.is_cj_script() + && glyph.x_offset > Em::zero() + { // If the first glyph is a CJK character adjusted by // [`add_cjk_latin_spacing`], restore the original width. let glyph = shaped.glyphs.to_mut().first_mut().unwrap(); @@ -359,7 +361,7 @@ fn adjust_cj_at_line_end(p: &Preparation, items: &mut Items) { let punct = shaped.glyphs.to_mut().last_mut().unwrap(); punct.shrink_right(shrink); shaped.width -= shrink.at(shaped.size); - } else if p.cjk_latin_spacing + } else if p.config.cjk_latin_spacing && glyph.is_cj_script() && (glyph.x_advance - glyph.x_offset) > Em::one() { @@ -424,16 +426,15 @@ pub fn commit( width: Abs, full: Abs, locator: &mut SplitLocator<'_>, - styles: StyleChain, ) -> SourceResult { - let mut remaining = width - line.width - p.hang; + let mut remaining = width - line.width - p.config.hanging_indent; let mut offset = Abs::zero(); // We always build the line from left to right. In an LTR paragraph, we must // thus add the hanging indent to the offset. In an RTL paragraph, the // hanging indent arises naturally due to the line width. - if p.dir == Dir::LTR { - offset += p.hang; + if p.config.dir == Dir::LTR { + offset += p.config.hanging_indent; } // Handle hanging punctuation to the left. @@ -554,11 +555,13 @@ pub fn commit( let mut output = Frame::soft(size); output.set_baseline(top); - add_par_line_marker(&mut output, styles, engine, locator, top); + if let Some(marker) = &p.config.numbering_marker { + add_par_line_marker(&mut output, marker, engine, locator, top); + } // Construct the line's frame. for (offset, frame) in frames { - let x = offset + p.align.position(remaining); + let x = offset + p.config.align.position(remaining); let y = top - frame.baseline(); output.push_frame(Point::new(x, y), frame); } @@ -575,26 +578,18 @@ pub fn commit( /// number in the margin, is aligned to the line's baseline. fn add_par_line_marker( output: &mut Frame, - styles: StyleChain, + marker: &Packed, engine: &mut Engine, locator: &mut SplitLocator, top: Abs, ) { - let Some(numbering) = ParLine::numbering_in(styles) else { return }; - let margin = ParLine::number_margin_in(styles); - let align = ParLine::number_align_in(styles); - - // Delay resolving the number clearance until line numbers are laid out to - // avoid inconsistent spacing depending on varying font size. - let clearance = ParLine::number_clearance_in(styles); - // Elements in tags must have a location for introspection to work. We do // the work here instead of going through all of the realization process // just for this, given we don't need to actually place the marker as we // manually search for it in the frame later (when building a root flow, // 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 mut marker = marker.clone(); let key = typst_utils::hash128(&marker); let loc = locator.next_location(engine.introspector, key); marker.set_location(loc); @@ -606,7 +601,7 @@ fn add_par_line_marker( // line's general baseline. However, the line number will still need to // manually adjust its own 'y' position based on its own baseline. let pos = Point::with_y(top); - output.push(pos, FrameItem::Tag(Tag::Start(marker))); + output.push(pos, FrameItem::Tag(Tag::Start(marker.pack()))); output.push(pos, FrameItem::Tag(Tag::End(loc, key))); } diff --git a/crates/typst-layout/src/inline/linebreak.rs b/crates/typst-layout/src/inline/linebreak.rs index 87113c689..a9f21188b 100644 --- a/crates/typst-layout/src/inline/linebreak.rs +++ b/crates/typst-layout/src/inline/linebreak.rs @@ -110,15 +110,7 @@ pub fn linebreak<'a>( p: &'a Preparation<'a>, width: Abs, ) -> Vec> { - let linebreaks = p.linebreaks.unwrap_or_else(|| { - if p.justify { - Linebreaks::Optimized - } else { - Linebreaks::Simple - } - }); - - match linebreaks { + match p.config.linebreaks { Linebreaks::Simple => linebreak_simple(engine, p, width), Linebreaks::Optimized => linebreak_optimized(engine, p, width), } @@ -384,7 +376,7 @@ fn linebreak_optimized_approximate( // Whether the line is justified. This is not 100% accurate w.r.t // to line()'s behaviour, but good enough. - let justify = p.justify && breakpoint != Breakpoint::Mandatory; + let justify = p.config.justify && breakpoint != Breakpoint::Mandatory; // We don't really know whether the line naturally ends with a dash // here, so we can miss that case, but it's ok, since all of this @@ -573,7 +565,7 @@ fn raw_ratio( // calculate the extra amount. Also, don't divide by zero. let extra_stretch = (delta - adjustability) / justifiables.max(1) as f64; // Normalize the amount by half the em size. - ratio = 1.0 + extra_stretch / (p.size / 2.0); + ratio = 1.0 + extra_stretch / (p.config.font_size / 2.0); } // The min value must be < MIN_RATIO, but how much smaller doesn't matter @@ -663,9 +655,9 @@ fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) { return; } - let hyphenate = p.hyphenate != Some(false); + let hyphenate = p.config.hyphenate != Some(false); let lb = LINEBREAK_DATA.as_borrowed(); - let segmenter = match p.lang { + let segmenter = match p.config.lang { Some(Lang::CHINESE | Lang::JAPANESE) => &CJ_SEGMENTER, _ => &SEGMENTER, }; @@ -830,18 +822,18 @@ fn linebreak_link(link: &str, mut f: impl FnMut(usize)) { /// Whether hyphenation is enabled at the given offset. fn hyphenate_at(p: &Preparation, offset: usize) -> bool { - p.hyphenate - .or_else(|| { - let (_, item) = p.get(offset); - let styles = item.text()?.styles; - Some(TextElem::hyphenate_in(styles)) - }) - .unwrap_or(false) + p.config.hyphenate.unwrap_or_else(|| { + let (_, item) = p.get(offset); + match item.text() { + Some(text) => TextElem::hyphenate_in(text.styles).unwrap_or(p.config.justify), + None => false, + } + }) } /// The text language at the given offset. fn lang_at(p: &Preparation, offset: usize) -> Option { - let lang = p.lang.or_else(|| { + let lang = p.config.lang.or_else(|| { let (_, item) = p.get(offset); let styles = item.text()?.styles; Some(TextElem::lang_in(styles)) @@ -865,13 +857,13 @@ impl CostMetrics { fn compute(p: &Preparation) -> Self { Self { // When justifying, we may stretch spaces below their natural width. - min_ratio: if p.justify { MIN_RATIO } else { 0.0 }, - min_approx_ratio: if p.justify { MIN_APPROX_RATIO } else { 0.0 }, + min_ratio: if p.config.justify { MIN_RATIO } else { 0.0 }, + min_approx_ratio: if p.config.justify { MIN_APPROX_RATIO } else { 0.0 }, // Approximate hyphen width for estimates. - approx_hyphen_width: Em::new(0.33).at(p.size), + approx_hyphen_width: Em::new(0.33).at(p.config.font_size), // Costs. - hyph_cost: DEFAULT_HYPH_COST * p.costs.hyphenation().get(), - runt_cost: DEFAULT_RUNT_COST * p.costs.runt().get(), + hyph_cost: DEFAULT_HYPH_COST * p.config.costs.hyphenation().get(), + runt_cost: DEFAULT_RUNT_COST * p.config.costs.runt().get(), } } diff --git a/crates/typst-layout/src/inline/mod.rs b/crates/typst-layout/src/inline/mod.rs index f8a36368d..5ef820d07 100644 --- a/crates/typst-layout/src/inline/mod.rs +++ b/crates/typst-layout/src/inline/mod.rs @@ -13,12 +13,17 @@ 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::{Packed, StyleChain}; +use typst_library::foundations::{Packed, Resolve, Smart, StyleChain}; use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator}; -use typst_library::layout::{Fragment, Size}; -use typst_library::model::ParElem; +use typst_library::layout::{Abs, AlignElem, Dir, FixedAlignment, Fragment, Size}; +use typst_library::model::{ + EnumElem, FirstLineIndent, Linebreaks, ListElem, ParElem, ParLine, ParLineMarker, + TermsElem, +}; use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; +use typst_library::text::{Costs, Lang, TextElem}; use typst_library::World; +use typst_utils::{Numeric, SliceExt}; use self::collect::{collect, Item, Segment, SpanMapper}; use self::deco::decorate; @@ -98,7 +103,7 @@ fn layout_par_impl( styles, )?; - layout_inline( + layout_inline_impl( &mut engine, &children, &mut locator, @@ -106,33 +111,134 @@ fn layout_par_impl( region, expand, Some(situation), + &ConfigBase { + justify: elem.justify(styles), + linebreaks: elem.linebreaks(styles), + first_line_indent: elem.first_line_indent(styles), + hanging_indent: elem.hanging_indent(styles), + }, ) } /// Lays out realized content with inline layout. -#[allow(clippy::too_many_arguments)] pub fn layout_inline<'a>( engine: &mut Engine, children: &[Pair<'a>], locator: &mut SplitLocator<'a>, - styles: StyleChain<'a>, + shared: StyleChain<'a>, + region: Size, + expand: bool, +) -> SourceResult { + layout_inline_impl( + engine, + children, + locator, + shared, + region, + expand, + None, + &ConfigBase { + justify: ParElem::justify_in(shared), + linebreaks: ParElem::linebreaks_in(shared), + first_line_indent: ParElem::first_line_indent_in(shared), + hanging_indent: ParElem::hanging_indent_in(shared), + }, + ) +} + +/// The internal implementation of [`layout_inline`]. +#[allow(clippy::too_many_arguments)] +fn layout_inline_impl<'a>( + engine: &mut Engine, + children: &[Pair<'a>], + locator: &mut SplitLocator<'a>, + shared: StyleChain<'a>, region: Size, expand: bool, par: Option, + base: &ConfigBase, ) -> SourceResult { + // Prepare configuration that is shared across the whole inline layout. + let config = configuration(base, children, shared, par); + // Collect all text into one string for BiDi analysis. - let (text, segments, spans) = - collect(children, engine, locator, styles, region, par)?; + let (text, segments, spans) = collect(children, engine, locator, &config, region)?; // Perform BiDi analysis and performs some preparation steps before we // proceed to line breaking. - let p = prepare(engine, children, &text, segments, spans, styles, par)?; + let p = prepare(engine, &config, &text, segments, spans)?; // Break the text into lines. - let lines = linebreak(engine, &p, region.x - p.hang); + let lines = linebreak(engine, &p, region.x - config.hanging_indent); // Turn the selected lines into frames. - finalize(engine, &p, &lines, styles, region, expand, locator) + finalize(engine, &p, &lines, region, expand, locator) +} + +/// Determine the inline layout's configuration. +fn configuration( + base: &ConfigBase, + children: &[Pair], + shared: StyleChain, + situation: Option, +) -> Config { + let justify = base.justify; + let font_size = TextElem::size_in(shared); + let dir = TextElem::dir_in(shared); + + Config { + justify, + linebreaks: base.linebreaks.unwrap_or_else(|| { + if justify { + Linebreaks::Optimized + } else { + Linebreaks::Simple + } + }), + first_line_indent: { + let FirstLineIndent { amount, all } = base.first_line_indent; + if !amount.is_zero() + && match situation { + // First-line indent for the first paragraph after a list + // bullet just looks bad. + Some(ParSituation::First) => all && !in_list(shared), + Some(ParSituation::Consecutive) => true, + Some(ParSituation::Other) => all, + None => false, + } + && AlignElem::alignment_in(shared).resolve(shared).x == dir.start().into() + { + amount.at(font_size) + } else { + Abs::zero() + } + }, + hanging_indent: if situation.is_some() { + base.hanging_indent + } else { + Abs::zero() + }, + numbering_marker: ParLine::numbering_in(shared).map(|numbering| { + Packed::new(ParLineMarker::new( + numbering, + ParLine::number_align_in(shared), + ParLine::number_margin_in(shared), + // Delay resolving the number clearance until line numbers are + // laid out to avoid inconsistent spacing depending on varying + // font size. + ParLine::number_clearance_in(shared), + )) + }), + align: AlignElem::alignment_in(shared).fix(dir).x, + font_size, + dir, + hyphenate: shared_get(children, shared, TextElem::hyphenate_in) + .map(|uniform| uniform.unwrap_or(justify)), + lang: shared_get(children, shared, TextElem::lang_in), + fallback: TextElem::fallback_in(shared), + cjk_latin_spacing: TextElem::cjk_latin_spacing_in(shared).is_auto(), + costs: TextElem::costs_in(shared), + } } /// Distinguishes between a few different kinds of paragraphs. @@ -148,3 +254,66 @@ pub enum ParSituation { /// Any other kind of paragraph. Other, } + +/// Raw values from a `ParElem` or style chain. Used to initialize a [`Config`]. +struct ConfigBase { + justify: bool, + linebreaks: Smart, + first_line_indent: FirstLineIndent, + hanging_indent: Abs, +} + +/// Shared configuration for the whole inline layout. +struct Config { + /// Whether to justify text. + justify: bool, + /// How to determine line breaks. + linebreaks: Linebreaks, + /// The indent the first line of a paragraph should have. + first_line_indent: Abs, + /// The indent that all but the first line of a paragraph should have. + hanging_indent: Abs, + /// Configuration for line numbering. + numbering_marker: Option>, + /// The resolved horizontal alignment. + align: FixedAlignment, + /// The text size. + font_size: Abs, + /// The dominant direction. + dir: Dir, + /// A uniform hyphenation setting (only `Some(_)` if it's the same for all + /// children, otherwise `None`). + hyphenate: Option, + /// The text language (only `Some(_)` if it's the same for all + /// children, otherwise `None`). + lang: Option, + /// Whether font fallback is enabled. + fallback: bool, + /// Whether to add spacing between CJK and Latin characters. + cjk_latin_spacing: bool, + /// Costs for various layout decisions. + costs: Costs, +} + +/// Get a style property, but only if it is the same for all of the children. +fn shared_get( + children: &[Pair], + styles: StyleChain<'_>, + getter: fn(StyleChain) -> T, +) -> Option { + let value = getter(styles); + children + .group_by_key(|&(_, s)| s) + .all(|(s, _)| getter(s) == value) + .then_some(value) +} + +/// Whether we have a list ancestor. +/// +/// When we support some kind of more general ancestry mechanism, this can +/// become more elegant. +fn in_list(styles: StyleChain) -> bool { + ListElem::depth_in(styles).0 > 0 + || !EnumElem::parents_in(styles).is_empty() + || TermsElem::within_in(styles) +} diff --git a/crates/typst-layout/src/inline/prepare.rs b/crates/typst-layout/src/inline/prepare.rs index 0344d4331..5d7fcd7cb 100644 --- a/crates/typst-layout/src/inline/prepare.rs +++ b/crates/typst-layout/src/inline/prepare.rs @@ -1,9 +1,4 @@ -use typst_library::foundations::{Resolve, Smart}; -use typst_library::layout::{Abs, AlignElem, Dir, Em, FixedAlignment}; -use typst_library::model::Linebreaks; -use typst_library::routines::Pair; -use typst_library::text::{Costs, Lang, TextElem}; -use typst_utils::SliceExt; +use typst_library::layout::{Dir, Em}; use unicode_bidi::{BidiInfo, Level as BidiLevel}; use super::*; @@ -17,6 +12,8 @@ use super::*; pub struct Preparation<'a> { /// The full text. pub text: &'a str, + /// Configuration for inline layout. + pub config: &'a Config, /// Bidirectional text embedding levels. /// /// This is `None` if all text directions are uniform (all the base @@ -28,28 +25,6 @@ pub struct Preparation<'a> { pub indices: Vec, /// The span mapper. pub spans: SpanMapper, - /// Whether to hyphenate if it's the same for all children. - pub hyphenate: Option, - /// Costs for various layout decisions. - pub costs: Costs, - /// The dominant direction. - pub dir: Dir, - /// The text language if it's the same for all children. - pub lang: Option, - /// The resolved horizontal alignment. - pub align: FixedAlignment, - /// Whether to justify text. - pub justify: bool, - /// Hanging indent to apply. - pub hang: Abs, - /// Whether to add spacing between CJK and Latin characters. - pub cjk_latin_spacing: bool, - /// Whether font fallback is enabled. - pub fallback: bool, - /// How to determine line breaks. - pub linebreaks: Smart, - /// The text size. - pub size: Abs, } impl<'a> Preparation<'a> { @@ -80,15 +55,12 @@ impl<'a> Preparation<'a> { #[typst_macros::time] pub fn prepare<'a>( engine: &mut Engine, - children: &[Pair<'a>], + config: &'a Config, text: &'a str, segments: Vec>, spans: SpanMapper, - styles: StyleChain<'a>, - situation: Option, ) -> SourceResult> { - let dir = TextElem::dir_in(styles); - let default_level = match dir { + let default_level = match config.dir { Dir::RTL => BidiLevel::rtl(), _ => BidiLevel::ltr(), }; @@ -124,51 +96,20 @@ pub fn prepare<'a>( indices.extend(range.clone().map(|_| i)); } - let cjk_latin_spacing = TextElem::cjk_latin_spacing_in(styles).is_auto(); - if cjk_latin_spacing { + if config.cjk_latin_spacing { add_cjk_latin_spacing(&mut items); } - // Only apply hanging indent to real paragraphs. - let hang = if situation.is_some() { - ParElem::hanging_indent_in(styles) - } else { - Abs::zero() - }; - Ok(Preparation { + config, text, bidi: is_bidi.then_some(bidi), items, indices, spans, - hyphenate: shared_get(children, styles, TextElem::hyphenate_in), - costs: TextElem::costs_in(styles), - dir, - lang: shared_get(children, styles, TextElem::lang_in), - align: AlignElem::alignment_in(styles).resolve(styles).x, - justify: ParElem::justify_in(styles), - hang, - cjk_latin_spacing, - fallback: TextElem::fallback_in(styles), - linebreaks: ParElem::linebreaks_in(styles), - size: TextElem::size_in(styles), }) } -/// Get a style property, but only if it is the same for all of the children. -fn shared_get( - children: &[Pair], - styles: StyleChain<'_>, - getter: fn(StyleChain) -> T, -) -> Option { - let value = getter(styles); - children - .group_by_key(|&(_, s)| s) - .all(|(s, _)| getter(s) == value) - .then_some(value) -} - /// Add some spacing between Han characters and western characters. See /// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition /// in Horizontal Written Mode diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs index 9a64992aa..59ac5b089 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -107,7 +107,6 @@ fn layout_inline_text( styles, Size::splat(Abs::inf()), false, - None, )? .into_frame(); diff --git a/crates/typst-library/src/model/link.rs b/crates/typst-library/src/model/link.rs index 24b746b7e..ea85aa945 100644 --- a/crates/typst-library/src/model/link.rs +++ b/crates/typst-library/src/model/link.rs @@ -11,7 +11,7 @@ use crate::foundations::{ use crate::html::{attr, tag, HtmlElem}; use crate::introspection::Location; use crate::layout::Position; -use crate::text::{Hyphenate, TextElem}; +use crate::text::TextElem; /// Links to a URL or a location in the document. /// @@ -138,7 +138,7 @@ impl Show for Packed { impl ShowSet for Packed { fn show_set(&self, _: StyleChain) -> Styles { let mut out = Styles::new(); - out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); + out.set(TextElem::set_hyphenate(Smart::Custom(false))); out } } diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index 12f4e4c59..30c2ea1d1 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -51,7 +51,6 @@ use crate::foundations::{ }; use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel}; use crate::math::{EquationElem, MathSize}; -use crate::model::ParElem; use crate::visualize::{Color, Paint, RelativeTo, Stroke}; use crate::World; @@ -504,9 +503,8 @@ pub struct TextElem { /// enabling hyphenation can /// improve justification. /// ``` - #[resolve] #[ghost] - pub hyphenate: Hyphenate, + pub hyphenate: Smart, /// The "cost" of various choices when laying out text. A higher cost means /// the layout engine will make the choice less often. Costs are specified @@ -1110,27 +1108,6 @@ impl Resolve for TextDir { } } -/// Whether to hyphenate text. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Hyphenate(pub Smart); - -cast! { - Hyphenate, - self => self.0.into_value(), - v: Smart => Self(v), -} - -impl Resolve for Hyphenate { - type Output = bool; - - fn resolve(self, styles: StyleChain) -> Self::Output { - match self.0 { - Smart::Auto => ParElem::justify_in(styles), - Smart::Custom(v) => v, - } - } -} - /// A set of stylistic sets to enable. #[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)] pub struct StylisticSets(u32); diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index 5bb21e43a..b330c01ef 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -21,9 +21,7 @@ use crate::html::{tag, HtmlElem}; use crate::layout::{BlockBody, BlockElem, Em, HAlignment}; use crate::loading::{DataSource, Load}; use crate::model::{Figurable, ParElem}; -use crate::text::{ - FontFamily, FontList, Hyphenate, LinebreakElem, LocalName, TextElem, TextSize, -}; +use crate::text::{FontFamily, FontList, LinebreakElem, LocalName, TextElem, TextSize}; use crate::visualize::Color; use crate::World; @@ -472,7 +470,7 @@ impl ShowSet for Packed { let mut out = Styles::new(); out.set(TextElem::set_overhang(false)); out.set(TextElem::set_lang(Lang::ENGLISH)); - out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); + out.set(TextElem::set_hyphenate(Smart::Custom(false))); out.set(TextElem::set_size(TextSize(Em::new(0.8).into()))); out.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")]))); out.set(TextElem::set_cjk_latin_spacing(Smart::Custom(None))); diff --git a/tests/ref/issue-5831-par-constructor-args.png b/tests/ref/issue-5831-par-constructor-args.png new file mode 100644 index 0000000000000000000000000000000000000000..440b612ba938a714148f598c52501450b6fe85aa GIT binary patch literal 1356 zcmV-S1+)5zP)e!Amty9JdH6Cr0@k9ib)FL`~)DA^aL==tIa*S3aps_WI7!m|Y zxFm8E5iG3+fk2BwOcqTzB4-jpf+07_{z?KbiX9NGm^b@dzf>25RyYT zs0D5-@$jV-NA4G?-k~?$>~K6F+X3hRMP)ZIVm& zAxx^J4#S0*9O?TS@CfpfU{UNgDFHmUkI$(!BY>lI6h>{23jh+X4tI>0JDm0&ivoa3 zkEDw_n+oVx16pIB4xW~dwgFITff2dw6)@Z|6w6sVML1p;_7nh424pd6)y&FRL(HV3$R&Z^hfP+{YE&z$wlW5_yB$yb}sfP;BGNA zTVarFY*PDGlQV5F#T_7Te6Sca2M%JrfV`QKPIm^sxo2>L-i+err>efBd!l=@ZrSZWp0Kgs^ zXTQ(LTg0kfpue)t9B@Y1pSPzz*Ka5#M!eq<&NP3`2Y{CMHPxRE_|*iT$tvNNy%Hvf zK`04}g0MQ8hv0!Dnhb!<+HR8*Mh{HL?daC_po@4hb}3{!I9o|2F%0b!IEXe2(GCDT zgua_FXr3E;OpiL>WC6JoiPy{4{&rTbtLHmG;-yGTooEF-wTl1$lIf4hT`R=Y+36EU z^Ne6ud0H<#4RJ3Ikvv`_a&BcY4XEUo|2?0tJo+%X>xEymLpIqS)pRxas-eSpaDTy zo$7VSKSypN^LkmuIA4JP+2hX?R=8thdmlMhB8kSg7VQKqIFHZKwHXXU8FX6PRIn|Z zziE812*COdBwv?CK==#J(4}rTRE6R7C{1n&1uzD$RpDpevv7>p%HL888u~)4i{Y4n zZ>NBUfvx!6_vUC~ylg5sxl$VdPjd#Ki5B2MUGrY#q$#_eguxdGQNX$s**|GNa*XOz z3lNou$OhSVcyg>k>;w4Vc@RD{YyBo*7M5Du)}zY2+%?Us{xhzC@eJ<7JJ`eK;~Xwh zcj%5q5^>j?aF8WqqyXp6rhRY7+nere+$Ax^aWZk?8yP$u=EWyguWP*-()rsDnT$tp zm!FOzCVxv?j3E2nZUnB~o$=>13dqy1*{Yy|C=JDwzq-+=TGe$j*0TEEPy~07F3sTC=IM0v?AQC=K8$jO zR8(J8+*47kHi;V0AySvvr1$j2w_qGQm5UxVi`wc;)GrJLQ!oX45&j3xlV%AnmsgDd O0000).len(), 1) +--- issue-5831-par-constructor-args --- +// Make sure that all arguments are also respected in the constructor. +A +#par( + leading: 2pt, + spacing: 20pt, + justify: true, + linebreaks: "simple", + first-line-indent: (amount: 1em, all: true), + hanging-indent: 5pt, +)[ + The par function has a constructor and justification. +] + --- show-par-set-block-hint --- // Warning: 2-36 `show par: set block(spacing: ..)` has no effect anymore // Hint: 2-36 this is specific to paragraphs as they are not considered blocks anymore From 81021fa1a277b04e8726d82fdf8f5fe11aeacab6 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 10 Feb 2025 16:39:14 +0100 Subject: [PATCH 022/172] Bump `typst-assets` (#5845) --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 140dccf74..bf69c12af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2753,7 +2753,7 @@ dependencies = [ [[package]] name = "typst-assets" version = "0.12.0" -source = "git+https://github.com/typst/typst-assets?rev=8cccef9#8cccef93b5da73a1c80389722cf2b655b624f577" +source = "git+https://github.com/typst/typst-assets?rev=8536748#8536748e4350198f34e519adff8593f258259cca" [[package]] name = "typst-cli" diff --git a/Cargo.toml b/Cargo.toml index 469439d38..ea9f8510e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ typst-svg = { path = "crates/typst-svg", version = "0.12.0" } typst-syntax = { path = "crates/typst-syntax", version = "0.12.0" } typst-timing = { path = "crates/typst-timing", version = "0.12.0" } typst-utils = { path = "crates/typst-utils", version = "0.12.0" } -typst-assets = { git = "https://github.com/typst/typst-assets", rev = "8cccef9" } +typst-assets = { git = "https://github.com/typst/typst-assets", rev = "8536748" } typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "7f8999d" } arrayvec = "0.7.4" az = "1.2" From a0cd89b478437e53ece754a901ccfc035b4f2acf Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 11 Feb 2025 11:30:30 +0100 Subject: [PATCH 023/172] Fix autocomplete and jumps in math (#5849) --- crates/typst-ide/src/complete.rs | 17 +++++++++++++++-- crates/typst-ide/src/jump.rs | 17 +++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index 7df788dc3..564b97bd7 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -306,7 +306,10 @@ fn complete_math(ctx: &mut CompletionContext) -> bool { } // Behind existing atom or identifier: "$a|$" or "$abc|$". - if matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathIdent) { + if matches!( + ctx.leaf.kind(), + SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::MathIdent + ) { ctx.from = ctx.leaf.offset(); math_completions(ctx); return true; @@ -358,7 +361,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool { // Behind an expression plus dot: "emoji.|". if_chain! { if ctx.leaf.kind() == SyntaxKind::Dot - || (ctx.leaf.kind() == SyntaxKind::Text + || (matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathText) && ctx.leaf.text() == "."); if ctx.leaf.range().end == ctx.cursor; if let Some(prev) = ctx.leaf.prev_sibling(); @@ -1768,4 +1771,14 @@ mod tests { test("#show outline.entry: it => it.\n#outline()\n= Hi", 30) .must_include(["indented", "body", "page"]); } + + #[test] + fn test_autocomplete_symbol_variants() { + test("#sym.arrow.", -1) + .must_include(["r", "dashed"]) + .must_exclude(["cases"]); + test("$ arrow. $", -3) + .must_include(["r", "dashed"]) + .must_exclude(["cases"]); + } } diff --git a/crates/typst-ide/src/jump.rs b/crates/typst-ide/src/jump.rs index ed74df226..428335426 100644 --- a/crates/typst-ide/src/jump.rs +++ b/crates/typst-ide/src/jump.rs @@ -73,7 +73,10 @@ pub fn jump_from_click( let Some(id) = span.id() else { continue }; let source = world.source(id).ok()?; let node = source.find(span)?; - let pos = if node.kind() == SyntaxKind::Text { + let pos = if matches!( + node.kind(), + SyntaxKind::Text | SyntaxKind::MathText + ) { let range = node.range(); let mut offset = range.start + usize::from(span_offset); if (click.x - pos.x) > width / 2.0 { @@ -115,7 +118,7 @@ pub fn jump_from_cursor( cursor: usize, ) -> Vec { fn is_text(node: &LinkedNode) -> bool { - node.get().kind() == SyntaxKind::Text + matches!(node.kind(), SyntaxKind::Text | SyntaxKind::MathText) } let root = LinkedNode::new(source.root()); @@ -261,6 +264,11 @@ mod tests { test_click(s, point(21.0, 12.0), cursor(56)); } + #[test] + fn test_jump_from_click_math() { + test_click("$a + b$", point(28.0, 14.0), cursor(5)); + } + #[test] fn test_jump_from_cursor() { let s = "*Hello* #box[ABC] World"; @@ -268,6 +276,11 @@ mod tests { test_cursor(s, 14, pos(1, 37.55, 16.58)); } + #[test] + fn test_jump_from_cursor_math() { + test_cursor("$a + b$", -3, pos(1, 27.51, 16.83)); + } + #[test] fn test_backlink() { let s = "#footnote[Hi]"; From 83ad407d3ccff4a8de1e7ffe198bfed874f5c0c7 Mon Sep 17 00:00:00 2001 From: +merlan #flirora Date: Wed, 12 Feb 2025 07:35:03 -0500 Subject: [PATCH 024/172] Update documentation for `float.{to-bits, from-bits}` (#5836) --- crates/typst-library/src/foundations/float.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/foundations/float.rs b/crates/typst-library/src/foundations/float.rs index fcc46b034..21d0a8d81 100644 --- a/crates/typst-library/src/foundations/float.rs +++ b/crates/typst-library/src/foundations/float.rs @@ -110,7 +110,7 @@ impl f64 { f64::signum(self) } - /// Converts bytes to a float. + /// Interprets bytes as a float. /// /// ```example /// #float.from-bytes(bytes((0, 0, 0, 0, 0, 0, 240, 63))) \ @@ -120,8 +120,10 @@ impl f64 { pub fn from_bytes( /// The bytes that should be converted to a float. /// - /// Must be of length exactly 8 so that the result fits into a 64-bit - /// float. + /// Must have a length of either 4 or 8. The bytes are then + /// interpreted in [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754)'s + /// binary32 (single-precision) or binary64 (double-precision) format + /// depending on the length of the bytes. bytes: Bytes, /// The endianness of the conversion. #[named] @@ -158,6 +160,13 @@ impl f64 { #[named] #[default(Endianness::Little)] endian: Endianness, + /// The size of the resulting bytes. + /// + /// This must be either 4 or 8. The call will return the + /// representation of this float in either + /// [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754)'s binary32 + /// (single-precision) or binary64 (double-precision) format + /// depending on the provided size. #[named] #[default(8)] size: u32, From 02cd43e27f2aafd7c332d7672a837e1b11cce120 Mon Sep 17 00:00:00 2001 From: +merlan #flirora Date: Wed, 12 Feb 2025 07:38:40 -0500 Subject: [PATCH 025/172] `Gradient::repeat`: Fix floating-point error in stop calculation (#5837) --- crates/typst-library/src/visualize/gradient.rs | 7 +++---- tests/suite/visualize/gradient.typ | 8 ++++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/typst-library/src/visualize/gradient.rs b/crates/typst-library/src/visualize/gradient.rs index 431f07dd4..d6530dd09 100644 --- a/crates/typst-library/src/visualize/gradient.rs +++ b/crates/typst-library/src/visualize/gradient.rs @@ -582,12 +582,11 @@ impl Gradient { let mut stops = stops .iter() .map(move |&(color, offset)| { - let t = i as f64 / n as f64; let r = offset.get(); if i % 2 == 1 && mirror { - (color, Ratio::new(t + (1.0 - r) / n as f64)) + (color, Ratio::new((i as f64 + 1.0 - r) / n as f64)) } else { - (color, Ratio::new(t + r / n as f64)) + (color, Ratio::new((i as f64 + r) / n as f64)) } }) .collect::>(); @@ -1230,7 +1229,7 @@ fn process_stops(stops: &[Spanned]) -> SourceResult Date: Wed, 12 Feb 2025 16:50:48 +0100 Subject: [PATCH 026/172] Lazy parsing of the package index (#5851) --- Cargo.lock | 2 + crates/typst-kit/Cargo.toml | 2 + crates/typst-kit/src/package.rs | 75 ++++++++++++++++++++++++++++++--- 3 files changed, 73 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bf69c12af..66a1e3a12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2901,6 +2901,8 @@ dependencies = [ "native-tls", "once_cell", "openssl", + "serde", + "serde_json", "tar", "typst-assets", "typst-library", diff --git a/crates/typst-kit/Cargo.toml b/crates/typst-kit/Cargo.toml index 266eba0b4..52aa407c3 100644 --- a/crates/typst-kit/Cargo.toml +++ b/crates/typst-kit/Cargo.toml @@ -23,6 +23,8 @@ flate2 = { workspace = true, optional = true } fontdb = { workspace = true, optional = true } native-tls = { workspace = true, optional = true } once_cell = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } tar = { workspace = true, optional = true } ureq = { workspace = true, optional = true } diff --git a/crates/typst-kit/src/package.rs b/crates/typst-kit/src/package.rs index e7eb71ee4..172d8740a 100644 --- a/crates/typst-kit/src/package.rs +++ b/crates/typst-kit/src/package.rs @@ -5,10 +5,9 @@ use std::path::{Path, PathBuf}; use ecow::eco_format; use once_cell::sync::OnceCell; +use serde::Deserialize; use typst_library::diag::{bail, PackageError, PackageResult, StrResult}; -use typst_syntax::package::{ - PackageInfo, PackageSpec, PackageVersion, VersionlessPackageSpec, -}; +use typst_syntax::package::{PackageSpec, PackageVersion, VersionlessPackageSpec}; use crate::download::{Downloader, Progress}; @@ -32,7 +31,7 @@ pub struct PackageStorage { /// The downloader used for fetching the index and packages. downloader: Downloader, /// The cached index of the default namespace. - index: OnceCell>, + index: OnceCell>, } impl PackageStorage { @@ -42,6 +41,18 @@ impl PackageStorage { package_cache_path: Option, package_path: Option, downloader: Downloader, + ) -> Self { + Self::with_index(package_cache_path, package_path, downloader, OnceCell::new()) + } + + /// Creates a new package storage with a pre-defined index. + /// + /// Useful for testing. + fn with_index( + package_cache_path: Option, + package_path: Option, + downloader: Downloader, + index: OnceCell>, ) -> Self { Self { package_cache_path: package_cache_path.or_else(|| { @@ -51,7 +62,7 @@ impl PackageStorage { dirs::data_dir().map(|data_dir| data_dir.join(DEFAULT_PACKAGES_SUBDIR)) }), downloader, - index: OnceCell::new(), + index, } } @@ -109,6 +120,7 @@ impl PackageStorage { // version. self.download_index()? .iter() + .filter_map(|value| MinimalPackageInfo::deserialize(value).ok()) .filter(|package| package.name == spec.name) .map(|package| package.version) .max() @@ -131,7 +143,7 @@ impl PackageStorage { } /// Download the package index. The result of this is cached for efficiency. - pub fn download_index(&self) -> StrResult<&[PackageInfo]> { + pub fn download_index(&self) -> StrResult<&[serde_json::Value]> { self.index .get_or_try_init(|| { let url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/index.json"); @@ -186,3 +198,54 @@ impl PackageStorage { }) } } + +/// Minimal information required about a package to determine its latest +/// version. +#[derive(Deserialize)] +struct MinimalPackageInfo { + name: String, + version: PackageVersion, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn lazy_deser_index() { + let storage = PackageStorage::with_index( + None, + None, + Downloader::new("typst/test"), + OnceCell::with_value(vec![ + serde_json::json!({ + "name": "charged-ieee", + "version": "0.1.0", + "entrypoint": "lib.typ", + }), + serde_json::json!({ + "name": "unequivocal-ams", + // This version number is currently not valid, so this package + // can't be parsed. + "version": "0.2.0-dev", + "entrypoint": "lib.typ", + }), + ]), + ); + + let ieee_version = storage.determine_latest_version(&VersionlessPackageSpec { + namespace: "preview".into(), + name: "charged-ieee".into(), + }); + assert_eq!(ieee_version, Ok(PackageVersion { major: 0, minor: 1, patch: 0 })); + + let ams_version = storage.determine_latest_version(&VersionlessPackageSpec { + namespace: "preview".into(), + name: "unequivocal-ams".into(), + }); + assert_eq!( + ams_version, + Err("failed to find package @preview/unequivocal-ams".into()) + ) + } +} From 5fc679f3e7501ee5831f1b4b7789350f43b4c221 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sun, 16 Feb 2025 14:18:39 +0100 Subject: [PATCH 027/172] Remove Linux Libertine warning (#5876) --- crates/typst-library/src/text/mod.rs | 19 +------------------ tests/suite/text/font.typ | 5 ----- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index 30c2ea1d1..3aac15ba5 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -1380,24 +1380,7 @@ pub fn is_default_ignorable(c: char) -> bool { fn check_font_list(engine: &mut Engine, list: &Spanned) { let book = engine.world.book(); for family in &list.v { - let found = book.contains_family(family.as_str()); - if family.as_str() == "linux libertine" { - let mut warning = warning!( - list.span, - "Typst's default font has changed from Linux Libertine to its successor Libertinus Serif"; - hint: "please set the font to `\"Libertinus Serif\"` instead" - ); - - if found { - warning.hint( - "Linux Libertine is available on your system - \ - you can ignore this warning if you are sure you want to use it", - ); - warning.hint("this warning will be removed in Typst 0.13"); - } - - engine.sink.warn(warning); - } else if !found { + if !book.contains_family(family.as_str()) { engine.sink.warn(warning!( list.span, "unknown font family: {}", diff --git a/tests/suite/text/font.typ b/tests/suite/text/font.typ index 9e5c0150d..60a1cd94d 100644 --- a/tests/suite/text/font.typ +++ b/tests/suite/text/font.typ @@ -77,11 +77,6 @@ I #let var = text(font: ("list-of", "nonexistent-fonts"))[don't] #var ---- text-font-linux-libertine --- -// Warning: 17-34 Typst's default font has changed from Linux Libertine to its successor Libertinus Serif -// Hint: 17-34 please set the font to `"Libertinus Serif"` instead -#set text(font: "Linux Libertine") - --- issue-5499-text-fill-in-clip-block --- #let t = tiling( From 25c86accbb4adc0e7542d2c5957dff6e939cbf48 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 17 Feb 2025 11:56:00 +0100 Subject: [PATCH 028/172] More robust SVG auto-detection (#5878) --- Cargo.lock | 1 + Cargo.toml | 1 + crates/typst-library/Cargo.toml | 1 + .../typst-library/src/visualize/image/mod.rs | 18 ++++++++++++++++-- docs/changelog/0.13.0.md | 3 +-- tests/ref/image-svg-auto-detection.png | Bin 0 -> 129 bytes tests/suite/visualize/image.typ | 15 +++++++++++++-- 7 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 tests/ref/image-svg-auto-detection.png diff --git a/Cargo.lock b/Cargo.lock index 66a1e3a12..249ee3bc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2966,6 +2966,7 @@ dependencies = [ "kamadak-exif", "kurbo", "lipsum", + "memchr", "palette", "phf", "png", diff --git a/Cargo.toml b/Cargo.toml index ea9f8510e..6fb64d3ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ kamadak-exif = "0.6" kurbo = "0.11" libfuzzer-sys = "0.4" lipsum = "0.9" +memchr = "2" miniz_oxide = "0.8" native-tls = "0.2" notify = "8" diff --git a/crates/typst-library/Cargo.toml b/crates/typst-library/Cargo.toml index cc5e26712..fb45ec862 100644 --- a/crates/typst-library/Cargo.toml +++ b/crates/typst-library/Cargo.toml @@ -38,6 +38,7 @@ indexmap = { workspace = true } kamadak-exif = { workspace = true } kurbo = { workspace = true } lipsum = { workspace = true } +memchr = { workspace = true } palette = { workspace = true } phf = { workspace = true } png = { workspace = true } diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 97189e22d..258eb96f3 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -398,8 +398,7 @@ impl ImageFormat { return Some(Self::Raster(RasterFormat::Exchange(format))); } - // SVG or compressed SVG. - if data.starts_with(b" bool { + // Check for the gzip magic bytes. This check is perhaps a bit too + // permissive as other formats than SVGZ could use gzip. + if data.starts_with(&[0x1f, 0x8b]) { + return true; + } + + // If the first 2048 bytes contain the SVG namespace declaration, we assume + // that it's an SVG. Note that, if the SVG does not contain a namespace + // declaration, usvg will reject it. + let head = &data[..data.len().min(2048)]; + memchr::memmem::find(head, b"http://www.w3.org/2000/svg").is_some() +} + /// A vector graphics format. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum VectorFormat { diff --git a/docs/changelog/0.13.0.md b/docs/changelog/0.13.0.md index 2caace723..e5315e5b6 100644 --- a/docs/changelog/0.13.0.md +++ b/docs/changelog/0.13.0.md @@ -99,8 +99,7 @@ description: Changes slated to appear in Typst 0.13.0 - Fixed interaction of clipping and outset on [`box`] and [`block`] - Fixed panic with [`path`] of infinite length - Fixed non-solid (e.g. tiling) text fills in clipped blocks -- Auto-detection of image formats from a raw buffer now has basic support for - SVGs +- Auto-detection of image formats from a raw buffer now has support for SVGs ## Scripting - Functions that accept [file paths]($syntax/#paths) now also accept raw diff --git a/tests/ref/image-svg-auto-detection.png b/tests/ref/image-svg-auto-detection.png new file mode 100644 index 0000000000000000000000000000000000000000..0240f8f5cf74eaa704282288c12784b981ebcf37 GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^6+j%%#0(_k7Qa3Zq<8{+LR|m<|KFdT-QeKxpMgPb zzGnhZ+`!YtF{I*FvIOhm1P;b+p%Mv9lT5zX^L$7Mse-`@58)NOZm&-S8niGl$Zgtk U`tV(DZlGQUPgg&ebxsLQ04L`tRR910 literal 0 HcmV?d00001 diff --git a/tests/suite/visualize/image.typ b/tests/suite/visualize/image.typ index e37932f28..7ce0c8c0a 100644 --- a/tests/suite/visualize/image.typ +++ b/tests/suite/visualize/image.typ @@ -65,6 +65,17 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B caption: [Bilingual text] ) +--- image-svg-auto-detection --- +#image(bytes( + ``` + + + + + + ```.text +)) + --- image-pixmap-rgb8 --- #image( bytes(( @@ -152,8 +163,8 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B #image("path/does/not/exist") --- image-bad-format --- -// Error: 2-22 unknown image format -#image("./image.typ") +// Error: 2-37 unknown image format +#image("/assets/plugins/hello.wasm") --- image-bad-svg --- // Error: 2-33 failed to parse SVG (found closing tag 'g' instead of 'style' in line 4) From 74e4f78687d7acb5db3d531959c956717cce837a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=A1=A5=E1=A0=A0=E1=A1=B3=E1=A1=A4=E1=A1=B3=E1=A0=B6?= =?UTF-8?q?=E1=A0=A0=20=E1=A1=A5=E1=A0=A0=E1=A0=AF=E1=A0=A0=C2=B7=E1=A0=A8?= =?UTF-8?q?=E1=A1=9D=E1=A1=B4=E1=A0=A3=20=E7=8C=AB?= Date: Tue, 18 Feb 2025 18:16:19 +0800 Subject: [PATCH 029/172] HTML export: Use `` for inline `RawElem` (#5884) --- crates/typst-library/src/text/raw.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index b330c01ef..1ce8bfc61 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -446,10 +446,14 @@ impl Show for Packed { let mut realized = Content::sequence(seq); if TargetElem::target_in(styles).is_html() { - return Ok(HtmlElem::new(tag::pre) - .with_body(Some(realized)) - .pack() - .spanned(self.span())); + return Ok(HtmlElem::new(if self.block(styles) { + tag::pre + } else { + tag::code + }) + .with_body(Some(realized)) + .pack() + .spanned(self.span())); } if self.block(styles) { From 3de3813ca06c332cd1eae14c64913725a9333aff Mon Sep 17 00:00:00 2001 From: Matthew Toohey Date: Tue, 18 Feb 2025 13:04:40 -0500 Subject: [PATCH 030/172] --make-deps fixes (#5873) --- crates/typst-cli/src/compile.rs | 83 +++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 25 deletions(-) diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index 515a777a2..2b6a7d820 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -6,8 +6,9 @@ use std::path::{Path, PathBuf}; use chrono::{DateTime, Datelike, Timelike, Utc}; use codespan_reporting::diagnostic::{Diagnostic, Label}; use codespan_reporting::term; -use ecow::{eco_format, EcoString}; +use ecow::eco_format; use parking_lot::RwLock; +use pathdiff::diff_paths; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use typst::diag::{ bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned, @@ -188,7 +189,7 @@ pub fn compile_once( match output { // Export the PDF / PNG. - Ok(()) => { + Ok(outputs) => { let duration = start.elapsed(); if config.watching { @@ -202,7 +203,7 @@ pub fn compile_once( print_diagnostics(world, &[], &warnings, config.diagnostic_format) .map_err(|err| eco_format!("failed to print diagnostics ({err})"))?; - write_make_deps(world, config)?; + write_make_deps(world, config, outputs)?; open_output(config)?; } @@ -226,12 +227,15 @@ pub fn compile_once( fn compile_and_export( world: &mut SystemWorld, config: &mut CompileConfig, -) -> Warned> { +) -> Warned>> { match config.output_format { OutputFormat::Html => { let Warned { output, warnings } = typst::compile::(world); let result = output.and_then(|document| export_html(&document, config)); - Warned { output: result, warnings } + Warned { + output: result.map(|()| vec![config.output.clone()]), + warnings, + } } _ => { let Warned { output, warnings } = typst::compile::(world); @@ -257,9 +261,14 @@ fn export_html(document: &HtmlDocument, config: &CompileConfig) -> SourceResult< } /// Export to a paged target format. -fn export_paged(document: &PagedDocument, config: &CompileConfig) -> SourceResult<()> { +fn export_paged( + document: &PagedDocument, + config: &CompileConfig, +) -> SourceResult> { match config.output_format { - OutputFormat::Pdf => export_pdf(document, config), + OutputFormat::Pdf => { + export_pdf(document, config).map(|()| vec![config.output.clone()]) + } OutputFormat::Png => { export_image(document, config, ImageExportFormat::Png).at(Span::detached()) } @@ -327,7 +336,7 @@ fn export_image( document: &PagedDocument, config: &CompileConfig, fmt: ImageExportFormat, -) -> StrResult<()> { +) -> StrResult> { // Determine whether we have indexable templates in output let can_handle_multiple = match config.output { Output::Stdout => false, @@ -383,7 +392,7 @@ fn export_image( && config.export_cache.is_cached(*i, &page.frame) && path.exists() { - return Ok(()); + return Ok(Output::Path(path.to_path_buf())); } Output::Path(path.to_owned()) @@ -392,11 +401,9 @@ fn export_image( }; export_image_page(config, page, &output, fmt)?; - Ok(()) + Ok(output) }) - .collect::, EcoString>>()?; - - Ok(()) + .collect::>>() } mod output_template { @@ -501,14 +508,25 @@ impl ExportCache { /// Writes a Makefile rule describing the relationship between the output and /// its dependencies to the path specified by the --make-deps argument, if it /// was provided. -fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult<()> { +fn write_make_deps( + world: &mut SystemWorld, + config: &CompileConfig, + outputs: Vec, +) -> StrResult<()> { let Some(ref make_deps_path) = config.make_deps else { return Ok(()) }; - let Output::Path(output_path) = &config.output else { - bail!("failed to create make dependencies file because output was stdout") - }; - let Some(output_path) = output_path.as_os_str().to_str() else { + let Ok(output_paths) = outputs + .into_iter() + .filter_map(|o| match o { + Output::Path(path) => Some(path.into_os_string().into_string()), + Output::Stdout => None, + }) + .collect::, _>>() + else { bail!("failed to create make dependencies file because output path was not valid unicode") }; + if output_paths.is_empty() { + bail!("failed to create make dependencies file because output was stdout") + } // Based on `munge` in libcpp/mkdeps.cc from the GCC source code. This isn't // perfect as some special characters can't be escaped. @@ -522,6 +540,10 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult res.push('$'); slashes = 0; } + ':' => { + res.push('\\'); + slashes = 0; + } ' ' | '\t' => { // `munge`'s source contains a comment here that says: "A // space or tab preceded by 2N+1 backslashes represents N @@ -544,18 +566,29 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult fn write( make_deps_path: &Path, - output_path: &str, + output_paths: Vec, root: PathBuf, dependencies: impl Iterator, ) -> io::Result<()> { let mut file = File::create(make_deps_path)?; + let current_dir = std::env::current_dir()?; + let relative_root = diff_paths(&root, ¤t_dir).unwrap_or(root.clone()); - file.write_all(munge(output_path).as_bytes())?; + for (i, output_path) in output_paths.into_iter().enumerate() { + if i != 0 { + file.write_all(b" ")?; + } + file.write_all(munge(&output_path).as_bytes())?; + } file.write_all(b":")?; for dependency in dependencies { - let Some(dependency) = - dependency.strip_prefix(&root).unwrap_or(&dependency).to_str() - else { + let relative_dependency = match dependency.strip_prefix(&root) { + Ok(root_relative_dependency) => { + relative_root.join(root_relative_dependency) + } + Err(_) => dependency, + }; + let Some(relative_dependency) = relative_dependency.to_str() else { // Silently skip paths that aren't valid unicode so we still // produce a rule that will work for the other paths that can be // processed. @@ -563,14 +596,14 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult }; file.write_all(b" ")?; - file.write_all(munge(dependency).as_bytes())?; + file.write_all(munge(relative_dependency).as_bytes())?; } file.write_all(b"\n")?; Ok(()) } - write(make_deps_path, output_path, world.root().to_owned(), world.dependencies()) + write(make_deps_path, output_paths, world.root().to_owned(), world.dependencies()) .map_err(|err| { eco_format!("failed to create make dependencies file due to IO error ({err})") }) From a543ee9445c0541b34a2bb5ea3b48ca596b71152 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 19 Feb 2025 10:59:27 +0100 Subject: [PATCH 031/172] Update changelog (#5894) --- docs/changelog/0.13.0.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/changelog/0.13.0.md b/docs/changelog/0.13.0.md index e5315e5b6..4212c8251 100644 --- a/docs/changelog/0.13.0.md +++ b/docs/changelog/0.13.0.md @@ -99,6 +99,8 @@ description: Changes slated to appear in Typst 0.13.0 - Fixed interaction of clipping and outset on [`box`] and [`block`] - Fixed panic with [`path`] of infinite length - Fixed non-solid (e.g. tiling) text fills in clipped blocks +- Fixed a crash for images with a DPI value of zero +- Fixed floating-point error in [`gradient.repeat`] - Auto-detection of image formats from a raw buffer now has support for SVGs ## Scripting @@ -186,12 +188,12 @@ description: Changes slated to appear in Typst 0.13.0 - [CJK-Latin-spacing]($text.cjk-latin-spacing) does not affect [raw] text anymore - Fixed wrong language codes being used for Greek and Ukrainian -- Fixed default quotes for Croatian +- Fixed default quotes for Croatian and Bulgarian - Fixed crash in RTL text handling - Added support for [`raw`] syntax highlighting for a few new languages: CFML, NSIS, and WGSL - New font metadata exception for New Computer Modern Sans Math -- Updated bundled New Computer Modern fonts to version 7.0 +- Updated bundled New Computer Modern fonts to version 7.0.1 ## Layout - Fixed various bugs with footnotes @@ -270,6 +272,9 @@ feature flag. - Added a live reloading HTTP server to `typst watch` when targeting HTML - Fixed self-update not being aware about certain target architectures - Fixed crash when piping `typst fonts` output to another command +- Fixed handling of relative paths in `--make-deps` output +- Fixed handling of multipage SVG and PNG export in `--make-deps` output +- Colons in filenames are now correctly escaped in `--make-deps` output ## Symbols - New @@ -312,6 +317,9 @@ feature flag. functions directly accepting both paths and bytes - The `sect` and its variants in favor of `inter`, and `integral.sect` in favor of `integral.inter` +- The compatibility behavior of type/str comparisons (e.g. `{int == "integer"}`) + which was temporarily introduced in Typst 0.8 now emits warnings. It will be + removed in Typst 0.14. ## Removals - Removed `style` function and `styles` argument of [`measure`], use a [context] @@ -323,9 +331,6 @@ feature flag. - Removed compatibility behavior where [`counter.display`] worked without [context] **(Breaking change)** - Removed compatibility behavior of [`locate`] **(Breaking change)** -- Removed compatibility behavior of type/str comparisons - (e.g. `{int == "integer"}`) which was temporarily introduced in Typst 0.8 - **(Breaking change)** ## Development - The `typst::compile` function is now generic and can return either a From d199546f9fe92b2d380dc337298fdca3e6fca8c8 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 19 Feb 2025 11:25:09 +0100 Subject: [PATCH 032/172] Bump version on main The tagged commit itself is on the 0.13 branch. --- Cargo.lock | 46 +++++++++++++++++++-------------------- Cargo.toml | 38 ++++++++++++++++---------------- docs/changelog/0.13.0.md | 9 +++++--- docs/changelog/welcome.md | 2 +- 4 files changed, 49 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 249ee3bc5..1851134a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2735,7 +2735,7 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" [[package]] name = "typst" -version = "0.12.0" +version = "0.13.0" dependencies = [ "comemo", "ecow", @@ -2752,12 +2752,12 @@ dependencies = [ [[package]] name = "typst-assets" -version = "0.12.0" -source = "git+https://github.com/typst/typst-assets?rev=8536748#8536748e4350198f34e519adff8593f258259cca" +version = "0.13.0" +source = "git+https://github.com/typst/typst-assets?rev=fa0f8a4#fa0f8a438cc4bc2113cc0aa3304cd68cdc2bc020" [[package]] name = "typst-cli" -version = "0.12.0" +version = "0.13.0" dependencies = [ "chrono", "clap", @@ -2802,12 +2802,12 @@ dependencies = [ [[package]] name = "typst-dev-assets" -version = "0.12.0" -source = "git+https://github.com/typst/typst-dev-assets?rev=7f8999d#7f8999d19907cd6e1148b295efbc844921c0761c" +version = "0.13.0" +source = "git+https://github.com/typst/typst-dev-assets?rev=61aebe9#61aebe9575a5abff889f76d73c7b01dc8e17e340" [[package]] name = "typst-docs" -version = "0.12.0" +version = "0.13.0" dependencies = [ "clap", "ecow", @@ -2830,7 +2830,7 @@ dependencies = [ [[package]] name = "typst-eval" -version = "0.12.0" +version = "0.13.0" dependencies = [ "comemo", "ecow", @@ -2848,7 +2848,7 @@ dependencies = [ [[package]] name = "typst-fuzz" -version = "0.12.0" +version = "0.13.0" dependencies = [ "comemo", "libfuzzer-sys", @@ -2860,7 +2860,7 @@ dependencies = [ [[package]] name = "typst-html" -version = "0.12.0" +version = "0.13.0" dependencies = [ "comemo", "ecow", @@ -2874,7 +2874,7 @@ dependencies = [ [[package]] name = "typst-ide" -version = "0.12.0" +version = "0.13.0" dependencies = [ "comemo", "ecow", @@ -2891,7 +2891,7 @@ dependencies = [ [[package]] name = "typst-kit" -version = "0.12.0" +version = "0.13.0" dependencies = [ "dirs", "ecow", @@ -2914,7 +2914,7 @@ dependencies = [ [[package]] name = "typst-layout" -version = "0.12.0" +version = "0.13.0" dependencies = [ "az", "bumpalo", @@ -2944,7 +2944,7 @@ dependencies = [ [[package]] name = "typst-library" -version = "0.12.0" +version = "0.13.0" dependencies = [ "az", "bitflags 2.8.0", @@ -3004,7 +3004,7 @@ dependencies = [ [[package]] name = "typst-macros" -version = "0.12.0" +version = "0.13.0" dependencies = [ "heck", "proc-macro2", @@ -3014,7 +3014,7 @@ dependencies = [ [[package]] name = "typst-pdf" -version = "0.12.0" +version = "0.13.0" dependencies = [ "arrayvec", "base64", @@ -3040,7 +3040,7 @@ dependencies = [ [[package]] name = "typst-realize" -version = "0.12.0" +version = "0.13.0" dependencies = [ "arrayvec", "bumpalo", @@ -3056,7 +3056,7 @@ dependencies = [ [[package]] name = "typst-render" -version = "0.12.0" +version = "0.13.0" dependencies = [ "bytemuck", "comemo", @@ -3072,7 +3072,7 @@ dependencies = [ [[package]] name = "typst-svg" -version = "0.12.0" +version = "0.13.0" dependencies = [ "base64", "comemo", @@ -3090,7 +3090,7 @@ dependencies = [ [[package]] name = "typst-syntax" -version = "0.12.0" +version = "0.13.0" dependencies = [ "ecow", "serde", @@ -3106,7 +3106,7 @@ dependencies = [ [[package]] name = "typst-tests" -version = "0.12.0" +version = "0.13.0" dependencies = [ "clap", "comemo", @@ -3131,7 +3131,7 @@ dependencies = [ [[package]] name = "typst-timing" -version = "0.12.0" +version = "0.13.0" dependencies = [ "parking_lot", "serde", @@ -3141,7 +3141,7 @@ dependencies = [ [[package]] name = "typst-utils" -version = "0.12.0" +version = "0.13.0" dependencies = [ "once_cell", "portable-atomic", diff --git a/Cargo.toml b/Cargo.toml index 6fb64d3ab..198aff3c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ default-members = ["crates/typst-cli"] resolver = "2" [workspace.package] -version = "0.12.0" +version = "0.13.0" rust-version = "1.80" # also change in ci.yml authors = ["The Typst Project Developers"] edition = "2021" @@ -16,24 +16,24 @@ keywords = ["typst"] 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-html = { path = "crates/typst-html", 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" } -typst-timing = { path = "crates/typst-timing", version = "0.12.0" } -typst-utils = { path = "crates/typst-utils", version = "0.12.0" } -typst-assets = { git = "https://github.com/typst/typst-assets", rev = "8536748" } -typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "7f8999d" } +typst = { path = "crates/typst", version = "0.13.0" } +typst-cli = { path = "crates/typst-cli", version = "0.13.0" } +typst-eval = { path = "crates/typst-eval", version = "0.13.0" } +typst-html = { path = "crates/typst-html", version = "0.13.0" } +typst-ide = { path = "crates/typst-ide", version = "0.13.0" } +typst-kit = { path = "crates/typst-kit", version = "0.13.0" } +typst-layout = { path = "crates/typst-layout", version = "0.13.0" } +typst-library = { path = "crates/typst-library", version = "0.13.0" } +typst-macros = { path = "crates/typst-macros", version = "0.13.0" } +typst-pdf = { path = "crates/typst-pdf", version = "0.13.0" } +typst-realize = { path = "crates/typst-realize", version = "0.13.0" } +typst-render = { path = "crates/typst-render", version = "0.13.0" } +typst-svg = { path = "crates/typst-svg", version = "0.13.0" } +typst-syntax = { path = "crates/typst-syntax", version = "0.13.0" } +typst-timing = { path = "crates/typst-timing", version = "0.13.0" } +typst-utils = { path = "crates/typst-utils", version = "0.13.0" } +typst-assets = { git = "https://github.com/typst/typst-assets", rev = "fa0f8a4" } +typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "61aebe9" } arrayvec = "0.7.4" az = "1.2" base64 = "0.22" diff --git a/docs/changelog/0.13.0.md b/docs/changelog/0.13.0.md index 4212c8251..6c2fe4275 100644 --- a/docs/changelog/0.13.0.md +++ b/docs/changelog/0.13.0.md @@ -1,9 +1,9 @@ --- -title: Unreleased changes planned for 0.13.0 -description: Changes slated to appear in Typst 0.13.0 +title: 0.13.0 +description: Changes in Typst 0.13.0 --- -# Unreleased +# Version 0.13.0 (February 19, 2025) ## Highlights - There is now a distinction between [proper paragraphs]($par) and just @@ -339,3 +339,6 @@ feature flag. feature is enabled - Increased minimum supported Rust version to 1.80 - Fixed linux/arm64 Docker image + +## Contributors + diff --git a/docs/changelog/welcome.md b/docs/changelog/welcome.md index bb245eb01..8fb85f870 100644 --- a/docs/changelog/welcome.md +++ b/docs/changelog/welcome.md @@ -10,7 +10,7 @@ forward. This section documents all changes to Typst since its initial public release. ## Versions -- [Unreleased changes planned for Typst 0.13.0]($changelog/0.13.0) +- [Typst 0.13.0]($changelog/0.13.0) - [Typst 0.12.0]($changelog/0.12.0) - [Typst 0.11.1]($changelog/0.11.1) - [Typst 0.11.0]($changelog/0.11.0) From 240f238eee4d6dfce7e3c4cabb9315ad052ca230 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Sun, 23 Feb 2025 08:26:14 -0300 Subject: [PATCH 033/172] Fix HTML export of table with gutter (#5920) --- .../typst-library/src/layout/grid/resolve.rs | 21 +++++++++++---- crates/typst-library/src/model/table.rs | 2 +- tests/ref/html/col-gutter-table.html | 26 ++++++++++++++++++ tests/ref/html/col-row-gutter-table.html | 26 ++++++++++++++++++ tests/ref/html/row-gutter-table.html | 26 ++++++++++++++++++ tests/suite/layout/grid/html.typ | 27 +++++++++++++++++++ 6 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 tests/ref/html/col-gutter-table.html create mode 100644 tests/ref/html/col-row-gutter-table.html create mode 100644 tests/ref/html/row-gutter-table.html diff --git a/crates/typst-library/src/layout/grid/resolve.rs b/crates/typst-library/src/layout/grid/resolve.rs index f6df57a37..762f94ed0 100644 --- a/crates/typst-library/src/layout/grid/resolve.rs +++ b/crates/typst-library/src/layout/grid/resolve.rs @@ -1526,11 +1526,7 @@ impl<'a> CellGrid<'a> { self.entry(x, y).map(|entry| match entry { Entry::Cell(_) => Axes::new(x, y), Entry::Merged { parent } => { - let c = if self.has_gutter { - 1 + self.cols.len() / 2 - } else { - self.cols.len() - }; + let c = self.non_gutter_column_count(); let factor = if self.has_gutter { 2 } else { 1 }; Axes::new(factor * (*parent % c), factor * (*parent / c)) } @@ -1602,6 +1598,21 @@ impl<'a> CellGrid<'a> { cell.rowspan.get() } } + + #[inline] + pub fn non_gutter_column_count(&self) -> usize { + if self.has_gutter { + // Calculation: With gutters, we have + // 'cols = 2 * (non-gutter cols) - 1', since there is a gutter + // column between each regular column. Therefore, + // 'floor(cols / 2)' will be equal to + // 'floor(non-gutter cols - 1/2) = non-gutter-cols - 1', + // so 'non-gutter cols = 1 + floor(cols / 2)'. + 1 + self.cols.len() / 2 + } else { + self.cols.len() + } + } } /// Given a cell's requested x and y, the vector with the resolved cell diff --git a/crates/typst-library/src/model/table.rs b/crates/typst-library/src/model/table.rs index 82c1cc08b..6f4461bd4 100644 --- a/crates/typst-library/src/model/table.rs +++ b/crates/typst-library/src/model/table.rs @@ -282,7 +282,7 @@ fn show_cell_html(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content { fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content { let elem = |tag, body| HtmlElem::new(tag).with_body(Some(body)).pack(); - let mut rows: Vec<_> = grid.entries.chunks(grid.cols.len()).collect(); + let mut rows: Vec<_> = grid.entries.chunks(grid.non_gutter_column_count()).collect(); let tr = |tag, row: &[Entry]| { let row = row diff --git a/tests/ref/html/col-gutter-table.html b/tests/ref/html/col-gutter-table.html new file mode 100644 index 000000000..54170f534 --- /dev/null +++ b/tests/ref/html/col-gutter-table.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + +
abc
def
ghi
+ + diff --git a/tests/ref/html/col-row-gutter-table.html b/tests/ref/html/col-row-gutter-table.html new file mode 100644 index 000000000..54170f534 --- /dev/null +++ b/tests/ref/html/col-row-gutter-table.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + +
abc
def
ghi
+ + diff --git a/tests/ref/html/row-gutter-table.html b/tests/ref/html/row-gutter-table.html new file mode 100644 index 000000000..54170f534 --- /dev/null +++ b/tests/ref/html/row-gutter-table.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + +
abc
def
ghi
+ + diff --git a/tests/suite/layout/grid/html.typ b/tests/suite/layout/grid/html.typ index 2a7dfc2ce..10345cb06 100644 --- a/tests/suite/layout/grid/html.typ +++ b/tests/suite/layout/grid/html.typ @@ -30,3 +30,30 @@ [row], ), ) + +--- col-gutter-table html --- +#table( + columns: 3, + column-gutter: 3pt, + [a], [b], [c], + [d], [e], [f], + [g], [h], [i] +) + +--- row-gutter-table html --- +#table( + columns: 3, + row-gutter: 3pt, + [a], [b], [c], + [d], [e], [f], + [g], [h], [i] +) + +--- col-row-gutter-table html --- +#table( + columns: 3, + gutter: 3pt, + [a], [b], [c], + [d], [e], [f], + [g], [h], [i] +) From 55bc5f4c940c86377f1ffe25b42fdb01a6827358 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 23 Feb 2025 11:28:24 +0000 Subject: [PATCH 034/172] Make math shorthands noncontinuable (#5925) --- crates/typst-syntax/src/parser.rs | 9 +++++---- tests/ref/math-shorthands-noncontinuable.png | Bin 0 -> 475 bytes tests/suite/math/syntax.typ | 5 +++++ 3 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 tests/ref/math-shorthands-noncontinuable.png diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index e187212da..c5d13c8b3 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -271,10 +271,11 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { } SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::MathShorthand => { - continuable = matches!( - math_class(p.current_text()), - None | Some(MathClass::Alphabetic) - ); + continuable = !p.at(SyntaxKind::MathShorthand) + && matches!( + math_class(p.current_text()), + None | Some(MathClass::Alphabetic) + ); if !maybe_delimited(p) { p.eat(); } diff --git a/tests/ref/math-shorthands-noncontinuable.png b/tests/ref/math-shorthands-noncontinuable.png new file mode 100644 index 0000000000000000000000000000000000000000..2f1ad1d14e0ebda63769157fe6a64631dfe58a31 GIT binary patch literal 475 zcmV<10VMv3P)LBEL@~mBuQWyMo>h%CY9761u02MG_@Qx zNm5Z8=fpv+UoiJT-FGuDvw0Smd+zA~qI7VW!yM)?CG6I?KurPr7jBGuE$I%VUVB2D(T@AvTS9$n7S&jjwZk@Yt{W{Hq;&9BN(G8cHT|SwLGrPxe zP=(}Hk1U(3c-niIlHzCF1FwJy%pZIKrjfx&3d5s9fAkE?u2W_@GSiPaKVXVkjKqFc zUs^FK+Sf_)K$--$8)y6^F!)?R0^j&+&lGTQePXOa0oS0ympu;uF}NYrp+Es2nVj_j z3RoOTxR!_(`ju-EcqGtSWd>hWerhD<9tURd%c|j{Rrhx`^UA{)jyrp^<@7qS*~$#= zv5?eSw%SmpD+~Z~9@9`5mS9k9)#A1`h2N?PQ&JcXOI_f_BI-BtEJvo=GJ6=ra{fs) ztDQznf<0WXV2Gc^=wwtNfqmKO0tpPZERew3w}%h~?DLK5`Lf4h4s-Y)z~4s$EOM5) RtCj!&002ovPDHLkV1k2W)8YUC literal 0 HcmV?d00001 diff --git a/tests/suite/math/syntax.typ b/tests/suite/math/syntax.typ index cd1124c37..7091d908c 100644 --- a/tests/suite/math/syntax.typ +++ b/tests/suite/math/syntax.typ @@ -13,6 +13,11 @@ $ underline(f' : NN -> RR) \ 1 - 0 thick &..., ) $ +--- math-shorthands-noncontinuable --- +// Test that shorthands are not continuable. +$ x >=(y) / z \ + x >= (y) / z $ + --- math-common-symbols --- // Test common symbols. $ dot \ dots \ ast \ tilde \ star $ From 56f4fa2b4d4d772c5b19c9842419dcc4e078744b Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Sun, 23 Feb 2025 12:31:28 +0100 Subject: [PATCH 035/172] Documentation improvements (#5888) --- crates/typst-library/src/foundations/symbol.rs | 1 + crates/typst-library/src/visualize/color.rs | 2 +- crates/typst-library/src/visualize/gradient.rs | 11 +++++------ docs/reference/groups.yml | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/typst-library/src/foundations/symbol.rs b/crates/typst-library/src/foundations/symbol.rs index 8a80506fe..2c391ee4c 100644 --- a/crates/typst-library/src/foundations/symbol.rs +++ b/crates/typst-library/src/foundations/symbol.rs @@ -21,6 +21,7 @@ use crate::foundations::{ /// be accessed using [field access notation]($scripting/#fields): /// /// - General symbols are defined in the [`sym` module]($category/symbols/sym) +/// and are accessible without the `sym.` prefix in math mode. /// - Emoji are defined in the [`emoji` module]($category/symbols/emoji) /// /// Moreover, you can define custom symbols with this type's constructor diff --git a/crates/typst-library/src/visualize/color.rs b/crates/typst-library/src/visualize/color.rs index b14312513..20b0f5719 100644 --- a/crates/typst-library/src/visualize/color.rs +++ b/crates/typst-library/src/visualize/color.rs @@ -130,7 +130,7 @@ static TO_SRGB: LazyLock = LazyLock::new(|| { /// /// # Predefined color maps /// Typst also includes a number of preset color maps that can be used for -/// [gradients]($gradient.linear). These are simply arrays of colors defined in +/// [gradients]($gradient/#stops). These are simply arrays of colors defined in /// the module `color.map`. /// /// ```example diff --git a/crates/typst-library/src/visualize/gradient.rs b/crates/typst-library/src/visualize/gradient.rs index d6530dd09..1a723a9f5 100644 --- a/crates/typst-library/src/visualize/gradient.rs +++ b/crates/typst-library/src/visualize/gradient.rs @@ -70,6 +70,9 @@ use crate::visualize::{Color, ColorSpace, WeightedColor}; /// the offsets when defining a gradient. In this case, Typst will space all /// stops evenly. /// +/// Typst predefines color maps that you can use as stops. See the +/// [`color`]($color/#predefined-color-maps) documentation for more details. +/// /// # Relativeness /// The location of the `{0%}` and `{100%}` stops depends on the dimensions /// of a container. This container can either be the shape that it is being @@ -157,10 +160,6 @@ use crate::visualize::{Color, ColorSpace, WeightedColor}; /// ) /// ``` /// -/// # Presets -/// Typst predefines color maps that you can use with your gradients. See the -/// [`color`]($color/#predefined-color-maps) documentation for more details. -/// /// # Note on file sizes /// /// Gradients can be quite large, especially if they have many stops. This is @@ -288,7 +287,7 @@ impl Gradient { /// )), /// ) /// ``` - #[func] + #[func(title = "Radial Gradient")] fn radial( span: Span, /// The color [stops](#stops) of the gradient. @@ -402,7 +401,7 @@ impl Gradient { /// )), /// ) /// ``` - #[func] + #[func(title = "Conic Gradient")] pub fn conic( span: Span, /// The color [stops](#stops) of the gradient. diff --git a/docs/reference/groups.yml b/docs/reference/groups.yml index 961d675dc..8fea3a1f2 100644 --- a/docs/reference/groups.yml +++ b/docs/reference/groups.yml @@ -170,8 +170,8 @@ category: symbols path: ["emoji"] details: | - Named emoji. + Named emojis. For example, `#emoji.face` produces the 😀 emoji. If you frequently use certain emojis, you can also import them from the `emoji` module (`[#import - emoji: face]`) to use them without the `#emoji.` prefix. + emoji: face]`) to use them without the `emoji.` prefix. From ebe25432641a729780578a2440eaf9fb07c80e38 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 24 Feb 2025 12:17:31 +0100 Subject: [PATCH 036/172] Fix comparison of `Func` and `NativeFuncData` (#5943) --- crates/typst-library/src/foundations/func.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/foundations/func.rs b/crates/typst-library/src/foundations/func.rs index 3ed1562f6..66c6b70a5 100644 --- a/crates/typst-library/src/foundations/func.rs +++ b/crates/typst-library/src/foundations/func.rs @@ -437,10 +437,10 @@ impl PartialEq for Func { } } -impl PartialEq<&NativeFuncData> for Func { - fn eq(&self, other: &&NativeFuncData) -> bool { +impl PartialEq<&'static NativeFuncData> for Func { + fn eq(&self, other: &&'static NativeFuncData) -> bool { match &self.repr { - Repr::Native(native) => native.function == other.function, + Repr::Native(native) => *native == Static(*other), _ => false, } } From 69c3f957051358eff961addbcae4ff02448513dc Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 24 Feb 2025 13:28:01 +0100 Subject: [PATCH 037/172] Bump MSRV to 1.83 and Rust in CI to 1.85 (#5946) --- .github/workflows/ci.yml | 6 +++--- .github/workflows/release.yml | 2 +- Cargo.toml | 2 +- crates/typst-cli/src/compile.rs | 2 +- crates/typst-ide/src/complete.rs | 2 +- crates/typst-layout/src/grid/layouter.rs | 8 ++++---- crates/typst-layout/src/grid/lines.rs | 2 +- crates/typst-layout/src/grid/rowspans.rs | 2 +- crates/typst-layout/src/inline/line.rs | 4 ++-- crates/typst-layout/src/inline/linebreak.rs | 4 ++-- crates/typst-layout/src/inline/shaping.rs | 6 +++--- crates/typst-library/src/foundations/symbol.rs | 2 +- crates/typst-library/src/layout/grid/resolve.rs | 2 +- crates/typst-library/src/text/font/book.rs | 2 +- crates/typst-library/src/text/shift.rs | 2 +- crates/typst-pdf/src/outline.rs | 4 ++-- crates/typst-syntax/src/node.rs | 2 +- crates/typst-syntax/src/package.rs | 4 ++-- crates/typst-utils/src/scalar.rs | 13 +------------ flake.lock | 6 +++--- flake.nix | 2 +- tests/src/collect.rs | 4 ++-- tests/src/run.rs | 2 +- 23 files changed, 37 insertions(+), 48 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01b3e8c3a..9f0ada9f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.83.0 + - uses: dtolnay/rust-toolchain@1.85.0 - uses: Swatinem/rust-cache@v2 - run: cargo test --workspace --no-run - run: cargo test --workspace --no-fail-fast @@ -59,7 +59,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.83.0 + - uses: dtolnay/rust-toolchain@1.85.0 with: components: clippy, rustfmt - uses: Swatinem/rust-cache@v2 @@ -73,7 +73,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.80.0 + - uses: dtolnay/rust-toolchain@1.83.0 - uses: Swatinem/rust-cache@v2 - run: cargo check --workspace diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5be6bfa2c..0d235aec5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,7 +44,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.83.0 + - uses: dtolnay/rust-toolchain@1.85.0 with: target: ${{ matrix.target }} diff --git a/Cargo.toml b/Cargo.toml index 198aff3c6..36195230e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "2" [workspace.package] version = "0.13.0" -rust-version = "1.80" # also change in ci.yml +rust-version = "1.83" # also change in ci.yml authors = ["The Typst Project Developers"] edition = "2021" homepage = "https://typst.app" diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index 2b6a7d820..ae71e298c 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -350,7 +350,7 @@ fn export_image( .iter() .enumerate() .filter(|(i, _)| { - config.pages.as_ref().map_or(true, |exported_page_ranges| { + config.pages.as_ref().is_none_or(|exported_page_ranges| { exported_page_ranges.includes_page_index(*i) }) }) diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index 564b97bd7..e3dcc442e 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -1455,7 +1455,7 @@ impl<'a> CompletionContext<'a> { let mut defined = BTreeMap::>::new(); named_items(self.world, self.leaf.clone(), |item| { let name = item.name(); - if !name.is_empty() && item.value().as_ref().map_or(true, filter) { + if !name.is_empty() && item.value().as_ref().is_none_or(filter) { defined.insert(name.clone(), item.value()); } diff --git a/crates/typst-layout/src/grid/layouter.rs b/crates/typst-layout/src/grid/layouter.rs index 1f9cf6796..af47ff72f 100644 --- a/crates/typst-layout/src/grid/layouter.rs +++ b/crates/typst-layout/src/grid/layouter.rs @@ -1377,7 +1377,7 @@ impl<'a> GridLayouter<'a> { .footer .as_ref() .and_then(Repeatable::as_repeated) - .map_or(true, |footer| footer.start != header.end) + .is_none_or(|footer| footer.start != header.end) && self.lrows.last().is_some_and(|row| row.index() < header.end) && !in_last_with_offset( self.regions, @@ -1446,7 +1446,7 @@ impl<'a> GridLayouter<'a> { .iter_mut() .filter(|rowspan| (rowspan.y..rowspan.y + rowspan.rowspan).contains(&y)) .filter(|rowspan| { - rowspan.max_resolved_row.map_or(true, |max_row| y > max_row) + rowspan.max_resolved_row.is_none_or(|max_row| y > max_row) }) { // If the first region wasn't defined yet, it will have the @@ -1494,7 +1494,7 @@ impl<'a> GridLayouter<'a> { // laid out at the first frame of the row). // Any rowspans ending before this row are laid out even // on this row's first frame. - if laid_out_footer_start.map_or(true, |footer_start| { + if laid_out_footer_start.is_none_or(|footer_start| { // If this is a footer row, then only lay out this rowspan // if the rowspan is contained within the footer. y < footer_start || rowspan.y >= footer_start @@ -1580,5 +1580,5 @@ pub(super) fn points( /// our case, headers). pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool { regions.backlog.is_empty() - && regions.last.map_or(true, |height| regions.size.y + offset == height) + && regions.last.is_none_or(|height| regions.size.y + offset == height) } diff --git a/crates/typst-layout/src/grid/lines.rs b/crates/typst-layout/src/grid/lines.rs index 1227953d1..7549673f1 100644 --- a/crates/typst-layout/src/grid/lines.rs +++ b/crates/typst-layout/src/grid/lines.rs @@ -463,7 +463,7 @@ pub fn hline_stroke_at_column( // region, we have the last index, and (as a failsafe) we don't have the // last row of cells above us. let use_bottom_border_stroke = !in_last_region - && local_top_y.map_or(true, |top_y| top_y + 1 != grid.rows.len()) + && local_top_y.is_none_or(|top_y| top_y + 1 != grid.rows.len()) && y == grid.rows.len(); let bottom_y = if use_bottom_border_stroke { grid.rows.len().saturating_sub(1) } else { y }; diff --git a/crates/typst-layout/src/grid/rowspans.rs b/crates/typst-layout/src/grid/rowspans.rs index 5039695d8..21992ed02 100644 --- a/crates/typst-layout/src/grid/rowspans.rs +++ b/crates/typst-layout/src/grid/rowspans.rs @@ -588,7 +588,7 @@ impl GridLayouter<'_> { measurement_data: &CellMeasurementData<'_>, ) -> bool { if sizes.len() <= 1 - && sizes.first().map_or(true, |&first_frame_size| { + && sizes.first().is_none_or(|&first_frame_size| { first_frame_size <= measurement_data.height_in_this_region }) { diff --git a/crates/typst-layout/src/inline/line.rs b/crates/typst-layout/src/inline/line.rs index bd08f30ef..659d33f4a 100644 --- a/crates/typst-layout/src/inline/line.rs +++ b/crates/typst-layout/src/inline/line.rs @@ -154,7 +154,7 @@ pub fn line<'a>( let mut items = collect_items(engine, p, range, trim); // Add a hyphen at the line start, if a previous dash should be repeated. - if pred.map_or(false, |pred| should_repeat_hyphen(pred, full)) { + if pred.is_some_and(|pred| should_repeat_hyphen(pred, full)) { if let Some(shaped) = items.first_text_mut() { shaped.prepend_hyphen(engine, p.config.fallback); } @@ -406,7 +406,7 @@ fn should_repeat_hyphen(pred_line: &Line, text: &str) -> bool { // // See § 4.1.1.1.2.e on the "Ortografía de la lengua española" // https://www.rae.es/ortografía/como-signo-de-división-de-palabras-a-final-de-línea - Lang::SPANISH => text.chars().next().map_or(false, |c| !c.is_uppercase()), + Lang::SPANISH => text.chars().next().is_some_and(|c| !c.is_uppercase()), _ => false, } diff --git a/crates/typst-layout/src/inline/linebreak.rs b/crates/typst-layout/src/inline/linebreak.rs index a9f21188b..31512604f 100644 --- a/crates/typst-layout/src/inline/linebreak.rs +++ b/crates/typst-layout/src/inline/linebreak.rs @@ -290,7 +290,7 @@ fn linebreak_optimized_bounded<'a>( } // If this attempt is better than what we had before, take it! - if best.as_ref().map_or(true, |best| best.total >= total) { + if best.as_ref().is_none_or(|best| best.total >= total) { best = Some(Entry { pred: pred_index, total, line: attempt, end }); } } @@ -423,7 +423,7 @@ fn linebreak_optimized_approximate( let total = pred.total + line_cost; // If this attempt is better than what we had before, take it! - if best.as_ref().map_or(true, |best| best.total >= total) { + if best.as_ref().is_none_or(|best| best.total >= total) { best = Some(Entry { pred: pred_index, total, diff --git a/crates/typst-layout/src/inline/shaping.rs b/crates/typst-layout/src/inline/shaping.rs index b688981ae..159619eb3 100644 --- a/crates/typst-layout/src/inline/shaping.rs +++ b/crates/typst-layout/src/inline/shaping.rs @@ -465,7 +465,7 @@ impl<'a> ShapedText<'a> { None }; let mut chain = families(self.styles) - .filter(|family| family.covers().map_or(true, |c| c.is_match("-"))) + .filter(|family| family.covers().is_none_or(|c| c.is_match("-"))) .map(|family| book.select(family.as_str(), self.variant)) .chain(fallback_func.iter().map(|f| f())) .flatten(); @@ -570,7 +570,7 @@ impl<'a> ShapedText<'a> { // for the next line. let dec = if ltr { usize::checked_sub } else { usize::checked_add }; while let Some(next) = dec(idx, 1) { - if self.glyphs.get(next).map_or(true, |g| g.range.start != text_index) { + if self.glyphs.get(next).is_none_or(|g| g.range.start != text_index) { break; } idx = next; @@ -812,7 +812,7 @@ fn shape_segment<'a>( .nth(1) .map(|(i, _)| offset + i) .unwrap_or(text.len()); - covers.map_or(true, |cov| cov.is_match(&text[offset..end])) + covers.is_none_or(|cov| cov.is_match(&text[offset..end])) }; // Collect the shaped glyphs, doing fallback and shaping parts again with diff --git a/crates/typst-library/src/foundations/symbol.rs b/crates/typst-library/src/foundations/symbol.rs index 2c391ee4c..50fcfb403 100644 --- a/crates/typst-library/src/foundations/symbol.rs +++ b/crates/typst-library/src/foundations/symbol.rs @@ -411,7 +411,7 @@ fn find<'a>( } let score = (matching, Reverse(total)); - if best_score.map_or(true, |b| score > b) { + if best_score.is_none_or(|b| score > b) { best = Some(candidate.1); best_score = Some(score); } diff --git a/crates/typst-library/src/layout/grid/resolve.rs b/crates/typst-library/src/layout/grid/resolve.rs index 762f94ed0..08d0130da 100644 --- a/crates/typst-library/src/layout/grid/resolve.rs +++ b/crates/typst-library/src/layout/grid/resolve.rs @@ -1387,7 +1387,7 @@ impl<'a> CellGrid<'a> { // Include the gutter right before the footer, unless there is // none, or the gutter is already included in the header (no // rows between the header and the footer). - if header_end.map_or(true, |header_end| header_end != footer.start) { + if header_end != Some(footer.start) { footer.start = footer.start.saturating_sub(1); } } diff --git a/crates/typst-library/src/text/font/book.rs b/crates/typst-library/src/text/font/book.rs index 23e27f64c..9f8acce87 100644 --- a/crates/typst-library/src/text/font/book.rs +++ b/crates/typst-library/src/text/font/book.rs @@ -160,7 +160,7 @@ impl FontBook { current.variant.weight.distance(variant.weight), ); - if best_key.map_or(true, |b| key < b) { + if best_key.is_none_or(|b| key < b) { best = Some(id); best_key = Some(key); } diff --git a/crates/typst-library/src/text/shift.rs b/crates/typst-library/src/text/shift.rs index 3eec0758b..dbf1be8a1 100644 --- a/crates/typst-library/src/text/shift.rs +++ b/crates/typst-library/src/text/shift.rs @@ -159,7 +159,7 @@ fn is_shapable(engine: &Engine, text: &str, styles: StyleChain) -> bool { { let covers = family.covers(); return text.chars().all(|c| { - covers.map_or(true, |cov| cov.is_match(c.encode_utf8(&mut [0; 4]))) + covers.is_none_or(|cov| cov.is_match(c.encode_utf8(&mut [0; 4]))) && font.ttf().glyph_index(c).is_some() }); } diff --git a/crates/typst-pdf/src/outline.rs b/crates/typst-pdf/src/outline.rs index ff72eb86a..eff1182c1 100644 --- a/crates/typst-pdf/src/outline.rs +++ b/crates/typst-pdf/src/outline.rs @@ -70,7 +70,7 @@ pub(crate) fn write_outline( // (not exceeding whichever is the most restrictive depth limit // of those two). while children.last().is_some_and(|last| { - last_skipped_level.map_or(true, |l| last.level < l) + last_skipped_level.is_none_or(|l| last.level < l) && last.level < leaf.level }) { children = &mut children.last_mut().unwrap().children; @@ -83,7 +83,7 @@ pub(crate) fn write_outline( // needed, following the usual rules listed above. last_skipped_level = None; children.push(leaf); - } else if last_skipped_level.map_or(true, |l| leaf.level < l) { + } else if last_skipped_level.is_none_or(|l| leaf.level < l) { // Only the topmost / lowest-level skipped heading matters when you // have consecutive skipped headings (since none of them are being // added to the bookmark tree), hence the condition above. diff --git a/crates/typst-syntax/src/node.rs b/crates/typst-syntax/src/node.rs index b7e1809d7..fde2eaca0 100644 --- a/crates/typst-syntax/src/node.rs +++ b/crates/typst-syntax/src/node.rs @@ -753,7 +753,7 @@ impl<'a> LinkedNode<'a> { // sibling's span number is larger than the target span's number. if children .peek() - .map_or(true, |next| next.span().number() > span.number()) + .is_none_or(|next| next.span().number() > span.number()) { if let Some(found) = child.find(span) { return Some(found); diff --git a/crates/typst-syntax/src/package.rs b/crates/typst-syntax/src/package.rs index 387057f37..aa537863d 100644 --- a/crates/typst-syntax/src/package.rs +++ b/crates/typst-syntax/src/package.rs @@ -327,8 +327,8 @@ impl PackageVersion { /// missing in the bound are ignored. pub fn matches_eq(&self, bound: &VersionBound) -> bool { self.major == bound.major - && bound.minor.map_or(true, |minor| self.minor == minor) - && bound.patch.map_or(true, |patch| self.patch == patch) + && bound.minor.is_none_or(|minor| self.minor == minor) + && bound.patch.is_none_or(|patch| self.patch == patch) } /// Performs a `>` match with the given version bound. The match only diff --git a/crates/typst-utils/src/scalar.rs b/crates/typst-utils/src/scalar.rs index 4036c2310..6d84fbfdf 100644 --- a/crates/typst-utils/src/scalar.rs +++ b/crates/typst-utils/src/scalar.rs @@ -28,7 +28,7 @@ impl Scalar { /// /// If the value is NaN, then it is set to `0.0` in the result. pub const fn new(x: f64) -> Self { - Self(if is_nan(x) { 0.0 } else { x }) + Self(if x.is_nan() { 0.0 } else { x }) } /// Gets the value of this [`Scalar`]. @@ -37,17 +37,6 @@ impl Scalar { } } -// We have to detect NaNs this way since `f64::is_nan` isn’t const -// on stable yet: -// ([tracking issue](https://github.com/rust-lang/rust/issues/57241)) -#[allow(clippy::unusual_byte_groupings)] -const fn is_nan(x: f64) -> bool { - // Safety: all bit patterns are valid for u64, and f64 has no padding bits. - // We cannot use `f64::to_bits` because it is not const. - let x_bits = unsafe { std::mem::transmute::(x) }; - (x_bits << 1 >> (64 - 12 + 1)) == 0b0_111_1111_1111 && (x_bits << 12) != 0 -} - impl Numeric for Scalar { fn zero() -> Self { Self(0.0) diff --git a/flake.lock b/flake.lock index c02466422..ad47d29cd 100644 --- a/flake.lock +++ b/flake.lock @@ -112,13 +112,13 @@ "rust-manifest": { "flake": false, "locked": { - "narHash": "sha256-Yqu2/i9170R7pQhvOCR1f5SyFr7PcFbO6xcMr9KWruQ=", + "narHash": "sha256-irgHsBXecwlFSdmP9MfGP06Cbpca2QALJdbN4cymcko=", "type": "file", - "url": "https://static.rust-lang.org/dist/channel-rust-1.83.0.toml" + "url": "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml" }, "original": { "type": "file", - "url": "https://static.rust-lang.org/dist/channel-rust-1.83.0.toml" + "url": "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml" } }, "systems": { diff --git a/flake.nix b/flake.nix index abdad27aa..6938f6e57 100644 --- a/flake.nix +++ b/flake.nix @@ -10,7 +10,7 @@ inputs.nixpkgs.follows = "nixpkgs"; }; rust-manifest = { - url = "https://static.rust-lang.org/dist/channel-rust-1.83.0.toml"; + url = "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml"; flake = false; }; }; diff --git a/tests/src/collect.rs b/tests/src/collect.rs index c6deba77b..33f4f7366 100644 --- a/tests/src/collect.rs +++ b/tests/src/collect.rs @@ -149,7 +149,7 @@ impl Collector { for entry in walkdir::WalkDir::new(crate::SUITE_PATH).sort_by_file_name() { let entry = entry.unwrap(); let path = entry.path(); - if !path.extension().is_some_and(|ext| ext == "typ") { + if path.extension().is_none_or(|ext| ext != "typ") { continue; } @@ -168,7 +168,7 @@ impl Collector { for entry in walkdir::WalkDir::new(crate::REF_PATH).sort_by_file_name() { let entry = entry.unwrap(); let path = entry.path(); - if !path.extension().is_some_and(|ext| ext == "png") { + if path.extension().is_none_or(|ext| ext != "png") { continue; } diff --git a/tests/src/run.rs b/tests/src/run.rs index f9a3c0434..4d08362cf 100644 --- a/tests/src/run.rs +++ b/tests/src/run.rs @@ -161,7 +161,7 @@ impl<'a> Runner<'a> { // Compare against reference output if available. // Test that is ok doesn't need to be updated. - if ref_data.as_ref().map_or(false, |r| D::matches(&live, r)) { + if ref_data.as_ref().is_ok_and(|r| D::matches(&live, r)) { return; } From 81efc82d3c0f7ccbcb40959ac8bddeca49e4c9f8 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 24 Feb 2025 16:05:36 +0000 Subject: [PATCH 038/172] Fix math accent base height calculation (#5941) --- crates/typst-layout/src/math/accent.rs | 4 ++-- tests/ref/gradient-math-misc.png | Bin 2993 -> 2993 bytes tests/ref/issue-math-realize-scripting.png | Bin 2610 -> 2605 bytes tests/ref/math-accent-align.png | Bin 614 -> 625 bytes tests/ref/math-accent-bounds.png | Bin 327 -> 327 bytes tests/ref/math-accent-dotless.png | Bin 1026 -> 1024 bytes tests/ref/math-accent-func.png | Bin 284 -> 284 bytes tests/ref/math-accent-sym-call.png | Bin 926 -> 930 bytes tests/ref/math-spacing-decorated.png | Bin 2385 -> 2375 bytes 9 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs index 951870d68..f2dfa2c45 100644 --- a/crates/typst-layout/src/math/accent.rs +++ b/crates/typst-layout/src/math/accent.rs @@ -34,7 +34,7 @@ pub fn layout_accent( // Try to replace accent glyph with flattened variant. let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); - if base.height() > flattened_base_height { + if base.ascent() > flattened_base_height { glyph.make_flattened_accent_form(ctx); } @@ -50,7 +50,7 @@ pub fn layout_accent( // 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 gap = -accent.descent() - base.ascent().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); diff --git a/tests/ref/gradient-math-misc.png b/tests/ref/gradient-math-misc.png index acf14c6fe20e84146001d4117700a949a4fda6d1..13f5c27b38c0a80e502068b2784146ae58b838ed 100644 GIT binary patch delta 2915 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 N002ovPDHLkV1m<1w@d&4 diff --git a/tests/ref/issue-math-realize-scripting.png b/tests/ref/issue-math-realize-scripting.png index ee2d4cdf7a64d95e4bd8fffe538a89de1bde1ae4..7d721ed776199bd6611d1178bb7582c1dd6fe7c8 100644 GIT binary patch delta 2596 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 LNkvXXu0mjf*}K?_ diff --git a/tests/ref/math-accent-align.png b/tests/ref/math-accent-align.png index 84e8dc8ccda955a6b747d1462610b51219f43f43..efc66ec3faf784dcc17fcb8d3fbcb7a58c151773 100644 GIT binary patch delta 600 zcmV-e0;m1v1n~rrB!4(bL_t(|+U?ihOH*MS$8rBN$w3kfBPv$eMcov2VL?y{MM8Gd z57tG+P#LwUIa5}x)1Z##bk5AS;bf#!r!0#&JL+oI**ZJt6W_~c-3eb0>hr#O_};vD z9u8kP4RELi2}@YQ65cUb>$&>GV*>U>jQR%P-C18S!VZ7Ng@1IO4#^ox1Mso6P;xWF z)B#ihSiJ%23r(7!o@a+U7MiOeIutdk0wCwcnPql(Tuls_5bElDzUahPZ6D16PQZo1 zDY-x-yvt$Ra0piK?60S;1Rf6Gt^qLqP!@pf{ngoic6h|RqP_-T`5sdl0MU;4#$9HZ zI%tB*%~L<=*ne#QNU*~LS`#7wthaBdJk47066|o-i$JdiF@N-{+*}ft@HXI$?hn(B zz$jG>N8s4#oC-00c-~-##}}^$+D*Xn-lNj=_$R8K1Up>Dkxhu*>c=6I9WGjp$UwO9 zsg*5vmK{FXb1pOkA*IjXs<6YbQ2T#m%grU>T?`lW@_&+*L0vXKocq2$_csAC^V%MM z>R)hCJgvkbG!Rw7{BUK< mETOK_Z@IZ7EMW=nVE+J61?9wOxq%M=0000@y^($#u_1zm+% zU4*?s7G`a@&AB-$v&h;&a;CDE7F1JbTFoVF%9^cBx3hD;@m!wW6+9kf&-Wkra9;R5 z@U&o2Eeck!f)%`FaK7KsdrSi(@=o*R>~LN6 zq%jrs`p-wyfGAkO{|tXvT$!r8q@e}*aG_Gmv5YybPk%m4ZA1S}i{HEd0uQ^@uw>x;^K>s?!o;_IoS)CbKf?i6j{pu)Hwcu$%!1JYD@< J);T3K0RVa;6*B+; delta 53 zcmX@kbew6z2hrCT?4_2y>JfK%bbHQ$Ri`Dy>}_k?CbKf?iKI9dFkf_)7H0qgPgg&e IbxsLQ0CcJp7XSbN diff --git a/tests/ref/math-accent-dotless.png b/tests/ref/math-accent-dotless.png index 81eb4fa2bd7b48cba24fa3e1c8d6beda09bb6506..389ceb634aad124cc19196123c8ea188a72a0633 100644 GIT binary patch delta 1002 zcmVCXn9AOw3HbC%@)O|M?u=yLe9RO2H|3+iKUIDXjeaBY#OPp@e5UJcz12iXlDm zA-RGw>lT-6aDPQ+rU#X9caat;a~o`z$u4bE=^-+Nsx>^#}PaW7~pC^5tMD}(lY;D7d!kM2=(2TCgkMhb!Qj!O}a z2H@(cc;N`l?XGb?JO_dO$D+kU0)M>-9WPx|3DdhuuN~(9W=;_$ox znC}j#fGO#e89pWxerIQRGc%kB6MNad9ennP8hGEDw-LBnO{*JLGqLn z5d-eF3A~mIP@NM8OpCICa$<)^=1*hN?`~t!M Y0Al&n0a`kXD*ylh07*qoM6N<$g5P%2D*ylh delta 1004 zcmVpnSbg;BivD_N6K0T_;x!|MRqw-ah_GybnFYHL&Y-nmojC2*{5Wi&jGA_ z`Lb4c>d1g>JVVCz2}q4)NJZVfvcY%VNNoZ$d^8v7z<3a&m=q@cuH_(-~R?Le-F|~6~J^2$x~2djYBil z0Mr`*R;oh3O+k2Q=(;XBjQ&@dr6Y^LeCaI{K)g7=I0V30Q&UrfwN5(a=`RLAHIzIA zp!AB&!a*CbIy!2o)c_M?U2)&qF~)!HUWDYZy?^RDQg#^NyKP9f-`@|?Pq|(|a(*0_ z?Ro>rd+ohW0QPQ0%2WZyUO%7#HuoQwjik#2G5~2CNI%rgfTZ7X4x}8Uj&M#n0E1~r z=>qfh4h=A8+hhjz$*jJ$IULPeO@xWP=-3E8xkn4UeaTk`T&$qc1B+SMde-1N9|GSo z1AkCf0o<4oI9LVH_ZrR?1RggE>}dq@^EJTncL*EOd4!2`44 z_rQD2(|yP9dwM`@0K7wD~l&E{xDzw0#8>zmvv4FO#o$i9smFU delta 69 zcmbQkG>2(|yBc@TM`@0K7wz|+;wWt~$(698sT9m@a! diff --git a/tests/ref/math-accent-sym-call.png b/tests/ref/math-accent-sym-call.png index 0837a86c9e861a960cbca4d157ea7e719b54cd0f..609197f3c3da71d9f4b4efa72b92bcc64311b935 100644 GIT binary patch delta 908 zcmV;719SYI2cid%B!6j1L_t(|+U?cdPg`XG$MMC##Ka44jLDMua>Iqg7~_IVhFRvK zbB3sM5rUh8xj`l{7fet$L4t{Mq@fFRFaaAFAVpxPY|t|3RyMKix{j`ID?0^B*VFSg zd7hI>7Bwc%7B1G$&G{wg;`=;T&n7m=6Pq?jKD@!<3F@gD2)<+#AwBM2 zK=9&K3u`u{m%$W!>&GlWrz1WFz-qO#4M87W^dQtmE7iicQwY&rw~-9%r)dD@8!=rA zU`j@i!ZU_axPNk}yaRx)J#$qE_x7BO0iYdhmZxg8pXf&DZLpNeLmjyEp-T9|$CP>i zD7%{7g0wS^(B|;e05CiF%?I+n&olG|0I|NV=TyQK{S-z3*v`dwkqVLg$9v8LQ2xEA zBn)tIKSzGO3t->7QV>=MpX(cV&B{kN;j{32x7eKol7BmZnkgVs8K=w#1%Wd~JvTmq z#KAvaY`<&mfVv-?&z?~TGkeFQarH7Vp}{~1vcEI~0SBaF!5Tc1ppS=d+|O8nJG1cx z;&#C8j;h>P`We#@@W$a3FsVVX(>lzsfs}P%3S3804rkVnP!w|!TNFbw3``jm!byNf&iZ%x&=Tn zf?=@?0uZ=dxJJ?Z2*L{W2LN+dSECjl+Z^C*1fdF{VpQ7-fYbK^6i3b@S$31tQi598 zb!w`3aS*8=fVF2Btpi~7rz6hg&DX{e241~SJ5u*{UAHNPs~b4FpMaM10iynIiXDzy5op9!~y=;KG!+2{|svnaMwk i`EWj*4?kAnzW^mukOPEx)hPe~002ovP6b4+LSTZ{g0Nix delta 904 zcmV;319$wQ2c8FzB!6W|L_t(|+U?cdPg7?A$8od2WXUdevn(-jFE?CFmSveCCbAf3 zHs?aNIWq{;S)4)~;0#OHrh^fd%t<C+g0$zm zz7@PCBL+)wE^rpA2%fUaXT zBLD>5Zuc7M?i*f>V2b-01IQq2Bm#ke@wmfuMLV}k%JSGb!IR_rq_gsMoHTKIMvA+qZ>lA(RE3cz$7NK-mU z$uLrA+)xUa^?#PO0?@H%vK(P?&&enNiGvODRCSIMHiQec)?#_610$cPgfD(Zu@``n zYuPPGJ7WmVPH!au6FuL5Ebsd=yTLPjAAPp$~7dI|(Fr0DqM(AY2xs#0Po$nF9Nb&mn&B zpI2M%XsuBFv-`y}3SlPZy$P;e1%?vP9fa(q20(X0IvS|NI}Dl2p&NJCG+=%rHbcw- zcsvo68%w`~)C;_QI1LOZAkeP$(PtoKognGgk(9%0{~y7Kxrok*AsNQ{R|;Vzxo|H0 zNZ~ldd4CWCj3*UtuC8(-m8ZBzU(Lx@Sl4bKO-?I^Nv?tPm%+V4*TK}UY1{lrmJ^y5 zQww|k#?+lfn74tH868Sv3I~I;NuDd56)1@f{e@ifKGY{6kt<~{- z8-im;+lA*GN@06rGo~$RgwzO<)l=IsdTt4!A%8>3H%RpcMU5zh{dqcH9l8ZTA%bDG z_5h$?&0nElx*uVQngd|&iVD=igPV2Eh7rmUECY$H0JwdxKw@5GXEJR{;fh+0?nh4~eZYXhBrNR2((~t{ z4u9C^IxqUpt{5J`m6zc3AQ>+(^x86~MwP<8eW#m_f7is=M}Q@@v=z{7Kh!0LivgSt zM@M^l;SFH#vHGLC3_=CKJ5}|E-$Nz5&SgF%b3J^c9!~y?Fa}cmCX)HqG@kn{=EAvf eF8ox5{{vrCkOUezRbl`D002ovPDHLkU;%=IT%-#C diff --git a/tests/ref/math-spacing-decorated.png b/tests/ref/math-spacing-decorated.png index b8846ff0595667a56f7004ac4b0fcc5925670200..2f3c704e5d981ce119ac1e903e154c4a186e1553 100644 GIT binary patch delta 2364 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 From 3744c99b07f97a954a8468bef5fdb08c5c7914d7 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Mon, 24 Feb 2025 17:15:17 +0100 Subject: [PATCH 039/172] Override the default math class of some characters (#5949) --- crates/typst-utils/src/lib.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/typst-utils/src/lib.rs b/crates/typst-utils/src/lib.rs index 34d6a9432..b346a8096 100644 --- a/crates/typst-utils/src/lib.rs +++ b/crates/typst-utils/src/lib.rs @@ -360,6 +360,21 @@ pub fn default_math_class(c: char) -> Option { // https://github.com/typst/typst/pull/5714 '\u{22A5}' => Some(MathClass::Normal), + // Used as a binary connector in linear logic, where it is referred to + // as "par". + // https://github.com/typst/typst/issues/5764 + '⅋' => Some(MathClass::Binary), + + // Those overrides should become the default in the next revision of + // MathClass.txt. + // https://github.com/typst/typst/issues/5764#issuecomment-2632435247 + '⎰' | '⟅' => Some(MathClass::Opening), + '⎱' | '⟆' => Some(MathClass::Closing), + + // Both ∨ and ⟑ are classified as Binary. + // https://github.com/typst/typst/issues/5764 + '⟇' => Some(MathClass::Binary), + c => unicode_math_class::class(c), } } From 36d83c8c092e7984eaa03dbecc1083f49da13129 Mon Sep 17 00:00:00 2001 From: Sharzy Date: Tue, 25 Feb 2025 00:35:13 +0800 Subject: [PATCH 040/172] HTML export: fix elem counting on classify_output (#5910) Co-authored-by: Laurenz --- crates/typst-html/src/lib.rs | 6 +++--- tests/ref/html/html-elem-alone-context.html | 2 ++ tests/suite/html/elem.typ | 7 +++++++ 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 tests/ref/html/html-elem-alone-context.html create mode 100644 tests/suite/html/elem.typ diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs index 25d0cd5d8..236a32544 100644 --- a/crates/typst-html/src/lib.rs +++ b/crates/typst-html/src/lib.rs @@ -307,18 +307,18 @@ fn head_element(info: &DocumentInfo) -> HtmlElement { /// Determine which kind of output the user generated. fn classify_output(mut output: Vec) -> SourceResult { - let len = output.len(); + let count = output.iter().filter(|node| !matches!(node, HtmlNode::Tag(_))).count(); for node in &mut output { let HtmlNode::Element(elem) = node else { continue }; let tag = elem.tag; let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html)); - match (tag, len) { + match (tag, count) { (tag::html, 1) => return Ok(OutputKind::Html(take())), (tag::body, 1) => return Ok(OutputKind::Body(take())), (tag::html | tag::body, _) => bail!( elem.span, "`{}` element must be the only element in the document", - elem.tag + elem.tag, ), _ => {} } diff --git a/tests/ref/html/html-elem-alone-context.html b/tests/ref/html/html-elem-alone-context.html new file mode 100644 index 000000000..69e9da411 --- /dev/null +++ b/tests/ref/html/html-elem-alone-context.html @@ -0,0 +1,2 @@ + + diff --git a/tests/suite/html/elem.typ b/tests/suite/html/elem.typ new file mode 100644 index 000000000..81ab94577 --- /dev/null +++ b/tests/suite/html/elem.typ @@ -0,0 +1,7 @@ +--- html-elem-alone-context html --- +#context html.elem("html") + +--- html-elem-not-alone html --- +// Error: 2-19 `` element must be the only element in the document +#html.elem("html") +Text From 225e845021b9cfb37e6dc719c8bc85ccdc1ff69f Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 25 Feb 2025 12:31:15 +0100 Subject: [PATCH 041/172] Fix introspection of HTML root sibling metadata (#5953) --- crates/typst-html/src/lib.rs | 2 +- .../src/introspection/introspector.rs | 18 +++++++++--------- tests/ref/html/html-elem-metadata.html | 2 ++ tests/suite/html/elem.typ | 8 ++++++++ 4 files changed, 20 insertions(+), 10 deletions(-) create mode 100644 tests/ref/html/html-elem-metadata.html diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs index 236a32544..aa769976e 100644 --- a/crates/typst-html/src/lib.rs +++ b/crates/typst-html/src/lib.rs @@ -83,8 +83,8 @@ fn html_document_impl( )?; let output = handle_list(&mut engine, &mut locator, children.iter().copied())?; + let introspector = Introspector::html(&output); let root = root_element(output, &info)?; - let introspector = Introspector::html(&root); Ok(HtmlDocument { info, root, introspector }) } diff --git a/crates/typst-library/src/introspection/introspector.rs b/crates/typst-library/src/introspection/introspector.rs index 8cbaea891..9751dfcb8 100644 --- a/crates/typst-library/src/introspection/introspector.rs +++ b/crates/typst-library/src/introspection/introspector.rs @@ -10,7 +10,7 @@ use typst_utils::NonZeroExt; use crate::diag::{bail, StrResult}; use crate::foundations::{Content, Label, Repr, Selector}; -use crate::html::{HtmlElement, HtmlNode}; +use crate::html::HtmlNode; use crate::introspection::{Location, Tag}; use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform}; use crate::model::Numbering; @@ -55,8 +55,8 @@ impl Introspector { /// Creates an introspector for HTML. #[typst_macros::time(name = "introspect html")] - pub fn html(root: &HtmlElement) -> Self { - IntrospectorBuilder::new().build_html(root) + pub fn html(output: &[HtmlNode]) -> Self { + IntrospectorBuilder::new().build_html(output) } /// Iterates over all locatable elements. @@ -392,9 +392,9 @@ impl IntrospectorBuilder { } /// Build an introspector for an HTML document. - fn build_html(mut self, root: &HtmlElement) -> Introspector { + fn build_html(mut self, output: &[HtmlNode]) -> Introspector { let mut elems = Vec::new(); - self.discover_in_html(&mut elems, root); + self.discover_in_html(&mut elems, output); self.finalize(elems) } @@ -434,16 +434,16 @@ impl IntrospectorBuilder { } /// Processes the tags in the HTML element. - fn discover_in_html(&mut self, sink: &mut Vec, elem: &HtmlElement) { - for child in &elem.children { - match child { + fn discover_in_html(&mut self, sink: &mut Vec, nodes: &[HtmlNode]) { + for node in nodes { + match node { HtmlNode::Tag(tag) => self.discover_in_tag( sink, tag, Position { page: NonZeroUsize::ONE, point: Point::zero() }, ), HtmlNode::Text(_, _) => {} - HtmlNode::Element(elem) => self.discover_in_html(sink, elem), + HtmlNode::Element(elem) => self.discover_in_html(sink, &elem.children), HtmlNode::Frame(frame) => self.discover_in_frame( sink, frame, diff --git a/tests/ref/html/html-elem-metadata.html b/tests/ref/html/html-elem-metadata.html new file mode 100644 index 000000000..c37a7d2ef --- /dev/null +++ b/tests/ref/html/html-elem-metadata.html @@ -0,0 +1,2 @@ + +Hi diff --git a/tests/suite/html/elem.typ b/tests/suite/html/elem.typ index 81ab94577..b416fdf94 100644 --- a/tests/suite/html/elem.typ +++ b/tests/suite/html/elem.typ @@ -5,3 +5,11 @@ // Error: 2-19 `` element must be the only element in the document #html.elem("html") Text + +--- html-elem-metadata html --- +#html.elem("html", context { + let val = query().first().value + test(val, "Hi") + val +}) +#metadata("Hi") From acd3a5b7a5999d22fbf2da488744d564b2f3638e Mon Sep 17 00:00:00 2001 From: aodenis <45949528+aodenis@users.noreply.github.com> Date: Tue, 25 Feb 2025 13:41:54 +0100 Subject: [PATCH 042/172] Fix high CPU usage due to inotify watch triggering itself (#5905) Co-authored-by: Laurenz --- crates/typst-cli/src/watch.rs | 57 +++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/crates/typst-cli/src/watch.rs b/crates/typst-cli/src/watch.rs index 91132fc30..cc727f0fc 100644 --- a/crates/typst-cli/src/watch.rs +++ b/crates/typst-cli/src/watch.rs @@ -204,6 +204,10 @@ impl Watcher { let event = event .map_err(|err| eco_format!("failed to watch dependencies ({err})"))?; + if !is_relevant_event_kind(&event.kind) { + continue; + } + // Workaround for notify-rs' implicit unwatch on remove/rename // (triggered by some editors when saving files) with the // inotify backend. By keeping track of the potentially @@ -224,7 +228,17 @@ impl Watcher { } } - relevant |= self.is_event_relevant(&event); + // Don't recompile because the output file changed. + // FIXME: This doesn't work properly for multifile image export. + if event + .paths + .iter() + .all(|path| is_same_file(path, &self.output).unwrap_or(false)) + { + continue; + } + + relevant = true; } // If we found a relevant event or if any of the missing files now @@ -234,32 +248,23 @@ impl Watcher { } } } +} - /// Whether a watch event is relevant for compilation. - fn is_event_relevant(&self, event: ¬ify::Event) -> bool { - // Never recompile because the output file changed. - if event - .paths - .iter() - .all(|path| is_same_file(path, &self.output).unwrap_or(false)) - { - return false; - } - - match &event.kind { - notify::EventKind::Any => true, - notify::EventKind::Access(_) => false, - notify::EventKind::Create(_) => true, - notify::EventKind::Modify(kind) => match kind { - notify::event::ModifyKind::Any => true, - notify::event::ModifyKind::Data(_) => true, - notify::event::ModifyKind::Metadata(_) => false, - notify::event::ModifyKind::Name(_) => true, - notify::event::ModifyKind::Other => false, - }, - notify::EventKind::Remove(_) => true, - notify::EventKind::Other => false, - } +/// Whether a kind of watch event is relevant for compilation. +fn is_relevant_event_kind(kind: ¬ify::EventKind) -> bool { + match kind { + notify::EventKind::Any => true, + notify::EventKind::Access(_) => false, + notify::EventKind::Create(_) => true, + notify::EventKind::Modify(kind) => match kind { + notify::event::ModifyKind::Any => true, + notify::event::ModifyKind::Data(_) => true, + notify::event::ModifyKind::Metadata(_) => false, + notify::event::ModifyKind::Name(_) => true, + notify::event::ModifyKind::Other => false, + }, + notify::EventKind::Remove(_) => true, + notify::EventKind::Other => false, } } From f31c9716240eb5c81ae225455c069089088015bc Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 25 Feb 2025 13:47:41 +0100 Subject: [PATCH 043/172] Deduplicate watcher update call (#5955) --- crates/typst-cli/src/watch.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/typst-cli/src/watch.rs b/crates/typst-cli/src/watch.rs index cc727f0fc..0813d8ffd 100644 --- a/crates/typst-cli/src/watch.rs +++ b/crates/typst-cli/src/watch.rs @@ -55,11 +55,11 @@ pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> { // Perform initial compilation. timer.record(&mut world, |world| compile_once(world, &mut config))??; - // Watch all dependencies of the initial compilation. - watcher.update(world.dependencies())?; - // Recompile whenever something relevant happens. loop { + // Watch all dependencies of the most recent compilation. + watcher.update(world.dependencies())?; + // Wait until anything relevant happens. watcher.wait()?; @@ -71,9 +71,6 @@ pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> { // Evict the cache. comemo::evict(10); - - // Adjust the file watching. - watcher.update(world.dependencies())?; } } From bad343748b834cdc155c5fe76cd944e74f4665cf Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 25 Feb 2025 14:00:22 +0100 Subject: [PATCH 044/172] Fix paper name in page setup guide (#5956) --- docs/guides/page-setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/page-setup.md b/docs/guides/page-setup.md index c93a778e2..36ed0fa23 100644 --- a/docs/guides/page-setup.md +++ b/docs/guides/page-setup.md @@ -56,7 +56,7 @@ requirements with examples. Typst's default page size is A4 paper. Depending on your region and your use case, you will want to change this. You can do this by using the [`{page}`]($page) set rule and passing it a string argument to use a common page -size. Options include the complete ISO 216 series (e.g. `"iso-a4"`, `"iso-c2"`), +size. Options include the complete ISO 216 series (e.g. `"a4"` and `"iso-c2"`), customary US formats like `"us-legal"` or `"us-letter"`, and more. Check out the reference for the [page's paper argument]($page.paper) to learn about all available options. From d11ad80dee669c5e2285ca8df8ebc99abc031ccd Mon Sep 17 00:00:00 2001 From: evie <50974538+mi2ebi@users.noreply.github.com> Date: Tue, 25 Feb 2025 06:01:01 -0800 Subject: [PATCH 045/172] Add `#str.normalize(form)` (#5631) Co-authored-by: +merlan #flirora Co-authored-by: Laurenz --- Cargo.lock | 1 + Cargo.toml | 1 + crates/typst-library/Cargo.toml | 1 + crates/typst-library/src/foundations/str.rs | 46 ++++++++++++++++++++- tests/suite/foundations/str.typ | 7 ++++ 5 files changed, 55 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 1851134a5..86f04ee52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2995,6 +2995,7 @@ dependencies = [ "typst-timing", "typst-utils", "unicode-math-class", + "unicode-normalization", "unicode-segmentation", "unscanny", "usvg", diff --git a/Cargo.toml b/Cargo.toml index 36195230e..f643856e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -129,6 +129,7 @@ unicode-bidi = "0.3.18" unicode-ident = "1.0" unicode-math-class = "0.1" unicode-script = "0.5" +unicode-normalization = "0.1.24" unicode-segmentation = "1" unscanny = "0.1" ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] } diff --git a/crates/typst-library/Cargo.toml b/crates/typst-library/Cargo.toml index fb45ec862..71729b63a 100644 --- a/crates/typst-library/Cargo.toml +++ b/crates/typst-library/Cargo.toml @@ -61,6 +61,7 @@ ttf-parser = { workspace = true } two-face = { workspace = true } typed-arena = { workspace = true } unicode-math-class = { workspace = true } +unicode-normalization = { workspace = true } unicode-segmentation = { workspace = true } unscanny = { workspace = true } usvg = { workspace = true } diff --git a/crates/typst-library/src/foundations/str.rs b/crates/typst-library/src/foundations/str.rs index 551ac04f5..23a1bd4cf 100644 --- a/crates/typst-library/src/foundations/str.rs +++ b/crates/typst-library/src/foundations/str.rs @@ -7,12 +7,13 @@ use comemo::Tracked; use ecow::EcoString; use serde::{Deserialize, Serialize}; use typst_syntax::{Span, Spanned}; +use unicode_normalization::UnicodeNormalization; use unicode_segmentation::UnicodeSegmentation; use crate::diag::{bail, At, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, dict, func, repr, scope, ty, Array, Bytes, Context, Decimal, Dict, Func, + cast, dict, func, repr, scope, ty, Array, Bytes, Cast, Context, Decimal, Dict, Func, IntoValue, Label, Repr, Type, Value, Version, }; use crate::layout::Alignment; @@ -286,6 +287,30 @@ impl Str { Ok(c.into()) } + /// Normalizes the string to the given Unicode normal form. + /// + /// This is useful when manipulating strings containing Unicode combining + /// characters. + /// + /// ```typ + /// #assert.eq("é".normalize(form: "nfd"), "e\u{0301}") + /// #assert.eq("ſ́".normalize(form: "nfkc"), "ś") + /// ``` + #[func] + pub fn normalize( + &self, + #[named] + #[default(UnicodeNormalForm::Nfc)] + form: UnicodeNormalForm, + ) -> Str { + match form { + UnicodeNormalForm::Nfc => self.nfc().collect(), + UnicodeNormalForm::Nfd => self.nfd().collect(), + UnicodeNormalForm::Nfkc => self.nfkc().collect(), + UnicodeNormalForm::Nfkd => self.nfkd().collect(), + } + } + /// Whether the string contains the specified pattern. /// /// This method also has dedicated syntax: You can write `{"bc" in "abcd"}` @@ -788,6 +813,25 @@ cast! { v: Str => Self::Str(v), } +/// A Unicode normalization form. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum UnicodeNormalForm { + /// Canonical composition where e.g. accented letters are turned into a + /// single Unicode codepoint. + #[string("nfc")] + Nfc, + /// Canonical decomposition where e.g. accented letters are split into a + /// separate base and diacritic. + #[string("nfd")] + Nfd, + /// Like NFC, but using the Unicode compatibility decompositions. + #[string("nfkc")] + Nfkc, + /// Like NFD, but using the Unicode compatibility decompositions. + #[string("nfkd")] + Nfkd, +} + /// Convert an item of std's `match_indices` to a dictionary. fn match_to_dict((start, text): (usize, &str)) -> Dict { dict! { diff --git a/tests/suite/foundations/str.typ b/tests/suite/foundations/str.typ index 56756416d..66fb912c0 100644 --- a/tests/suite/foundations/str.typ +++ b/tests/suite/foundations/str.typ @@ -86,6 +86,13 @@ // Error: 2-28 0x110000 is not a valid codepoint #str.from-unicode(0x110000) // 0x10ffff is the highest valid code point +--- str-normalize --- +// Test the `normalize` method. +#test("e\u{0301}".normalize(form: "nfc"), "é") +#test("é".normalize(form: "nfd"), "e\u{0301}") +#test("ſ\u{0301}".normalize(form: "nfkc"), "ś") +#test("ſ\u{0301}".normalize(form: "nfkd"), "s\u{0301}") + --- string-len --- // Test the `len` method. #test("Hello World!".len(), 12) From 2eef9e84e117670ea0db964a5a8addc89e0ee785 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Tue, 25 Feb 2025 15:09:52 +0100 Subject: [PATCH 046/172] Improve hints for show rule recursion depth (#5856) --- crates/typst-library/src/engine.rs | 3 ++- tests/suite/scripting/recursion.typ | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/typst-library/src/engine.rs b/crates/typst-library/src/engine.rs index 80aaef224..43a7b4671 100644 --- a/crates/typst-library/src/engine.rs +++ b/crates/typst-library/src/engine.rs @@ -312,7 +312,8 @@ impl Route<'_> { if !self.within(Route::MAX_SHOW_RULE_DEPTH) { bail!( "maximum show rule depth exceeded"; - hint: "check whether the show rule matches its own output" + hint: "maybe a show rule matches its own output"; + hint: "maybe there are too deeply nested elements" ); } Ok(()) diff --git a/tests/suite/scripting/recursion.typ b/tests/suite/scripting/recursion.typ index 6be96c1ec..e92b67fb7 100644 --- a/tests/suite/scripting/recursion.typ +++ b/tests/suite/scripting/recursion.typ @@ -44,18 +44,21 @@ --- recursion-via-include-in-layout --- // Test cyclic imports during layout. // Error: 2-38 maximum show rule depth exceeded -// Hint: 2-38 check whether the show rule matches its own output +// Hint: 2-38 maybe a show rule matches its own output +// Hint: 2-38 maybe there are too deeply nested elements #layout(_ => include "recursion.typ") --- recursion-show-math --- // Test recursive show rules. // Error: 22-25 maximum show rule depth exceeded -// Hint: 22-25 check whether the show rule matches its own output +// Hint: 22-25 maybe a show rule matches its own output +// Hint: 22-25 maybe there are too deeply nested elements #show math.equation: $x$ $ x $ --- recursion-show-math-realize --- // Error: 22-33 maximum show rule depth exceeded -// Hint: 22-33 check whether the show rule matches its own output +// Hint: 22-33 maybe a show rule matches its own output +// Hint: 22-33 maybe there are too deeply nested elements #show heading: it => heading[it] $ #heading[hi] $ From 8f039dd614ba518976b8b486e0a138bd6a9c660c Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 25 Feb 2025 15:10:01 +0100 Subject: [PATCH 047/172] Only autocomplete methods which take self (#5824) --- crates/typst-ide/src/complete.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index e3dcc442e..e3d777115 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -410,9 +410,17 @@ fn field_access_completions( elem.into_iter().chain(Some(ty)) }; - // Autocomplete methods from the element's or type's scope. + // Autocomplete methods from the element's or type's scope. We only complete + // those which have a `self` parameter. for (name, binding) in scopes.flat_map(|scope| scope.iter()) { - ctx.call_completion(name.clone(), binding.read()); + let Ok(func) = binding.read().clone().cast::() else { continue }; + if func + .params() + .and_then(|params| params.first()) + .is_some_and(|param| param.name == "self") + { + ctx.call_completion(name.clone(), binding.read()); + } } if let Some(scope) = value.scope() { @@ -1764,6 +1772,7 @@ mod tests { #[test] fn test_autocomplete_type_methods() { test("#\"hello\".", -1).must_include(["len", "contains"]); + test("#table().", -1).must_exclude(["cell"]); } #[test] From d6b0d68ffa4963459f52f7d774080f1f128841d4 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Tue, 25 Feb 2025 15:19:17 +0100 Subject: [PATCH 048/172] Add more methods to `direction` (#5893) --- crates/typst-library/src/layout/dir.rs | 52 ++++++++++++++++++++++++++ tests/suite/layout/dir.typ | 27 ++++++++++++- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/crates/typst-library/src/layout/dir.rs b/crates/typst-library/src/layout/dir.rs index 9a2e77105..699c8c481 100644 --- a/crates/typst-library/src/layout/dir.rs +++ b/crates/typst-library/src/layout/dir.rs @@ -50,6 +50,42 @@ impl Dir { pub const TTB: Self = Self::TTB; pub const BTT: Self = Self::BTT; + /// Returns a direction from a starting point. + /// + /// ```example + /// direction.from(left) \ + /// direction.from(right) \ + /// direction.from(top) \ + /// direction.from(bottom) + /// ``` + #[func] + pub const fn from(side: Side) -> Dir { + match side { + Side::Left => Self::LTR, + Side::Right => Self::RTL, + Side::Top => Self::TTB, + Side::Bottom => Self::BTT, + } + } + + /// Returns a direction from an end point. + /// + /// ```example + /// direction.to(left) \ + /// direction.to(right) \ + /// direction.to(top) \ + /// direction.to(bottom) + /// ``` + #[func] + pub const fn to(side: Side) -> Dir { + match side { + Side::Right => Self::LTR, + Side::Left => Self::RTL, + Side::Bottom => Self::TTB, + Side::Top => Self::BTT, + } + } + /// The axis this direction belongs to, either `{"horizontal"}` or /// `{"vertical"}`. /// @@ -65,6 +101,22 @@ impl Dir { } } + /// The corresponding sign, for use in calculations. + /// + /// ```example + /// #ltr.sign() \ + /// #rtl.sign() \ + /// #ttb.sign() \ + /// #btt.sign() + /// ``` + #[func] + pub const fn sign(self) -> i64 { + match self { + Self::LTR | Self::TTB => 1, + Self::RTL | Self::BTT => -1, + } + } + /// The start point of this direction, as an alignment. /// /// ```example diff --git a/tests/suite/layout/dir.typ b/tests/suite/layout/dir.typ index 139a2285d..e6db54da5 100644 --- a/tests/suite/layout/dir.typ +++ b/tests/suite/layout/dir.typ @@ -1,10 +1,35 @@ +--- dir-from --- +#test(direction.from(left), ltr) +#test(direction.from(right), rtl) +#test(direction.from(top), ttb) +#test(direction.from(bottom), btt) + +--- dir-from-invalid --- +// Error: 17-23 cannot convert this alignment to a side +#direction.from(center) + +--- dir-to --- +#test(direction.to(left), rtl) +#test(direction.to(right), ltr) +#test(direction.to(top), btt) +#test(direction.to(bottom), ttb) + +-- dir-to-invalid --- +// Error: 15-21 cannot convert this alignment to a side +#direction.to(center) + --- dir-axis --- -// Test direction methods. #test(ltr.axis(), "horizontal") #test(rtl.axis(), "horizontal") #test(ttb.axis(), "vertical") #test(btt.axis(), "vertical") +--- dir-sign --- +#test(ltr.sign(), 1) +#test(rtl.sign(), -1) +#test(ttb.sign(), 1) +#test(btt.sign(), -1) + --- dir-start --- #test(ltr.start(), left) #test(rtl.start(), right) From 52f1f53973414be72bf22c3253ab365f8db067df Mon Sep 17 00:00:00 2001 From: Emmanuel Lesueur <48604057+Emm54321@users.noreply.github.com> Date: Wed, 26 Feb 2025 19:07:29 +0100 Subject: [PATCH 049/172] Fix curve with multiple non-closed components. (#5963) --- crates/typst-layout/src/shapes.rs | 1 + tests/ref/curve-multiple-non-closed.png | Bin 0 -> 85 bytes tests/suite/visualize/curve.typ | 10 ++++++++++ 3 files changed, 11 insertions(+) create mode 100644 tests/ref/curve-multiple-non-closed.png diff --git a/crates/typst-layout/src/shapes.rs b/crates/typst-layout/src/shapes.rs index 21d0a518f..7ab41e9d4 100644 --- a/crates/typst-layout/src/shapes.rs +++ b/crates/typst-layout/src/shapes.rs @@ -284,6 +284,7 @@ impl<'a> CurveBuilder<'a> { self.last_point = point; self.last_control_from = point; self.is_started = true; + self.is_empty = true; } /// Add a line segment. diff --git a/tests/ref/curve-multiple-non-closed.png b/tests/ref/curve-multiple-non-closed.png new file mode 100644 index 0000000000000000000000000000000000000000..f4332e363f7500fbfdf1745ddb07156cd699804e GIT binary patch literal 85 zcmeAS@N?(olHy`uVBq!ia0vp^6+o=P2qYL}Co*>cDH%@}$B>F!$v^tJB-rNEH#ADl iwSQ&zYG?k0qvj00FFdmKHq?9ssrPjCb6Mw<&;$TP`50CJ literal 0 HcmV?d00001 diff --git a/tests/suite/visualize/curve.typ b/tests/suite/visualize/curve.typ index f98f634a7..14a1c0cc8 100644 --- a/tests/suite/visualize/curve.typ +++ b/tests/suite/visualize/curve.typ @@ -38,6 +38,16 @@ curve.close(mode: "smooth"), ) +--- curve-multiple-non-closed --- +#curve( + stroke: 2pt, + curve.line((20pt, 0pt)), + curve.move((0pt, 10pt)), + curve.line((20pt, 10pt)), + curve.move((0pt, 20pt)), + curve.line((20pt, 20pt)), +) + --- curve-line --- #curve( fill: purple, From cfb3b1a2709107f0f06f89ea25cabc939cec15e5 Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski <133046678+wrzian@users.noreply.github.com> Date: Wed, 26 Feb 2025 15:10:36 -0500 Subject: [PATCH 050/172] Improve clarity of `ast.rs` for newcomers to the codebase (#5784) Co-authored-by: PgBiel <9021226+PgBiel@users.noreply.github.com> Co-authored-by: T0mstone <39707032+T0mstone@users.noreply.github.com> --- crates/typst-eval/src/call.rs | 8 +- crates/typst-eval/src/code.rs | 36 +- crates/typst-eval/src/markup.rs | 4 +- crates/typst-eval/src/rules.rs | 2 +- crates/typst-ide/src/complete.rs | 8 +- crates/typst-ide/src/matchers.rs | 4 +- crates/typst-ide/src/tooltip.rs | 2 +- crates/typst-syntax/src/ast.rs | 641 ++++++++++++++++++------------- crates/typst-syntax/src/node.rs | 21 - 9 files changed, 415 insertions(+), 311 deletions(-) diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index c68bef963..1ca7b4b8f 100644 --- a/crates/typst-eval/src/call.rs +++ b/crates/typst-eval/src/call.rs @@ -466,7 +466,7 @@ impl<'a> CapturesVisitor<'a> { } // Code and content blocks create a scope. - Some(ast::Expr::Code(_) | ast::Expr::Content(_)) => { + Some(ast::Expr::CodeBlock(_) | ast::Expr::ContentBlock(_)) => { self.internal.enter(); for child in node.children() { self.visit(child); @@ -516,7 +516,7 @@ impl<'a> CapturesVisitor<'a> { // A let expression contains a binding, but that binding is only // active after the body is evaluated. - Some(ast::Expr::Let(expr)) => { + Some(ast::Expr::LetBinding(expr)) => { if let Some(init) = expr.init() { self.visit(init.to_untyped()); } @@ -529,7 +529,7 @@ impl<'a> CapturesVisitor<'a> { // A for loop contains one or two bindings in its pattern. These are // active after the iterable is evaluated but before the body is // evaluated. - Some(ast::Expr::For(expr)) => { + Some(ast::Expr::ForLoop(expr)) => { self.visit(expr.iterable().to_untyped()); self.internal.enter(); @@ -544,7 +544,7 @@ impl<'a> CapturesVisitor<'a> { // An import contains items, but these are active only after the // path is evaluated. - Some(ast::Expr::Import(expr)) => { + Some(ast::Expr::ModuleImport(expr)) => { self.visit(expr.source().to_untyped()); if let Some(ast::Imports::Items(items)) = expr.imports() { for item in items.iter() { diff --git a/crates/typst-eval/src/code.rs b/crates/typst-eval/src/code.rs index a7b6b6f90..9078418e4 100644 --- a/crates/typst-eval/src/code.rs +++ b/crates/typst-eval/src/code.rs @@ -30,7 +30,7 @@ fn eval_code<'a>( while let Some(expr) = exprs.next() { let span = expr.span(); let value = match expr { - ast::Expr::Set(set) => { + ast::Expr::SetRule(set) => { let styles = set.eval(vm)?; if vm.flow.is_some() { break; @@ -39,7 +39,7 @@ fn eval_code<'a>( let tail = eval_code(vm, exprs)?.display(); Value::Content(tail.styled_with_map(styles)) } - ast::Expr::Show(show) => { + ast::Expr::ShowRule(show) => { let recipe = show.eval(vm)?; if vm.flow.is_some() { break; @@ -94,9 +94,9 @@ impl Eval for ast::Expr<'_> { Self::Label(v) => v.eval(vm), Self::Ref(v) => v.eval(vm).map(Value::Content), Self::Heading(v) => v.eval(vm).map(Value::Content), - Self::List(v) => v.eval(vm).map(Value::Content), - Self::Enum(v) => v.eval(vm).map(Value::Content), - Self::Term(v) => v.eval(vm).map(Value::Content), + Self::ListItem(v) => v.eval(vm).map(Value::Content), + Self::EnumItem(v) => v.eval(vm).map(Value::Content), + Self::TermItem(v) => v.eval(vm).map(Value::Content), Self::Equation(v) => v.eval(vm).map(Value::Content), Self::Math(v) => v.eval(vm).map(Value::Content), Self::MathText(v) => v.eval(vm).map(Value::Content), @@ -116,8 +116,8 @@ impl Eval for ast::Expr<'_> { Self::Float(v) => v.eval(vm), Self::Numeric(v) => v.eval(vm), Self::Str(v) => v.eval(vm), - Self::Code(v) => v.eval(vm), - Self::Content(v) => v.eval(vm).map(Value::Content), + Self::CodeBlock(v) => v.eval(vm), + Self::ContentBlock(v) => v.eval(vm).map(Value::Content), Self::Array(v) => v.eval(vm).map(Value::Array), Self::Dict(v) => v.eval(vm).map(Value::Dict), Self::Parenthesized(v) => v.eval(vm), @@ -126,19 +126,19 @@ impl Eval for ast::Expr<'_> { Self::Closure(v) => v.eval(vm), Self::Unary(v) => v.eval(vm), Self::Binary(v) => v.eval(vm), - Self::Let(v) => v.eval(vm), - Self::DestructAssign(v) => v.eval(vm), - Self::Set(_) => bail!(forbidden("set")), - Self::Show(_) => bail!(forbidden("show")), + Self::LetBinding(v) => v.eval(vm), + Self::DestructAssignment(v) => v.eval(vm), + Self::SetRule(_) => bail!(forbidden("set")), + Self::ShowRule(_) => bail!(forbidden("show")), Self::Contextual(v) => v.eval(vm).map(Value::Content), Self::Conditional(v) => v.eval(vm), - Self::While(v) => v.eval(vm), - Self::For(v) => v.eval(vm), - Self::Import(v) => v.eval(vm), - Self::Include(v) => v.eval(vm).map(Value::Content), - Self::Break(v) => v.eval(vm), - Self::Continue(v) => v.eval(vm), - Self::Return(v) => v.eval(vm), + Self::WhileLoop(v) => v.eval(vm), + Self::ForLoop(v) => v.eval(vm), + Self::ModuleImport(v) => v.eval(vm), + Self::ModuleInclude(v) => v.eval(vm).map(Value::Content), + Self::LoopBreak(v) => v.eval(vm), + Self::LoopContinue(v) => v.eval(vm), + Self::FuncReturn(v) => v.eval(vm), }? .spanned(span); diff --git a/crates/typst-eval/src/markup.rs b/crates/typst-eval/src/markup.rs index 3a5ebe1fc..5beefa912 100644 --- a/crates/typst-eval/src/markup.rs +++ b/crates/typst-eval/src/markup.rs @@ -33,7 +33,7 @@ fn eval_markup<'a>( while let Some(expr) = exprs.next() { match expr { - ast::Expr::Set(set) => { + ast::Expr::SetRule(set) => { let styles = set.eval(vm)?; if vm.flow.is_some() { break; @@ -41,7 +41,7 @@ fn eval_markup<'a>( seq.push(eval_markup(vm, exprs)?.styled_with_map(styles)) } - ast::Expr::Show(show) => { + ast::Expr::ShowRule(show) => { let recipe = show.eval(vm)?; if vm.flow.is_some() { break; diff --git a/crates/typst-eval/src/rules.rs b/crates/typst-eval/src/rules.rs index 646354d4b..f4c1563f3 100644 --- a/crates/typst-eval/src/rules.rs +++ b/crates/typst-eval/src/rules.rs @@ -45,7 +45,7 @@ impl Eval for ast::ShowRule<'_> { let transform = self.transform(); let transform = match transform { - ast::Expr::Set(set) => Transformation::Style(set.eval(vm)?), + ast::Expr::SetRule(set) => Transformation::Style(set.eval(vm)?), expr => expr.eval(vm)?.cast::().at(transform.span())?, }; diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index e3d777115..91fa53f9a 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -517,7 +517,7 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool { // "#import "path.typ": a, b, |". if_chain! { if let Some(prev) = ctx.leaf.prev_sibling(); - if let Some(ast::Expr::Import(import)) = prev.get().cast(); + if let Some(ast::Expr::ModuleImport(import)) = prev.get().cast(); if let Some(ast::Imports::Items(items)) = import.imports(); if let Some(source) = prev.children().find(|child| child.is::()); then { @@ -536,7 +536,7 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool { if let Some(grand) = parent.parent(); if grand.kind() == SyntaxKind::ImportItems; if let Some(great) = grand.parent(); - if let Some(ast::Expr::Import(import)) = great.get().cast(); + if let Some(ast::Expr::ModuleImport(import)) = great.get().cast(); if let Some(ast::Imports::Items(items)) = import.imports(); if let Some(source) = great.children().find(|child| child.is::()); then { @@ -677,10 +677,10 @@ fn complete_params(ctx: &mut CompletionContext) -> bool { if let Some(args) = parent.get().cast::(); if let Some(grand) = parent.parent(); if let Some(expr) = grand.get().cast::(); - let set = matches!(expr, ast::Expr::Set(_)); + let set = matches!(expr, ast::Expr::SetRule(_)); if let Some(callee) = match expr { ast::Expr::FuncCall(call) => Some(call.callee()), - ast::Expr::Set(set) => Some(set.target()), + ast::Expr::SetRule(set) => Some(set.target()), _ => None, }; then { diff --git a/crates/typst-ide/src/matchers.rs b/crates/typst-ide/src/matchers.rs index 270d2f43c..93fdc5dd5 100644 --- a/crates/typst-ide/src/matchers.rs +++ b/crates/typst-ide/src/matchers.rs @@ -232,7 +232,9 @@ pub fn deref_target(node: LinkedNode) -> Option> { ast::Expr::FuncCall(call) => { DerefTarget::Callee(expr_node.find(call.callee().span())?) } - ast::Expr::Set(set) => DerefTarget::Callee(expr_node.find(set.target().span())?), + ast::Expr::SetRule(set) => { + DerefTarget::Callee(expr_node.find(set.target().span())?) + } ast::Expr::Ident(_) | ast::Expr::MathIdent(_) | ast::Expr::FieldAccess(_) => { DerefTarget::VarAccess(expr_node) } diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index cfb977733..cbfffe530 100644 --- a/crates/typst-ide/src/tooltip.rs +++ b/crates/typst-ide/src/tooltip.rs @@ -201,7 +201,7 @@ fn named_param_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option(); if let Some(ast::Expr::Ident(callee)) = match expr { ast::Expr::FuncCall(call) => Some(call.callee()), - ast::Expr::Set(set) => Some(set.target()), + ast::Expr::SetRule(set) => Some(set.target()), _ => None, }; diff --git a/crates/typst-syntax/src/ast.rs b/crates/typst-syntax/src/ast.rs index 640138e77..f79e65982 100644 --- a/crates/typst-syntax/src/ast.rs +++ b/crates/typst-syntax/src/ast.rs @@ -1,6 +1,81 @@ -//! A typed layer over the untyped syntax tree. -//! -//! The AST is rooted in the [`Markup`] node. +/*! +# Abstract Syntax Tree Interface + +Typst's Abstract Syntax Tree (AST) is a lazy, typed view over the untyped +Concrete Syntax Tree (CST) and is rooted in the [`Markup`] node. + +## The AST is a View + +Most AST nodes are wrapper structs around [`SyntaxNode`] pointers. This summary +will use a running example of the [`Raw`] node type, which is declared (after +macro expansion) as: `struct Raw<'a>(&'a SyntaxNode);`. + +[`SyntaxNode`]s are generated by the parser and constitute the Concrete Syntax +Tree (CST). The CST is _concrete_ because it has the property that an in-order +tree traversal will recreate the text of the source file exactly. + +[`SyntaxNode`]s in the CST contain their [`SyntaxKind`], but don't themselves +provide access to the semantic meaning of their contents. That semantic meaning +is available through the Abstract Syntax Tree by iterating over CST nodes and +inspecting their contents. The format is prepared ahead-of-time by the parser so +that this module can unpack the abstract meaning from the CST's structure. + +Raw nodes are parsed by recognizing paired backtick delimiters, which you will +find as CST nodes with the [`RawDelim`] kind. However, the AST doesn't include +these delimiters because it _abstracts_ over the backticks. Instead, the parent +raw node will only use its child [`RawDelim`] CST nodes to determine whether the +element is a block or inline. + +## The AST is Typed + +AST nodes all implement the [`AstNode`] trait, but nodes can also implement +their own unique methods. These unique methods are the "real" interface of the +AST, and provide access to the abstract, semantic, representation of each kind +of node. For example, the [`Raw`] node provides 3 methods that specify its +abstract representation: [`Raw::lines()`] returns the raw text as an iterator of +lines, [`Raw::lang()`] provides the optionally present [`RawLang`] language tag, +and [`Raw::block()`] gives a bool for whether the raw element is a block or +inline. + +This semantic information is unavailable in the CST. Only by converting a CST +node to an AST struct will Rust let you call a method of that struct. This is a +safe interface because the only way to create an AST node outside this file is +to call [`AstNode::from_untyped`]. The `node!` macro implements `from_untyped` +by checking the node's kind before constructing it, returning `Some()` only if +the kind matches. So we know that it will have the expected children underneath, +otherwise the parser wouldn't have produced this node. + +## The AST is rooted in the [`Markup`] node + +The AST is rooted in the [`Markup`] node, which provides only one method: +[`Markup::exprs`]. This returns an iterator of the main [`Expr`] enum. [`Expr`] +is important because it contains the majority of expressions that Typst will +evaluate. Not just markup, but also math and code expressions. Not all +expression types are available from the parser at every step, but this does +decrease the amount of wrapper enums needed in the AST (and this file is long +enough already). + +Expressions also branch off into the remaining tree. You can view enums in this +file as edges on a graph: areas where the tree has paths from one type to +another (accessed through methods), then structs are the nodes of the graph, +providing methods that return enums, etc. etc. + +## The AST is Lazy + +Being lazy means that the untyped CST nodes are converted to typed AST nodes +only as the tree is traversed. If we parse a file and a raw block is contained +in a branch of an if-statement that we don't take, then we won't pay the cost of +creating an iterator over the lines or checking whether it was a block or +inline (although it will still be parsed into nodes). + +This is also a factor of the current "tree-interpreter" evaluation model. A +bytecode interpreter might instead eagerly convert the AST into bytecode, but it +would still traverse using this lazy interface. While the tree-interpreter +evaluation is straightforward and easy to add new features onto, it has to +re-traverse the AST every time a function is evaluated. A bytecode interpreter +using the lazy interface would only need to traverse each node once, improving +throughput at the cost of initial latency and development flexibility. +*/ use std::num::NonZeroUsize; use std::ops::Deref; @@ -27,8 +102,55 @@ pub trait AstNode<'a>: Sized { } } +// A generic interface for converting untyped nodes into typed AST nodes. +impl SyntaxNode { + /// Whether the node can be cast to the given AST node. + pub fn is<'a, T: AstNode<'a>>(&'a self) -> bool { + self.cast::().is_some() + } + + /// Try to convert the node to a typed AST node. + pub fn cast<'a, T: AstNode<'a>>(&'a self) -> Option { + T::from_untyped(self) + } + + /// Find the first child that can cast to the AST type `T`. + fn try_cast_first<'a, T: AstNode<'a>>(&'a self) -> Option { + self.children().find_map(Self::cast) + } + + /// Find the last child that can cast to the AST type `T`. + fn try_cast_last<'a, T: AstNode<'a>>(&'a self) -> Option { + self.children().rev().find_map(Self::cast) + } + + /// Get the first child of AST type `T` or a placeholder if none. + fn cast_first<'a, T: AstNode<'a> + Default>(&'a self) -> T { + self.try_cast_first().unwrap_or_default() + } + + /// Get the last child of AST type `T` or a placeholder if none. + fn cast_last<'a, T: AstNode<'a> + Default>(&'a self) -> T { + self.try_cast_last().unwrap_or_default() + } +} + +/// Implements [`AstNode`] for a struct whose name matches a [`SyntaxKind`] +/// variant. +/// +/// The struct becomes a wrapper around a [`SyntaxNode`] pointer, and the +/// implementation of [`AstNode::from_untyped`] checks that the pointer's kind +/// matches when converting, returning `Some` or `None` respectively. +/// +/// The generated struct is the basis for typed accessor methods for properties +/// of this AST node. For example, the [`Raw`] struct has methods for accessing +/// its content by lines, its optional language tag, and whether the raw element +/// is inline or a block. These methods are accessible only _after_ a +/// `SyntaxNode` is coerced to the `Raw` struct type (via `from_untyped`), +/// guaranteeing their implementations will work with the expected structure. macro_rules! node { - ($(#[$attr:meta])* $name:ident) => { + ($(#[$attr:meta])* struct $name:ident) => { + // Create the struct as a wrapper around a `SyntaxNode` reference. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[repr(transparent)] $(#[$attr])* @@ -63,7 +185,7 @@ macro_rules! node { node! { /// The syntactical root capable of representing a full parsed document. - Markup + struct Markup } impl<'a> Markup<'a> { @@ -117,11 +239,11 @@ pub enum Expr<'a> { /// A section heading: `= Introduction`. Heading(Heading<'a>), /// An item in a bullet list: `- ...`. - List(ListItem<'a>), + ListItem(ListItem<'a>), /// An item in an enumeration (numbered list): `+ ...` or `1. ...`. - Enum(EnumItem<'a>), + EnumItem(EnumItem<'a>), /// An item in a term list: `/ Term: Details`. - Term(TermItem<'a>), + TermItem(TermItem<'a>), /// A mathematical equation: `$x$`, `$ x^2 $`. Equation(Equation<'a>), /// The contents of a mathematical equation: `x^2 + 1`. @@ -161,9 +283,9 @@ pub enum Expr<'a> { /// A quoted string: `"..."`. Str(Str<'a>), /// A code block: `{ let x = 1; x + 2 }`. - Code(CodeBlock<'a>), + CodeBlock(CodeBlock<'a>), /// A content block: `[*Hi* there!]`. - Content(ContentBlock<'a>), + ContentBlock(ContentBlock<'a>), /// A grouped expression: `(1 + 2)`. Parenthesized(Parenthesized<'a>), /// An array: `(1, "hi", 12cm)`. @@ -181,37 +303,37 @@ pub enum Expr<'a> { /// A closure: `(x, y) => z`. Closure(Closure<'a>), /// A let binding: `let x = 1`. - Let(LetBinding<'a>), + LetBinding(LetBinding<'a>), /// A destructuring assignment: `(x, y) = (1, 2)`. - DestructAssign(DestructAssignment<'a>), + DestructAssignment(DestructAssignment<'a>), /// A set rule: `set text(...)`. - Set(SetRule<'a>), + SetRule(SetRule<'a>), /// A show rule: `show heading: it => emph(it.body)`. - Show(ShowRule<'a>), + ShowRule(ShowRule<'a>), /// A contextual expression: `context text.lang`. Contextual(Contextual<'a>), /// An if-else conditional: `if x { y } else { z }`. Conditional(Conditional<'a>), /// A while loop: `while x { y }`. - While(WhileLoop<'a>), + WhileLoop(WhileLoop<'a>), /// A for loop: `for x in y { z }`. - For(ForLoop<'a>), + ForLoop(ForLoop<'a>), /// A module import: `import "utils.typ": a, b, c`. - Import(ModuleImport<'a>), + ModuleImport(ModuleImport<'a>), /// A module include: `include "chapter1.typ"`. - Include(ModuleInclude<'a>), + ModuleInclude(ModuleInclude<'a>), /// A break from a loop: `break`. - Break(LoopBreak<'a>), + LoopBreak(LoopBreak<'a>), /// A continue in a loop: `continue`. - Continue(LoopContinue<'a>), + LoopContinue(LoopContinue<'a>), /// A return from a function: `return`, `return x + 1`. - Return(FuncReturn<'a>), + FuncReturn(FuncReturn<'a>), } impl<'a> Expr<'a> { fn cast_with_space(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Space => node.cast().map(Self::Space), + SyntaxKind::Space => Some(Self::Space(Space(node))), _ => Self::from_untyped(node), } } @@ -220,64 +342,69 @@ impl<'a> Expr<'a> { impl<'a> AstNode<'a> for Expr<'a> { fn from_untyped(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Linebreak => node.cast().map(Self::Linebreak), - SyntaxKind::Parbreak => node.cast().map(Self::Parbreak), - SyntaxKind::Text => node.cast().map(Self::Text), - SyntaxKind::Escape => node.cast().map(Self::Escape), - SyntaxKind::Shorthand => node.cast().map(Self::Shorthand), - SyntaxKind::SmartQuote => node.cast().map(Self::SmartQuote), - SyntaxKind::Strong => node.cast().map(Self::Strong), - SyntaxKind::Emph => node.cast().map(Self::Emph), - SyntaxKind::Raw => node.cast().map(Self::Raw), - SyntaxKind::Link => node.cast().map(Self::Link), - SyntaxKind::Label => node.cast().map(Self::Label), - SyntaxKind::Ref => node.cast().map(Self::Ref), - SyntaxKind::Heading => node.cast().map(Self::Heading), - SyntaxKind::ListItem => node.cast().map(Self::List), - SyntaxKind::EnumItem => node.cast().map(Self::Enum), - SyntaxKind::TermItem => node.cast().map(Self::Term), - SyntaxKind::Equation => node.cast().map(Self::Equation), - SyntaxKind::Math => node.cast().map(Self::Math), - SyntaxKind::MathText => node.cast().map(Self::MathText), - SyntaxKind::MathIdent => node.cast().map(Self::MathIdent), - SyntaxKind::MathShorthand => node.cast().map(Self::MathShorthand), - SyntaxKind::MathAlignPoint => node.cast().map(Self::MathAlignPoint), - SyntaxKind::MathDelimited => node.cast().map(Self::MathDelimited), - SyntaxKind::MathAttach => node.cast().map(Self::MathAttach), - SyntaxKind::MathPrimes => node.cast().map(Self::MathPrimes), - SyntaxKind::MathFrac => node.cast().map(Self::MathFrac), - SyntaxKind::MathRoot => node.cast().map(Self::MathRoot), - SyntaxKind::Ident => node.cast().map(Self::Ident), - SyntaxKind::None => node.cast().map(Self::None), - SyntaxKind::Auto => node.cast().map(Self::Auto), - SyntaxKind::Bool => node.cast().map(Self::Bool), - SyntaxKind::Int => node.cast().map(Self::Int), - SyntaxKind::Float => node.cast().map(Self::Float), - SyntaxKind::Numeric => node.cast().map(Self::Numeric), - SyntaxKind::Str => node.cast().map(Self::Str), - SyntaxKind::CodeBlock => node.cast().map(Self::Code), - SyntaxKind::ContentBlock => node.cast().map(Self::Content), - SyntaxKind::Parenthesized => node.cast().map(Self::Parenthesized), - SyntaxKind::Array => node.cast().map(Self::Array), - SyntaxKind::Dict => node.cast().map(Self::Dict), - SyntaxKind::Unary => node.cast().map(Self::Unary), - SyntaxKind::Binary => node.cast().map(Self::Binary), - SyntaxKind::FieldAccess => node.cast().map(Self::FieldAccess), - SyntaxKind::FuncCall => node.cast().map(Self::FuncCall), - SyntaxKind::Closure => node.cast().map(Self::Closure), - SyntaxKind::LetBinding => node.cast().map(Self::Let), - SyntaxKind::DestructAssignment => node.cast().map(Self::DestructAssign), - SyntaxKind::SetRule => node.cast().map(Self::Set), - SyntaxKind::ShowRule => node.cast().map(Self::Show), - SyntaxKind::Contextual => node.cast().map(Self::Contextual), - SyntaxKind::Conditional => node.cast().map(Self::Conditional), - SyntaxKind::WhileLoop => node.cast().map(Self::While), - SyntaxKind::ForLoop => node.cast().map(Self::For), - SyntaxKind::ModuleImport => node.cast().map(Self::Import), - SyntaxKind::ModuleInclude => node.cast().map(Self::Include), - SyntaxKind::LoopBreak => node.cast().map(Self::Break), - SyntaxKind::LoopContinue => node.cast().map(Self::Continue), - SyntaxKind::FuncReturn => node.cast().map(Self::Return), + SyntaxKind::Space => Option::None, // Skipped unless using `cast_with_space`. + SyntaxKind::Linebreak => Some(Self::Linebreak(Linebreak(node))), + SyntaxKind::Parbreak => Some(Self::Parbreak(Parbreak(node))), + SyntaxKind::Text => Some(Self::Text(Text(node))), + SyntaxKind::Escape => Some(Self::Escape(Escape(node))), + SyntaxKind::Shorthand => Some(Self::Shorthand(Shorthand(node))), + SyntaxKind::SmartQuote => Some(Self::SmartQuote(SmartQuote(node))), + SyntaxKind::Strong => Some(Self::Strong(Strong(node))), + SyntaxKind::Emph => Some(Self::Emph(Emph(node))), + SyntaxKind::Raw => Some(Self::Raw(Raw(node))), + SyntaxKind::Link => Some(Self::Link(Link(node))), + SyntaxKind::Label => Some(Self::Label(Label(node))), + SyntaxKind::Ref => Some(Self::Ref(Ref(node))), + SyntaxKind::Heading => Some(Self::Heading(Heading(node))), + SyntaxKind::ListItem => Some(Self::ListItem(ListItem(node))), + SyntaxKind::EnumItem => Some(Self::EnumItem(EnumItem(node))), + SyntaxKind::TermItem => Some(Self::TermItem(TermItem(node))), + SyntaxKind::Equation => Some(Self::Equation(Equation(node))), + SyntaxKind::Math => Some(Self::Math(Math(node))), + SyntaxKind::MathText => Some(Self::MathText(MathText(node))), + SyntaxKind::MathIdent => Some(Self::MathIdent(MathIdent(node))), + SyntaxKind::MathShorthand => Some(Self::MathShorthand(MathShorthand(node))), + SyntaxKind::MathAlignPoint => { + Some(Self::MathAlignPoint(MathAlignPoint(node))) + } + SyntaxKind::MathDelimited => Some(Self::MathDelimited(MathDelimited(node))), + SyntaxKind::MathAttach => Some(Self::MathAttach(MathAttach(node))), + SyntaxKind::MathPrimes => Some(Self::MathPrimes(MathPrimes(node))), + SyntaxKind::MathFrac => Some(Self::MathFrac(MathFrac(node))), + SyntaxKind::MathRoot => Some(Self::MathRoot(MathRoot(node))), + SyntaxKind::Ident => Some(Self::Ident(Ident(node))), + SyntaxKind::None => Some(Self::None(None(node))), + SyntaxKind::Auto => Some(Self::Auto(Auto(node))), + SyntaxKind::Bool => Some(Self::Bool(Bool(node))), + SyntaxKind::Int => Some(Self::Int(Int(node))), + SyntaxKind::Float => Some(Self::Float(Float(node))), + SyntaxKind::Numeric => Some(Self::Numeric(Numeric(node))), + SyntaxKind::Str => Some(Self::Str(Str(node))), + SyntaxKind::CodeBlock => Some(Self::CodeBlock(CodeBlock(node))), + SyntaxKind::ContentBlock => Some(Self::ContentBlock(ContentBlock(node))), + SyntaxKind::Parenthesized => Some(Self::Parenthesized(Parenthesized(node))), + SyntaxKind::Array => Some(Self::Array(Array(node))), + SyntaxKind::Dict => Some(Self::Dict(Dict(node))), + SyntaxKind::Unary => Some(Self::Unary(Unary(node))), + SyntaxKind::Binary => Some(Self::Binary(Binary(node))), + SyntaxKind::FieldAccess => Some(Self::FieldAccess(FieldAccess(node))), + SyntaxKind::FuncCall => Some(Self::FuncCall(FuncCall(node))), + SyntaxKind::Closure => Some(Self::Closure(Closure(node))), + SyntaxKind::LetBinding => Some(Self::LetBinding(LetBinding(node))), + SyntaxKind::DestructAssignment => { + Some(Self::DestructAssignment(DestructAssignment(node))) + } + SyntaxKind::SetRule => Some(Self::SetRule(SetRule(node))), + SyntaxKind::ShowRule => Some(Self::ShowRule(ShowRule(node))), + SyntaxKind::Contextual => Some(Self::Contextual(Contextual(node))), + SyntaxKind::Conditional => Some(Self::Conditional(Conditional(node))), + SyntaxKind::WhileLoop => Some(Self::WhileLoop(WhileLoop(node))), + SyntaxKind::ForLoop => Some(Self::ForLoop(ForLoop(node))), + SyntaxKind::ModuleImport => Some(Self::ModuleImport(ModuleImport(node))), + SyntaxKind::ModuleInclude => Some(Self::ModuleInclude(ModuleInclude(node))), + SyntaxKind::LoopBreak => Some(Self::LoopBreak(LoopBreak(node))), + SyntaxKind::LoopContinue => Some(Self::LoopContinue(LoopContinue(node))), + SyntaxKind::FuncReturn => Some(Self::FuncReturn(FuncReturn(node))), _ => Option::None, } } @@ -298,9 +425,9 @@ impl<'a> AstNode<'a> for Expr<'a> { Self::Label(v) => v.to_untyped(), Self::Ref(v) => v.to_untyped(), Self::Heading(v) => v.to_untyped(), - Self::List(v) => v.to_untyped(), - Self::Enum(v) => v.to_untyped(), - Self::Term(v) => v.to_untyped(), + Self::ListItem(v) => v.to_untyped(), + Self::EnumItem(v) => v.to_untyped(), + Self::TermItem(v) => v.to_untyped(), Self::Equation(v) => v.to_untyped(), Self::Math(v) => v.to_untyped(), Self::MathText(v) => v.to_untyped(), @@ -320,8 +447,8 @@ impl<'a> AstNode<'a> for Expr<'a> { Self::Float(v) => v.to_untyped(), Self::Numeric(v) => v.to_untyped(), Self::Str(v) => v.to_untyped(), - Self::Code(v) => v.to_untyped(), - Self::Content(v) => v.to_untyped(), + Self::CodeBlock(v) => v.to_untyped(), + Self::ContentBlock(v) => v.to_untyped(), Self::Array(v) => v.to_untyped(), Self::Dict(v) => v.to_untyped(), Self::Parenthesized(v) => v.to_untyped(), @@ -330,19 +457,19 @@ impl<'a> AstNode<'a> for Expr<'a> { Self::FieldAccess(v) => v.to_untyped(), Self::FuncCall(v) => v.to_untyped(), Self::Closure(v) => v.to_untyped(), - Self::Let(v) => v.to_untyped(), - Self::DestructAssign(v) => v.to_untyped(), - Self::Set(v) => v.to_untyped(), - Self::Show(v) => v.to_untyped(), + Self::LetBinding(v) => v.to_untyped(), + Self::DestructAssignment(v) => v.to_untyped(), + Self::SetRule(v) => v.to_untyped(), + Self::ShowRule(v) => v.to_untyped(), Self::Contextual(v) => v.to_untyped(), Self::Conditional(v) => v.to_untyped(), - Self::While(v) => v.to_untyped(), - Self::For(v) => v.to_untyped(), - Self::Import(v) => v.to_untyped(), - Self::Include(v) => v.to_untyped(), - Self::Break(v) => v.to_untyped(), - Self::Continue(v) => v.to_untyped(), - Self::Return(v) => v.to_untyped(), + Self::WhileLoop(v) => v.to_untyped(), + Self::ForLoop(v) => v.to_untyped(), + Self::ModuleImport(v) => v.to_untyped(), + Self::ModuleInclude(v) => v.to_untyped(), + Self::LoopBreak(v) => v.to_untyped(), + Self::LoopContinue(v) => v.to_untyped(), + Self::FuncReturn(v) => v.to_untyped(), } } } @@ -360,25 +487,25 @@ impl Expr<'_> { | Self::Float(_) | Self::Numeric(_) | Self::Str(_) - | Self::Code(_) - | Self::Content(_) + | Self::CodeBlock(_) + | Self::ContentBlock(_) | Self::Array(_) | Self::Dict(_) | Self::Parenthesized(_) | Self::FieldAccess(_) | Self::FuncCall(_) - | Self::Let(_) - | Self::Set(_) - | Self::Show(_) + | Self::LetBinding(_) + | Self::SetRule(_) + | Self::ShowRule(_) | Self::Contextual(_) | Self::Conditional(_) - | Self::While(_) - | Self::For(_) - | Self::Import(_) - | Self::Include(_) - | Self::Break(_) - | Self::Continue(_) - | Self::Return(_) + | Self::WhileLoop(_) + | Self::ForLoop(_) + | Self::ModuleImport(_) + | Self::ModuleInclude(_) + | Self::LoopBreak(_) + | Self::LoopContinue(_) + | Self::FuncReturn(_) ) } @@ -405,7 +532,7 @@ impl Default for Expr<'_> { node! { /// Plain text without markup. - Text + struct Text } impl<'a> Text<'a> { @@ -418,22 +545,22 @@ impl<'a> Text<'a> { node! { /// Whitespace in markup or math. Has at most one newline in markup, as more /// indicate a paragraph break. - Space + struct Space } node! { /// A forced line break: `\`. - Linebreak + struct Linebreak } node! { /// A paragraph break, indicated by one or multiple blank lines. - Parbreak + struct Parbreak } node! { /// An escape sequence: `\#`, `\u{1F5FA}`. - Escape + struct Escape } impl Escape<'_> { @@ -456,7 +583,7 @@ impl Escape<'_> { node! { /// A shorthand for a unicode codepoint. For example, `~` for a non-breaking /// space or `-?` for a soft hyphen. - Shorthand + struct Shorthand } impl Shorthand<'_> { @@ -482,7 +609,7 @@ impl Shorthand<'_> { node! { /// A smart quote: `'` or `"`. - SmartQuote + struct SmartQuote } impl SmartQuote<'_> { @@ -494,31 +621,31 @@ impl SmartQuote<'_> { node! { /// Strong content: `*Strong*`. - Strong + struct Strong } impl<'a> Strong<'a> { /// The contents of the strong node. pub fn body(self) -> Markup<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// Emphasized content: `_Emphasized_`. - Emph + struct Emph } impl<'a> Emph<'a> { /// The contents of the emphasis node. pub fn body(self) -> Markup<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// Raw text with optional syntax highlighting: `` `...` ``. - Raw + struct Raw } impl<'a> Raw<'a> { @@ -530,18 +657,18 @@ impl<'a> Raw<'a> { /// An optional identifier specifying the language to syntax-highlight in. pub fn lang(self) -> Option> { // Only blocky literals are supposed to contain a language. - let delim: RawDelim = self.0.cast_first_match()?; + let delim: RawDelim = self.0.try_cast_first()?; if delim.0.len() < 3 { return Option::None; } - self.0.cast_first_match() + self.0.try_cast_first() } /// Whether the raw text should be displayed in a separate block. pub fn block(self) -> bool { self.0 - .cast_first_match() + .try_cast_first() .is_some_and(|delim: RawDelim| delim.0.len() >= 3) && self.0.children().any(|e| { e.kind() == SyntaxKind::RawTrimmed && e.text().chars().any(is_newline) @@ -551,7 +678,7 @@ impl<'a> Raw<'a> { node! { /// A language tag at the start of raw element: ``typ ``. - RawLang + struct RawLang } impl<'a> RawLang<'a> { @@ -563,12 +690,12 @@ impl<'a> RawLang<'a> { node! { /// A raw delimiter in single or 3+ backticks: `` ` ``. - RawDelim + struct RawDelim } node! { /// A hyperlink: `https://typst.org`. - Link + struct Link } impl<'a> Link<'a> { @@ -580,7 +707,7 @@ impl<'a> Link<'a> { node! { /// A label: ``. - Label + struct Label } impl<'a> Label<'a> { @@ -592,7 +719,7 @@ impl<'a> Label<'a> { node! { /// A reference: `@target`, `@target[..]`. - Ref + struct Ref } impl<'a> Ref<'a> { @@ -607,19 +734,19 @@ impl<'a> Ref<'a> { /// Get the supplement. pub fn supplement(self) -> Option> { - self.0.cast_last_match() + self.0.try_cast_last() } } node! { /// A section heading: `= Introduction`. - Heading + struct Heading } impl<'a> Heading<'a> { /// The contents of the heading. pub fn body(self) -> Markup<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The section depth (number of equals signs). @@ -634,19 +761,19 @@ impl<'a> Heading<'a> { node! { /// An item in a bullet list: `- ...`. - ListItem + struct ListItem } impl<'a> ListItem<'a> { /// The contents of the list item. pub fn body(self) -> Markup<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// An item in an enumeration (numbered list): `+ ...` or `1. ...`. - EnumItem + struct EnumItem } impl<'a> EnumItem<'a> { @@ -660,36 +787,36 @@ impl<'a> EnumItem<'a> { /// The contents of the list item. pub fn body(self) -> Markup<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// An item in a term list: `/ Term: Details`. - TermItem + struct TermItem } impl<'a> TermItem<'a> { /// The term described by the item. pub fn term(self) -> Markup<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The description of the term. pub fn description(self) -> Markup<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A mathematical equation: `$x$`, `$ x^2 $`. - Equation + struct Equation } impl<'a> Equation<'a> { /// The contained math. pub fn body(self) -> Math<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// Whether the equation should be displayed as a separate block. @@ -703,7 +830,7 @@ impl<'a> Equation<'a> { node! { /// The contents of a mathematical equation: `x^2 + 1`. - Math + struct Math } impl<'a> Math<'a> { @@ -715,7 +842,7 @@ impl<'a> Math<'a> { node! { /// A lone text fragment in math: `x`, `25`, `3.1415`, `=`, `[`. - MathText + struct MathText } /// The underlying text kind. @@ -743,7 +870,7 @@ impl<'a> MathText<'a> { node! { /// An identifier in math: `pi`. - MathIdent + struct MathIdent } impl<'a> MathIdent<'a> { @@ -770,7 +897,7 @@ impl Deref for MathIdent<'_> { node! { /// A shorthand for a unicode codepoint in math: `a <= b`. - MathShorthand + struct MathShorthand } impl MathShorthand<'_> { @@ -828,40 +955,40 @@ impl MathShorthand<'_> { node! { /// An alignment point in math: `&`. - MathAlignPoint + struct MathAlignPoint } node! { /// Matched delimiters in math: `[x + y]`. - MathDelimited + struct MathDelimited } impl<'a> MathDelimited<'a> { /// The opening delimiter. pub fn open(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The contents, including the delimiters. pub fn body(self) -> Math<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The closing delimiter. pub fn close(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A base with optional attachments in math: `a_1^2`. - MathAttach + struct MathAttach } impl<'a> MathAttach<'a> { /// The base, to which things are attached. pub fn base(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The bottom attachment. @@ -892,7 +1019,7 @@ impl<'a> MathAttach<'a> { node! { /// Grouped primes in math: `a'''`. - MathPrimes + struct MathPrimes } impl MathPrimes<'_> { @@ -907,24 +1034,24 @@ impl MathPrimes<'_> { node! { /// A fraction in math: `x/2` - MathFrac + struct MathFrac } impl<'a> MathFrac<'a> { /// The numerator. pub fn num(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The denominator. pub fn denom(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A root in math: `√x`, `∛x` or `∜x`. - MathRoot + struct MathRoot } impl<'a> MathRoot<'a> { @@ -940,13 +1067,13 @@ impl<'a> MathRoot<'a> { /// The radicand. pub fn radicand(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// An identifier: `it`. - Ident + struct Ident } impl<'a> Ident<'a> { @@ -973,17 +1100,17 @@ impl Deref for Ident<'_> { node! { /// The `none` literal. - None + struct None } node! { /// The `auto` literal. - Auto + struct Auto } node! { /// A boolean: `true`, `false`. - Bool + struct Bool } impl Bool<'_> { @@ -995,7 +1122,7 @@ impl Bool<'_> { node! { /// An integer: `120`. - Int + struct Int } impl Int<'_> { @@ -1017,7 +1144,7 @@ impl Int<'_> { node! { /// A floating-point number: `1.2`, `10e-4`. - Float + struct Float } impl Float<'_> { @@ -1029,7 +1156,7 @@ impl Float<'_> { node! { /// A numeric value with a unit: `12pt`, `3cm`, `2em`, `90deg`, `50%`. - Numeric + struct Numeric } impl Numeric<'_> { @@ -1086,7 +1213,7 @@ pub enum Unit { node! { /// A quoted string: `"..."`. - Str + struct Str } impl Str<'_> { @@ -1136,19 +1263,19 @@ impl Str<'_> { node! { /// A code block: `{ let x = 1; x + 2 }`. - CodeBlock + struct CodeBlock } impl<'a> CodeBlock<'a> { /// The contained code. pub fn body(self) -> Code<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// The body of a code block. - Code + struct Code } impl<'a> Code<'a> { @@ -1160,19 +1287,19 @@ impl<'a> Code<'a> { node! { /// A content block: `[*Hi* there!]`. - ContentBlock + struct ContentBlock } impl<'a> ContentBlock<'a> { /// The contained markup. pub fn body(self) -> Markup<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// A grouped expression: `(1 + 2)`. - Parenthesized + struct Parenthesized } impl<'a> Parenthesized<'a> { @@ -1180,20 +1307,20 @@ impl<'a> Parenthesized<'a> { /// /// Should only be accessed if this is contained in an `Expr`. pub fn expr(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The wrapped pattern. /// /// Should only be accessed if this is contained in a `Pattern`. pub fn pattern(self) -> Pattern<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// An array: `(1, "hi", 12cm)`. - Array + struct Array } impl<'a> Array<'a> { @@ -1215,7 +1342,7 @@ pub enum ArrayItem<'a> { impl<'a> AstNode<'a> for ArrayItem<'a> { fn from_untyped(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Spread => node.cast().map(Self::Spread), + SyntaxKind::Spread => Some(Self::Spread(Spread(node))), _ => node.cast().map(Self::Pos), } } @@ -1230,7 +1357,7 @@ impl<'a> AstNode<'a> for ArrayItem<'a> { node! { /// A dictionary: `(thickness: 3pt, dash: "solid")`. - Dict + struct Dict } impl<'a> Dict<'a> { @@ -1254,9 +1381,9 @@ pub enum DictItem<'a> { impl<'a> AstNode<'a> for DictItem<'a> { fn from_untyped(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Named => node.cast().map(Self::Named), - SyntaxKind::Keyed => node.cast().map(Self::Keyed), - SyntaxKind::Spread => node.cast().map(Self::Spread), + SyntaxKind::Named => Some(Self::Named(Named(node))), + SyntaxKind::Keyed => Some(Self::Keyed(Keyed(node))), + SyntaxKind::Spread => Some(Self::Spread(Spread(node))), _ => Option::None, } } @@ -1272,13 +1399,13 @@ impl<'a> AstNode<'a> for DictItem<'a> { node! { /// A named pair: `thickness: 3pt`. - Named + struct Named } impl<'a> Named<'a> { /// The name: `thickness`. pub fn name(self) -> Ident<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The right-hand side of the pair: `3pt`. @@ -1286,7 +1413,7 @@ impl<'a> Named<'a> { /// This should only be accessed if this `Named` is contained in a /// `DictItem`, `Arg`, or `Param`. pub fn expr(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } /// The right-hand side of the pair as a pattern. @@ -1294,19 +1421,19 @@ impl<'a> Named<'a> { /// This should only be accessed if this `Named` is contained in a /// `Destructuring`. pub fn pattern(self) -> Pattern<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A keyed pair: `"spacy key": true`. - Keyed + struct Keyed } impl<'a> Keyed<'a> { /// The key: `"spacy key"`. pub fn key(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The right-hand side of the pair: `true`. @@ -1314,13 +1441,13 @@ impl<'a> Keyed<'a> { /// This should only be accessed if this `Keyed` is contained in a /// `DictItem`. pub fn expr(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A spread: `..x` or `..x.at(0)`. - Spread + struct Spread } impl<'a> Spread<'a> { @@ -1329,7 +1456,7 @@ impl<'a> Spread<'a> { /// This should only be accessed if this `Spread` is contained in an /// `ArrayItem`, `DictItem`, or `Arg`. pub fn expr(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The sink identifier, if present. @@ -1337,7 +1464,7 @@ impl<'a> Spread<'a> { /// This should only be accessed if this `Spread` is contained in a /// `Param` or binding `DestructuringItem`. pub fn sink_ident(self) -> Option> { - self.0.cast_first_match() + self.0.try_cast_first() } /// The sink expressions, if present. @@ -1345,13 +1472,13 @@ impl<'a> Spread<'a> { /// This should only be accessed if this `Spread` is contained in a /// `DestructuringItem`. pub fn sink_expr(self) -> Option> { - self.0.cast_first_match() + self.0.try_cast_first() } } node! { /// A unary operation: `-x`. - Unary + struct Unary } impl<'a> Unary<'a> { @@ -1365,7 +1492,7 @@ impl<'a> Unary<'a> { /// The expression to operate on: `x`. pub fn expr(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } @@ -1411,7 +1538,7 @@ impl UnOp { node! { /// A binary operation: `a + b`. - Binary + struct Binary } impl<'a> Binary<'a> { @@ -1433,12 +1560,12 @@ impl<'a> Binary<'a> { /// The left-hand side of the operation: `a`. pub fn lhs(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The right-hand side of the operation: `b`. pub fn rhs(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } @@ -1598,41 +1725,41 @@ pub enum Assoc { node! { /// A field access: `properties.age`. - FieldAccess + struct FieldAccess } impl<'a> FieldAccess<'a> { /// The expression to access the field on. pub fn target(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The name of the field. pub fn field(self) -> Ident<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// An invocation of a function or method: `f(x, y)`. - FuncCall + struct FuncCall } impl<'a> FuncCall<'a> { /// The function to call. pub fn callee(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The arguments to the function. pub fn args(self) -> Args<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A function call's argument list: `(12pt, y)`. - Args + struct Args } impl<'a> Args<'a> { @@ -1666,8 +1793,8 @@ pub enum Arg<'a> { impl<'a> AstNode<'a> for Arg<'a> { fn from_untyped(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Named => node.cast().map(Self::Named), - SyntaxKind::Spread => node.cast().map(Self::Spread), + SyntaxKind::Named => Some(Self::Named(Named(node))), + SyntaxKind::Spread => Some(Self::Spread(Spread(node))), _ => node.cast().map(Self::Pos), } } @@ -1683,7 +1810,7 @@ impl<'a> AstNode<'a> for Arg<'a> { node! { /// A closure: `(x, y) => z`. - Closure + struct Closure } impl<'a> Closure<'a> { @@ -1696,18 +1823,18 @@ impl<'a> Closure<'a> { /// The parameter bindings. pub fn params(self) -> Params<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The body of the closure. pub fn body(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A closure's parameters: `(x, y)`. - Params + struct Params } impl<'a> Params<'a> { @@ -1731,8 +1858,8 @@ pub enum Param<'a> { impl<'a> AstNode<'a> for Param<'a> { fn from_untyped(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Named => node.cast().map(Self::Named), - SyntaxKind::Spread => node.cast().map(Self::Spread), + SyntaxKind::Named => Some(Self::Named(Named(node))), + SyntaxKind::Spread => Some(Self::Spread(Spread(node))), _ => node.cast().map(Self::Pos), } } @@ -1762,9 +1889,9 @@ pub enum Pattern<'a> { impl<'a> AstNode<'a> for Pattern<'a> { fn from_untyped(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Underscore => node.cast().map(Self::Placeholder), - SyntaxKind::Parenthesized => node.cast().map(Self::Parenthesized), - SyntaxKind::Destructuring => node.cast().map(Self::Destructuring), + SyntaxKind::Underscore => Some(Self::Placeholder(Underscore(node))), + SyntaxKind::Parenthesized => Some(Self::Parenthesized(Parenthesized(node))), + SyntaxKind::Destructuring => Some(Self::Destructuring(Destructuring(node))), _ => node.cast().map(Self::Normal), } } @@ -1799,12 +1926,12 @@ impl Default for Pattern<'_> { node! { /// An underscore: `_` - Underscore + struct Underscore } node! { /// A destructuring pattern: `x` or `(x, _, ..y)`. - Destructuring + struct Destructuring } impl<'a> Destructuring<'a> { @@ -1841,8 +1968,8 @@ pub enum DestructuringItem<'a> { impl<'a> AstNode<'a> for DestructuringItem<'a> { fn from_untyped(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Named => node.cast().map(Self::Named), - SyntaxKind::Spread => node.cast().map(Self::Spread), + SyntaxKind::Named => Some(Self::Named(Named(node))), + SyntaxKind::Spread => Some(Self::Spread(Spread(node))), _ => node.cast().map(Self::Pattern), } } @@ -1858,7 +1985,7 @@ impl<'a> AstNode<'a> for DestructuringItem<'a> { node! { /// A let binding: `let x = 1`. - LetBinding + struct LetBinding } /// The kind of a let binding, either a normal one or a closure. @@ -1883,11 +2010,11 @@ impl<'a> LetBindingKind<'a> { impl<'a> LetBinding<'a> { /// The kind of the let binding. pub fn kind(self) -> LetBindingKind<'a> { - match self.0.cast_first_match::() { - Some(Pattern::Normal(Expr::Closure(closure))) => { + match self.0.cast_first() { + Pattern::Normal(Expr::Closure(closure)) => { LetBindingKind::Closure(closure.name().unwrap_or_default()) } - pattern => LetBindingKind::Normal(pattern.unwrap_or_default()), + pattern => LetBindingKind::Normal(pattern), } } @@ -1897,43 +2024,43 @@ impl<'a> LetBinding<'a> { LetBindingKind::Normal(Pattern::Normal(_) | Pattern::Parenthesized(_)) => { self.0.children().filter_map(SyntaxNode::cast).nth(1) } - LetBindingKind::Normal(_) => self.0.cast_first_match(), - LetBindingKind::Closure(_) => self.0.cast_first_match(), + LetBindingKind::Normal(_) => self.0.try_cast_first(), + LetBindingKind::Closure(_) => self.0.try_cast_first(), } } } node! { /// An assignment expression `(x, y) = (1, 2)`. - DestructAssignment + struct DestructAssignment } impl<'a> DestructAssignment<'a> { /// The pattern of the assignment. pub fn pattern(self) -> Pattern<'a> { - self.0.cast_first_match::().unwrap_or_default() + self.0.cast_first() } /// The expression that is assigned. pub fn value(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A set rule: `set text(...)`. - SetRule + struct SetRule } impl<'a> SetRule<'a> { /// The function to set style properties for. pub fn target(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The style properties to set. pub fn args(self) -> Args<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } /// A condition under which the set rule applies. @@ -1947,7 +2074,7 @@ impl<'a> SetRule<'a> { node! { /// A show rule: `show heading: it => emph(it.body)`. - ShowRule + struct ShowRule } impl<'a> ShowRule<'a> { @@ -1962,31 +2089,31 @@ impl<'a> ShowRule<'a> { /// The transformation recipe. pub fn transform(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A contextual expression: `context text.lang`. - Contextual + struct Contextual } impl<'a> Contextual<'a> { /// The expression which depends on the context. pub fn body(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// An if-else conditional: `if x { y } else { z }`. - Conditional + struct Conditional } impl<'a> Conditional<'a> { /// The condition which selects the body to evaluate. pub fn condition(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The expression to evaluate if the condition is true. @@ -2006,30 +2133,30 @@ impl<'a> Conditional<'a> { node! { /// A while loop: `while x { y }`. - WhileLoop + struct WhileLoop } impl<'a> WhileLoop<'a> { /// The condition which selects whether to evaluate the body. pub fn condition(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The expression to evaluate while the condition is true. pub fn body(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A for loop: `for x in y { z }`. - ForLoop + struct ForLoop } impl<'a> ForLoop<'a> { /// The pattern to assign to. pub fn pattern(self) -> Pattern<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The expression to iterate over. @@ -2043,19 +2170,19 @@ impl<'a> ForLoop<'a> { /// The expression to evaluate for each iteration. pub fn body(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A module import: `import "utils.typ": a, b, c`. - ModuleImport + struct ModuleImport } impl<'a> ModuleImport<'a> { /// The module or path from which the items should be imported. pub fn source(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The items to be imported. @@ -2135,7 +2262,7 @@ pub enum Imports<'a> { node! { /// Items to import from a module: `a, b, c`. - ImportItems + struct ImportItems } impl<'a> ImportItems<'a> { @@ -2151,7 +2278,7 @@ impl<'a> ImportItems<'a> { node! { /// A path to a submodule's imported name: `a.b.c`. - ImportItemPath + struct ImportItemPath } impl<'a> ImportItemPath<'a> { @@ -2162,7 +2289,7 @@ impl<'a> ImportItemPath<'a> { /// The name of the imported item. This is the last segment in the path. pub fn name(self) -> Ident<'a> { - self.iter().last().unwrap_or_default() + self.0.cast_last() } } @@ -2207,13 +2334,13 @@ impl<'a> ImportItem<'a> { node! { /// A renamed import item: `a as d` - RenamedImportItem + struct RenamedImportItem } impl<'a> RenamedImportItem<'a> { /// The path to the imported item. pub fn path(self) -> ImportItemPath<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The original name of the imported item (`a` in `a as d` or `c.b.a as d`). @@ -2223,45 +2350,41 @@ impl<'a> RenamedImportItem<'a> { /// The new name of the imported item (`d` in `a as d`). pub fn new_name(self) -> Ident<'a> { - self.0 - .children() - .filter_map(SyntaxNode::cast) - .last() - .unwrap_or_default() + self.0.cast_last() } } node! { /// A module include: `include "chapter1.typ"`. - ModuleInclude + struct ModuleInclude } impl<'a> ModuleInclude<'a> { /// The module or path from which the content should be included. pub fn source(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A break from a loop: `break`. - LoopBreak + struct LoopBreak } node! { /// A continue in a loop: `continue`. - LoopContinue + struct LoopContinue } node! { /// A return from a function: `return`, `return x + 1`. - FuncReturn + struct FuncReturn } impl<'a> FuncReturn<'a> { /// The expression to return. pub fn body(self) -> Option> { - self.0.cast_last_match() + self.0.try_cast_last() } } diff --git a/crates/typst-syntax/src/node.rs b/crates/typst-syntax/src/node.rs index fde2eaca0..948657ca4 100644 --- a/crates/typst-syntax/src/node.rs +++ b/crates/typst-syntax/src/node.rs @@ -5,7 +5,6 @@ use std::sync::Arc; use ecow::{eco_format, eco_vec, EcoString, EcoVec}; -use crate::ast::AstNode; use crate::{FileId, Span, SyntaxKind}; /// A node in the untyped syntax tree. @@ -119,26 +118,6 @@ impl SyntaxNode { } } - /// Whether the node can be cast to the given AST node. - pub fn is<'a, T: AstNode<'a>>(&'a self) -> bool { - self.cast::().is_some() - } - - /// Try to convert the node to a typed AST node. - pub fn cast<'a, T: AstNode<'a>>(&'a self) -> Option { - T::from_untyped(self) - } - - /// Cast the first child that can cast to the AST type `T`. - pub fn cast_first_match<'a, T: AstNode<'a>>(&'a self) -> Option { - self.children().find_map(Self::cast) - } - - /// Cast the last child that can cast to the AST type `T`. - pub fn cast_last_match<'a, T: AstNode<'a>>(&'a self) -> Option { - self.children().rev().find_map(Self::cast) - } - /// Whether the node or its children contain an error. pub fn erroneous(&self) -> bool { match &self.0 { From 66679920b25a80bf106148b59642fbae166e0d7a Mon Sep 17 00:00:00 2001 From: Tijme <68817281+7ijme@users.noreply.github.com> Date: Mon, 3 Mar 2025 10:32:06 +0100 Subject: [PATCH 051/172] Fix docs example with type/string comparison (#5987) --- crates/typst-library/src/loading/xml.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/loading/xml.rs b/crates/typst-library/src/loading/xml.rs index daccd02fc..e76c4e9cf 100644 --- a/crates/typst-library/src/loading/xml.rs +++ b/crates/typst-library/src/loading/xml.rs @@ -34,14 +34,14 @@ use crate::loading::{DataSource, Load, Readable}; /// let author = find-child(elem, "author") /// let pars = find-child(elem, "content") /// -/// heading(title.children.first()) +/// [= #title.children.first()] /// text(10pt, weight: "medium")[ /// Published by /// #author.children.first() /// ] /// /// for p in pars.children { -/// if (type(p) == "dictionary") { +/// if type(p) == dictionary { /// parbreak() /// p.children.first() /// } @@ -50,7 +50,7 @@ use crate::loading::{DataSource, Load, Readable}; /// /// #let data = xml("example.xml") /// #for elem in data.first().children { -/// if (type(elem) == "dictionary") { +/// if type(elem) == dictionary { /// article(elem) /// } /// } From d4def0996235a791291bb39a570ae301feea099c Mon Sep 17 00:00:00 2001 From: F2011 <110890521+F2011@users.noreply.github.com> Date: Mon, 3 Mar 2025 21:23:29 +1000 Subject: [PATCH 052/172] Correct typo (#5971) --- docs/guides/guide-for-latex-users.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/guide-for-latex-users.md b/docs/guides/guide-for-latex-users.md index 5137ae1a9..fffa6c521 100644 --- a/docs/guides/guide-for-latex-users.md +++ b/docs/guides/guide-for-latex-users.md @@ -447,7 +447,7 @@ document. To let a function style your whole document, the show rule processes everything that comes after it and calls the function specified after the colon with the result as an argument. The `.with` part is a _method_ that takes the `conf` -function and pre-configures some if its arguments before passing it on to the +function and pre-configures some of its arguments before passing it on to the show rule. From bf0d45e2c0086ba2ae71eeebbfd26db7dfe7692f Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Mon, 3 Mar 2025 14:31:39 +0300 Subject: [PATCH 053/172] Make `array.chunks` example more readable (#5975) --- crates/typst-library/src/foundations/array.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-library/src/foundations/array.rs b/crates/typst-library/src/foundations/array.rs index aad7266bc..e81b9e645 100644 --- a/crates/typst-library/src/foundations/array.rs +++ b/crates/typst-library/src/foundations/array.rs @@ -751,7 +751,7 @@ impl Array { /// /// ```example /// #let array = (1, 2, 3, 4, 5, 6, 7, 8) - /// #array.chunks(3) + /// #array.chunks(3) \ /// #array.chunks(3, exact: true) /// ``` #[func] From 9a6ffbc7db95eff2aedd8028b8969a744717aaa4 Mon Sep 17 00:00:00 2001 From: andis854 <123587604+andis854@users.noreply.github.com> Date: Mon, 3 Mar 2025 12:40:58 +0100 Subject: [PATCH 054/172] Added snap to installation instructions (#5984) --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a5d20d2e6..41f465152 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,9 @@ Typst's CLI is available from different sources: - You can install Typst through different package managers. Note that the versions in the package managers might lag behind the latest release. - - Linux: View [Typst on Repology][repology] + - Linux: + - View [Typst on Repology][repology] + - View [Typst's Snap][snap] - macOS: `brew install typst` - Windows: `winget install --id Typst.Typst` @@ -254,3 +256,4 @@ instant preview. To achieve these goals, we follow three core design principles: [contributing]: https://github.com/typst/typst/blob/main/CONTRIBUTING.md [packages]: https://github.com/typst/packages/ [`comemo`]: https://github.com/typst/comemo/ +[snap]: https://snapcraft.io/typst From 8820a00beb08b7253a99a7cf66bb752cd181bb03 Mon Sep 17 00:00:00 2001 From: 3w36zj6 <52315048+3w36zj6@users.noreply.github.com> Date: Mon, 3 Mar 2025 20:50:47 +0900 Subject: [PATCH 055/172] Respect `quotes: false` in inline quote (#5991) Co-authored-by: Laurenz --- crates/typst-library/src/model/quote.rs | 2 +- .../ref/issue-5536-quote-inline-quotes-false.png | Bin 0 -> 389 bytes tests/suite/model/quote.typ | 3 +++ 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 tests/ref/issue-5536-quote-inline-quotes-false.png diff --git a/crates/typst-library/src/model/quote.rs b/crates/typst-library/src/model/quote.rs index 919ab12c7..cd45eec8e 100644 --- a/crates/typst-library/src/model/quote.rs +++ b/crates/typst-library/src/model/quote.rs @@ -161,7 +161,7 @@ impl Show for Packed { let block = self.block(styles); let html = TargetElem::target_in(styles).is_html(); - if self.quotes(styles) == Smart::Custom(true) || !block { + if self.quotes(styles).unwrap_or(!block) { let quotes = SmartQuotes::get( SmartQuoteElem::quotes_in(styles), TextElem::lang_in(styles), diff --git a/tests/ref/issue-5536-quote-inline-quotes-false.png b/tests/ref/issue-5536-quote-inline-quotes-false.png new file mode 100644 index 0000000000000000000000000000000000000000..e7b29d7120b74fd980a69fb8ee6139efd8e8b88c GIT binary patch literal 389 zcmV;00eb$4P)ONM)uhjEBn0gdOb&EAX z#FoiDuO9`jFZe%q@y6$2yDO$F%DM~`{yt^8?E9Y`bLWFBzS}nO7-U9}8)bamw+aHC2;>7#r+*#BL>OT_y|75>3{%|Gn zzt^q*i!%N{{vUoG=$d_5K*{`>-}Em{srX-b_@?iFr&Ir}e@&k7|NkMXSiB- Date: Mon, 3 Mar 2025 14:10:58 +0100 Subject: [PATCH 056/172] Run tests on 32-bit via Ubuntu multilib (#5937) Co-authored-by: Laurenz --- .github/workflows/ci.yml | 24 +++++++++++++++++++----- tests/suite/model/numbering.typ | 1 - 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f0ada9f9..41f17d137 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ env: RUSTFLAGS: "-Dwarnings" RUSTDOCFLAGS: "-Dwarnings" TYPST_TESTS_EXTENDED: true + PKG_CONFIG_i686-unknown-linux-gnu: /usr/bin/i686-linux-gnu-pkgconf jobs: # This allows us to have one branch protection rule for the full test matrix. @@ -27,30 +28,43 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest] + bits: [64] + include: + - os: ubuntu-latest + bits: 32 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 + - if: startsWith(matrix.os, 'ubuntu-') && matrix.bits == 32 + run: | + sudo dpkg --add-architecture i386 + sudo apt update + sudo apt install -y gcc-multilib libssl-dev:i386 pkg-config:i386 - uses: dtolnay/rust-toolchain@1.85.0 + with: + targets: ${{ matrix.bits == 32 && 'i686-unknown-linux-gnu' || '' }} - uses: Swatinem/rust-cache@v2 - - run: cargo test --workspace --no-run - - run: cargo test --workspace --no-fail-fast + with: + key: ${{ matrix.bits }} + - run: cargo test --workspace --no-run ${{ matrix.bits == 32 && '--target i686-unknown-linux-gnu' || '' }} + - run: cargo test --workspace --no-fail-fast ${{ matrix.bits == 32 && '--target i686-unknown-linux-gnu' || '' }} - name: Upload rendered test output if: failure() uses: actions/upload-artifact@v4 with: - name: tests-rendered-${{ matrix.os }} + name: tests-rendered-${{ matrix.os }}-${{ matrix.bits }} path: tests/store/render/** retention-days: 3 - name: Update test artifacts if: failure() run: | - cargo test --workspace --test tests -- --update + cargo test --workspace --test tests ${{ matrix.bits == 32 && '--target i686-unknown-linux-gnu' || '' }} -- --update echo 'updated_artifacts=1' >> "$GITHUB_ENV" - name: Upload updated reference output (for use if the test changes are desired) if: failure() && env.updated_artifacts uses: actions/upload-artifact@v4 with: - name: tests-updated-${{ matrix.os }} + name: tests-updated-${{ matrix.os }}-${{ matrix.bits }} path: tests/ref/** retention-days: 3 diff --git a/tests/suite/model/numbering.typ b/tests/suite/model/numbering.typ index 6af989ff1..ccd7cfc18 100644 --- a/tests/suite/model/numbering.typ +++ b/tests/suite/model/numbering.typ @@ -49,7 +49,6 @@ 2000000001, "βΜκʹ, αʹ", 2000010001, "βΜκʹ, αΜαʹ, αʹ", 2056839184, "βΜκʹ, αΜ͵εχπγ, ͵θρπδ", - 12312398676, "βΜρκγʹ, αΜ͵ασλθ, ͵ηχοϛ", ) #t( pat: sym.Alpha, From 6271cdceae146efe75942ebde7712a942627c42f Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Tue, 4 Mar 2025 12:33:39 +0300 Subject: [PATCH 057/172] Fix debug implementation of Recipe (#5997) --- crates/typst-library/src/foundations/styles.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/typst-library/src/foundations/styles.rs b/crates/typst-library/src/foundations/styles.rs index 983803300..d124f2c87 100644 --- a/crates/typst-library/src/foundations/styles.rs +++ b/crates/typst-library/src/foundations/styles.rs @@ -471,7 +471,8 @@ impl Debug for Recipe { selector.fmt(f)?; f.write_str(", ")?; } - self.transform.fmt(f) + self.transform.fmt(f)?; + f.write_str(")") } } From e1a9166e1d6a24076796efaf4eec073567bfb037 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 7 Mar 2025 09:22:42 +0100 Subject: [PATCH 058/172] Hotfix for labels on symbols (#6015) --- crates/typst-realize/src/lib.rs | 5 ++++- tests/ref/issue-5930-symbol-label.png | Bin 0 -> 243 bytes tests/suite/symbols/symbol.typ | 4 ++++ 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 tests/ref/issue-5930-symbol-label.png diff --git a/crates/typst-realize/src/lib.rs b/crates/typst-realize/src/lib.rs index 50685a962..151ae76ba 100644 --- a/crates/typst-realize/src/lib.rs +++ b/crates/typst-realize/src/lib.rs @@ -326,7 +326,10 @@ fn visit_math_rules<'a>( // Symbols in non-math content transparently convert to `TextElem` so we // don't have to handle them in non-math layout. if let Some(elem) = content.to_packed::() { - let text = TextElem::packed(elem.text).spanned(elem.span()); + let mut text = TextElem::packed(elem.text).spanned(elem.span()); + if let Some(label) = elem.label() { + text.set_label(label); + } visit(s, s.store(text), styles)?; return Ok(true); } diff --git a/tests/ref/issue-5930-symbol-label.png b/tests/ref/issue-5930-symbol-label.png new file mode 100644 index 0000000000000000000000000000000000000000..e8127aa0cc494f76def5a178a1985e2ceb294f94 GIT binary patch literal 243 zcmV@g#X!k|JZr|%uUb4ul2)7>$^n8xuD{*KiR4}*s40knJ~PO zEV76vgJ>{#SQT(i1!hG6U}$ZP0001HNklEz3` t-N$in$PcO^<-AY_xoBX~AE|Thn+MaJ1Fvqfmev3O002ovPDHLkV1n1FcTE5Q literal 0 HcmV?d00001 diff --git a/tests/suite/symbols/symbol.typ b/tests/suite/symbols/symbol.typ index 6d2513c1f..5bc2cafae 100644 --- a/tests/suite/symbols/symbol.typ +++ b/tests/suite/symbols/symbol.typ @@ -151,3 +151,7 @@ --- symbol-sect-deprecated --- // Warning: 5-9 `sect` is deprecated, use `inter` instead $ A sect B = A inter B $ + +--- issue-5930-symbol-label --- +#emoji.face +#context test(query().first().text, "😀") From 99b7d2898e802356c66fed01179d42cab9198617 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 7 Mar 2025 09:47:56 +0100 Subject: [PATCH 059/172] Replace `par` function call in tutorial (#6023) --- docs/tutorial/2-formatting.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/tutorial/2-formatting.md b/docs/tutorial/2-formatting.md index fabb544f4..a8c72cefe 100644 --- a/docs/tutorial/2-formatting.md +++ b/docs/tutorial/2-formatting.md @@ -13,11 +13,11 @@ your report using Typst's styling system. As we have seen in the previous chapter, Typst has functions that _insert_ content (e.g. the [`image`] function) and others that _manipulate_ content that they received as arguments (e.g. the [`align`] function). The first impulse you -might have when you want, for example, to justify the report, could be to look +might have when you want, for example, to change the font, could be to look for a function that does that and wrap the complete document in it. ```example -#par(justify: true)[ +#text(font: "New Computer Modern")[ = Background In the case of glaciers, fluid dynamics principles can be used @@ -37,9 +37,9 @@ do in Typst, there is special syntax for it: Instead of putting the content inside of the argument list, you can write it in square brackets directly after the normal arguments, saving on punctuation. -As seen above, that works. The [`par`] function justifies all paragraphs within -it. However, wrapping the document in countless functions and applying styles -selectively and in-situ can quickly become cumbersome. +As seen above, that works. With the [`text`] function, we can adjust the font +for all text within it. However, wrapping the document in countless functions +and applying styles selectively and in-situ can quickly become cumbersome. Fortunately, Typst has a more elegant solution. With _set rules,_ you can apply style properties to all occurrences of some kind of content. You write a set @@ -47,7 +47,9 @@ rule by entering the `{set}` keyword, followed by the name of the function whose properties you want to set, and a list of arguments in parentheses. ```example -#set par(justify: true) +#set text( + font: "New Computer Modern" +) = Background In the case of glaciers, fluid From e0b2c32a8ee6fd5bb43e7f8973972d76bdef573b Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Fri, 7 Mar 2025 10:05:16 +0100 Subject: [PATCH 060/172] Mention that `sym.ohm` was removed in the 0.13.0 changelog (#6017) Co-authored-by: Laurenz --- docs/changelog/0.13.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog/0.13.0.md b/docs/changelog/0.13.0.md index 6c2fe4275..50e7fca72 100644 --- a/docs/changelog/0.13.0.md +++ b/docs/changelog/0.13.0.md @@ -294,7 +294,6 @@ feature flag. `errorbar.diamond.stroked`, `errorbar.diamond.filled`, `errorbar.circle.stroked`, `errorbar.circle.filled` - `numero` - - `Omega.inv` - Renamed - `ohm.inv` to `Omega.inv` - Changed codepoint @@ -308,6 +307,7 @@ feature flag. - `degree.c` in favor of `°C` (`[$upright(°C)$]` or `[$upright(degree C)$]` in math) - `degree.f` in favor of `°F` (`[$upright(°F)$]` or `[$upright(degree F)$]` in math) - `kelvin` in favor of just K (`[$upright(K)$]` in math) + - `ohm` in favor of `Omega` ## Deprecations - The [`path`] function in favor of the [`curve`] function From 476c2df312e8c80ff455a355ce1e987312444cb8 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 7 Mar 2025 10:17:11 +0100 Subject: [PATCH 061/172] Mark breaking symbol changes as breaking in 0.13.0 changelog (#6024) --- docs/changelog/0.13.0.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changelog/0.13.0.md b/docs/changelog/0.13.0.md index 50e7fca72..1cca48aa2 100644 --- a/docs/changelog/0.13.0.md +++ b/docs/changelog/0.13.0.md @@ -294,16 +294,16 @@ feature flag. `errorbar.diamond.stroked`, `errorbar.diamond.filled`, `errorbar.circle.stroked`, `errorbar.circle.filled` - `numero` -- Renamed +- Renamed **(Breaking change)** - `ohm.inv` to `Omega.inv` -- Changed codepoint +- Changed codepoint **(Breaking change)** - `angle.l.double` from `《` to `⟪` - `angle.r.double` from `》` to `⟫` - `angstrom` from U+212B (`Å`) to U+00C5 (`Å`) - Deprecated - `sect` and all its variants in favor of `inter` - `integral.sect` in favor of `integral.inter` -- Removed +- Removed **(Breaking change)** - `degree.c` in favor of `°C` (`[$upright(°C)$]` or `[$upright(degree C)$]` in math) - `degree.f` in favor of `°F` (`[$upright(°F)$]` or `[$upright(degree F)$]` in math) - `kelvin` in favor of just K (`[$upright(K)$]` in math) From 8d3488a07df83760cafa1e17bf4ed4de415a4d69 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 7 Mar 2025 11:03:52 +0100 Subject: [PATCH 062/172] 0.13.1 changelog (#6025) --- docs/changelog/0.13.1.md | 26 ++++++++++++++++++++++++++ docs/changelog/welcome.md | 1 + docs/src/lib.rs | 1 + 3 files changed, 28 insertions(+) create mode 100644 docs/changelog/0.13.1.md diff --git a/docs/changelog/0.13.1.md b/docs/changelog/0.13.1.md new file mode 100644 index 000000000..15bd9f6d8 --- /dev/null +++ b/docs/changelog/0.13.1.md @@ -0,0 +1,26 @@ +--- +title: 0.13.1 +description: Changes in Typst 0.13.1 +--- + +# Version 0.13.1 + +## Command Line Interface +- Fixed high CPU usage for `typst watch` on Linux. Depending on the project + size, CPU usage would spike for varying amounts of time. This bug appeared + with 0.13.0 due to a behavioral change in the inotify file watching backend. + +## HTML export +- Fixed export of tables with [gutters]($table.gutter) +- Fixed usage of `` and `` element within [context] +- Fixed querying of [metadata] next to `` and `` element + +## Visualization +- Fixed [curves]($curve) with multiple non-closed components + +## Introspection +- Fixed a regression where labelled [symbols]($symbol) could not be + [queried]($query) by label + +## Deprecations +- Fixed false positives in deprecation warnings for type/str comparisons diff --git a/docs/changelog/welcome.md b/docs/changelog/welcome.md index 8fb85f870..7611f1c44 100644 --- a/docs/changelog/welcome.md +++ b/docs/changelog/welcome.md @@ -10,6 +10,7 @@ forward. This section documents all changes to Typst since its initial public release. ## Versions +- [Typst 0.13.1]($changelog/0.13.1) - [Typst 0.13.0]($changelog/0.13.0) - [Typst 0.12.0]($changelog/0.12.0) - [Typst 0.11.1]($changelog/0.11.1) diff --git a/docs/src/lib.rs b/docs/src/lib.rs index e9771738d..091bb1b24 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -188,6 +188,7 @@ fn changelog_pages(resolver: &dyn Resolver) -> PageModel { let mut page = md_page(resolver, resolver.base(), load!("changelog/welcome.md")); let base = format!("{}changelog/", resolver.base()); page.children = vec![ + md_page(resolver, &base, load!("changelog/0.13.1.md")), md_page(resolver, &base, load!("changelog/0.13.0.md")), md_page(resolver, &base, load!("changelog/0.12.0.md")), md_page(resolver, &base, load!("changelog/0.11.1.md")), From db9a83d9fc2c9928bcfbc78ccafc2a799ccca2f0 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 7 Mar 2025 11:19:12 +0100 Subject: [PATCH 063/172] Bump version on main The tagged commit itself is on the 0.13 branch. --- Cargo.lock | 48 ++++++++++++++++++++-------------------- Cargo.toml | 38 +++++++++++++++---------------- docs/changelog/0.13.1.md | 5 ++++- 3 files changed, 47 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 86f04ee52..85698d8bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler2" @@ -2735,7 +2735,7 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" [[package]] name = "typst" -version = "0.13.0" +version = "0.13.1" dependencies = [ "comemo", "ecow", @@ -2752,12 +2752,12 @@ dependencies = [ [[package]] name = "typst-assets" -version = "0.13.0" -source = "git+https://github.com/typst/typst-assets?rev=fa0f8a4#fa0f8a438cc4bc2113cc0aa3304cd68cdc2bc020" +version = "0.13.1" +source = "git+https://github.com/typst/typst-assets?rev=ab1295f#ab1295ff896444e51902e03c2669955e1d73604a" [[package]] name = "typst-cli" -version = "0.13.0" +version = "0.13.1" dependencies = [ "chrono", "clap", @@ -2802,12 +2802,12 @@ dependencies = [ [[package]] name = "typst-dev-assets" -version = "0.13.0" -source = "git+https://github.com/typst/typst-dev-assets?rev=61aebe9#61aebe9575a5abff889f76d73c7b01dc8e17e340" +version = "0.13.1" +source = "git+https://github.com/typst/typst-dev-assets?rev=9879589#9879589f4b3247b12c5e694d0d7fa86d4d8a198e" [[package]] name = "typst-docs" -version = "0.13.0" +version = "0.13.1" dependencies = [ "clap", "ecow", @@ -2830,7 +2830,7 @@ dependencies = [ [[package]] name = "typst-eval" -version = "0.13.0" +version = "0.13.1" dependencies = [ "comemo", "ecow", @@ -2848,7 +2848,7 @@ dependencies = [ [[package]] name = "typst-fuzz" -version = "0.13.0" +version = "0.13.1" dependencies = [ "comemo", "libfuzzer-sys", @@ -2860,7 +2860,7 @@ dependencies = [ [[package]] name = "typst-html" -version = "0.13.0" +version = "0.13.1" dependencies = [ "comemo", "ecow", @@ -2874,7 +2874,7 @@ dependencies = [ [[package]] name = "typst-ide" -version = "0.13.0" +version = "0.13.1" dependencies = [ "comemo", "ecow", @@ -2891,7 +2891,7 @@ dependencies = [ [[package]] name = "typst-kit" -version = "0.13.0" +version = "0.13.1" dependencies = [ "dirs", "ecow", @@ -2914,7 +2914,7 @@ dependencies = [ [[package]] name = "typst-layout" -version = "0.13.0" +version = "0.13.1" dependencies = [ "az", "bumpalo", @@ -2944,7 +2944,7 @@ dependencies = [ [[package]] name = "typst-library" -version = "0.13.0" +version = "0.13.1" dependencies = [ "az", "bitflags 2.8.0", @@ -3005,7 +3005,7 @@ dependencies = [ [[package]] name = "typst-macros" -version = "0.13.0" +version = "0.13.1" dependencies = [ "heck", "proc-macro2", @@ -3015,7 +3015,7 @@ dependencies = [ [[package]] name = "typst-pdf" -version = "0.13.0" +version = "0.13.1" dependencies = [ "arrayvec", "base64", @@ -3041,7 +3041,7 @@ dependencies = [ [[package]] name = "typst-realize" -version = "0.13.0" +version = "0.13.1" dependencies = [ "arrayvec", "bumpalo", @@ -3057,7 +3057,7 @@ dependencies = [ [[package]] name = "typst-render" -version = "0.13.0" +version = "0.13.1" dependencies = [ "bytemuck", "comemo", @@ -3073,7 +3073,7 @@ dependencies = [ [[package]] name = "typst-svg" -version = "0.13.0" +version = "0.13.1" dependencies = [ "base64", "comemo", @@ -3091,7 +3091,7 @@ dependencies = [ [[package]] name = "typst-syntax" -version = "0.13.0" +version = "0.13.1" dependencies = [ "ecow", "serde", @@ -3107,7 +3107,7 @@ dependencies = [ [[package]] name = "typst-tests" -version = "0.13.0" +version = "0.13.1" dependencies = [ "clap", "comemo", @@ -3132,7 +3132,7 @@ dependencies = [ [[package]] name = "typst-timing" -version = "0.13.0" +version = "0.13.1" dependencies = [ "parking_lot", "serde", @@ -3142,7 +3142,7 @@ dependencies = [ [[package]] name = "typst-utils" -version = "0.13.0" +version = "0.13.1" dependencies = [ "once_cell", "portable-atomic", diff --git a/Cargo.toml b/Cargo.toml index f643856e1..0bfd92821 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ default-members = ["crates/typst-cli"] resolver = "2" [workspace.package] -version = "0.13.0" +version = "0.13.1" rust-version = "1.83" # also change in ci.yml authors = ["The Typst Project Developers"] edition = "2021" @@ -16,24 +16,24 @@ keywords = ["typst"] readme = "README.md" [workspace.dependencies] -typst = { path = "crates/typst", version = "0.13.0" } -typst-cli = { path = "crates/typst-cli", version = "0.13.0" } -typst-eval = { path = "crates/typst-eval", version = "0.13.0" } -typst-html = { path = "crates/typst-html", version = "0.13.0" } -typst-ide = { path = "crates/typst-ide", version = "0.13.0" } -typst-kit = { path = "crates/typst-kit", version = "0.13.0" } -typst-layout = { path = "crates/typst-layout", version = "0.13.0" } -typst-library = { path = "crates/typst-library", version = "0.13.0" } -typst-macros = { path = "crates/typst-macros", version = "0.13.0" } -typst-pdf = { path = "crates/typst-pdf", version = "0.13.0" } -typst-realize = { path = "crates/typst-realize", version = "0.13.0" } -typst-render = { path = "crates/typst-render", version = "0.13.0" } -typst-svg = { path = "crates/typst-svg", version = "0.13.0" } -typst-syntax = { path = "crates/typst-syntax", version = "0.13.0" } -typst-timing = { path = "crates/typst-timing", version = "0.13.0" } -typst-utils = { path = "crates/typst-utils", version = "0.13.0" } -typst-assets = { git = "https://github.com/typst/typst-assets", rev = "fa0f8a4" } -typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "61aebe9" } +typst = { path = "crates/typst", version = "0.13.1" } +typst-cli = { path = "crates/typst-cli", version = "0.13.1" } +typst-eval = { path = "crates/typst-eval", version = "0.13.1" } +typst-html = { path = "crates/typst-html", version = "0.13.1" } +typst-ide = { path = "crates/typst-ide", version = "0.13.1" } +typst-kit = { path = "crates/typst-kit", version = "0.13.1" } +typst-layout = { path = "crates/typst-layout", version = "0.13.1" } +typst-library = { path = "crates/typst-library", version = "0.13.1" } +typst-macros = { path = "crates/typst-macros", version = "0.13.1" } +typst-pdf = { path = "crates/typst-pdf", version = "0.13.1" } +typst-realize = { path = "crates/typst-realize", version = "0.13.1" } +typst-render = { path = "crates/typst-render", version = "0.13.1" } +typst-svg = { path = "crates/typst-svg", version = "0.13.1" } +typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" } +typst-timing = { path = "crates/typst-timing", version = "0.13.1" } +typst-utils = { path = "crates/typst-utils", version = "0.13.1" } +typst-assets = { git = "https://github.com/typst/typst-assets", rev = "ab1295f" } +typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "9879589" } arrayvec = "0.7.4" az = "1.2" base64 = "0.22" diff --git a/docs/changelog/0.13.1.md b/docs/changelog/0.13.1.md index 15bd9f6d8..caf523e1c 100644 --- a/docs/changelog/0.13.1.md +++ b/docs/changelog/0.13.1.md @@ -3,7 +3,7 @@ title: 0.13.1 description: Changes in Typst 0.13.1 --- -# Version 0.13.1 +# Version 0.13.1 (March 7, 2025) ## Command Line Interface - Fixed high CPU usage for `typst watch` on Linux. Depending on the project @@ -24,3 +24,6 @@ description: Changes in Typst 0.13.1 ## Deprecations - Fixed false positives in deprecation warnings for type/str comparisons + +## Contributors + From e66e190a21be9fdb62191d023362154d7c24ffa9 Mon Sep 17 00:00:00 2001 From: Ludovico Gerardi Date: Mon, 10 Mar 2025 12:39:30 +0100 Subject: [PATCH 064/172] Fix typo in docs (#6034) --- crates/typst-library/src/foundations/func.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-library/src/foundations/func.rs b/crates/typst-library/src/foundations/func.rs index 66c6b70a5..27eb34eac 100644 --- a/crates/typst-library/src/foundations/func.rs +++ b/crates/typst-library/src/foundations/func.rs @@ -112,7 +112,7 @@ use crate::foundations::{ /// it into another file by writing `{import "foo.typ": alert}`. /// /// # Unnamed functions { #unnamed } -/// You can also created an unnamed function without creating a binding by +/// You can also create an unnamed function without creating a binding by /// specifying a parameter list followed by `=>` and the function body. If your /// function has just one parameter, the parentheses around the parameter list /// are optional. Unnamed functions are mainly useful for show rules, but also From bd531e08dc3dbe26ac779d5730bf0814800b7de9 Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Mon, 10 Mar 2025 15:45:08 +0300 Subject: [PATCH 065/172] Bump `rustybuzz` (and adjacent crates) (#5407) --- Cargo.lock | 40 ++++++++++++++++++------------------ Cargo.toml | 14 ++++++------- crates/typst-pdf/src/font.rs | 2 +- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 85698d8bb..ac08b57ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -772,9 +772,9 @@ dependencies = [ [[package]] name = "fontdb" -version = "0.21.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37be9fc20d966be438cd57a45767f73349477fb0f85ce86e000557f787298afb" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" dependencies = [ "fontconfig-parser", "log", @@ -1175,9 +1175,9 @@ dependencies = [ [[package]] name = "image-webp" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" +checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" dependencies = [ "byteorder-lite", "quick-error", @@ -1804,9 +1804,9 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pixglyph" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d15afa937836bf3d876f5a04ce28810c06045857bf46c3d0d31073b8aada5494" +checksum = "3c1106193bc18a4b840eb075ff6664c8a0b0270f0531bb12a7e9c803e53b55c5" dependencies = [ "ttf-parser", ] @@ -2048,9 +2048,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "resvg" -version = "0.43.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7314563c59c7ce31c18e23ad3dd092c37b928a0fa4e1c0a1a6504351ab411d1" +checksum = "dd43d1c474e9dadf09a8fdf22d713ba668b499b5117b9b9079500224e26b5b29" dependencies = [ "gif", "image-webp", @@ -2121,9 +2121,9 @@ checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "rustybuzz" -version = "0.18.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85d1ccd519e61834798eb52c4e886e8c2d7d698dd3d6ce0b1b47eb8557f1181" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" dependencies = [ "bitflags 2.8.0", "bytemuck", @@ -2410,9 +2410,9 @@ checksum = "74f98178f34057d4d4de93d68104007c6dea4dfac930204a69ab4622daefa648" [[package]] name = "svg2pdf" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5014c9dadcf318fb7ef8c16438e95abcc9de1ae24d60d5bccc64c55100c50364" +checksum = "e50dc062439cc1a396181059c80932a6e6bd731b130e674c597c0c8874b6df22" dependencies = [ "fontdb", "image", @@ -2709,9 +2709,9 @@ dependencies = [ [[package]] name = "ttf-parser" -version = "0.24.1" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be21190ff5d38e8b4a2d3b6a3ae57f612cc39c96e83cedeaf7abc338a8bac4a" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" dependencies = [ "core_maths", ] @@ -3185,15 +3185,15 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-bidi-mirroring" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64af057ad7466495ca113126be61838d8af947f41d93a949980b2389a118082f" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" [[package]] name = "unicode-ccc" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" [[package]] name = "unicode-ident" @@ -3288,9 +3288,9 @@ dependencies = [ [[package]] name = "usvg" -version = "0.43.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6803057b5cbb426e9fb8ce2216f3a9b4ca1dd2c705ba3cbebc13006e437735fd" +checksum = "2ac8e0e3e4696253dc06167990b3fe9a2668ab66270adf949a464db4088cb354" dependencies = [ "base64", "data-url", diff --git a/Cargo.toml b/Cargo.toml index 0bfd92821..40abaaca7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,7 @@ dirs = "6" ecow = { version = "0.2", features = ["serde"] } env_proxy = "0.4" flate2 = "1" -fontdb = { version = "0.21", default-features = false } +fontdb = { version = "0.23", default-features = false } fs_extra = "1.3" hayagriva = "0.8.1" heck = "0.5" @@ -86,7 +86,7 @@ parking_lot = "0.12.1" pathdiff = "0.2" pdf-writer = "0.12.1" phf = { version = "0.11", features = ["macros"] } -pixglyph = "0.5.1" +pixglyph = "0.6" png = "0.17" portable-atomic = "1.6" proc-macro2 = "1" @@ -96,10 +96,10 @@ quote = "1" rayon = "1.7.0" regex = "1" regex-syntax = "0.8" -resvg = { version = "0.43", default-features = false, features = ["raster-images"] } +resvg = { version = "0.45", default-features = false, features = ["raster-images"] } roxmltree = "0.20" rust_decimal = { version = "1.36.0", default-features = false, features = ["maths"] } -rustybuzz = "0.18" +rustybuzz = "0.20" same-file = "1" self-replace = "1.3.7" semver = "1" @@ -112,7 +112,7 @@ siphasher = "1" smallvec = { version = "1.11.1", features = ["union", "const_generics", "const_new"] } stacker = "0.1.15" subsetter = "0.2" -svg2pdf = "0.12" +svg2pdf = "0.13" syn = { version = "2", features = ["full", "extra-traits"] } syntect = { version = "5", default-features = false, features = ["parsing", "regex-fancy", "plist-load", "yaml-load"] } tar = "0.4" @@ -122,7 +122,7 @@ time = { version = "0.3.20", features = ["formatting", "macros", "parsing"] } tiny_http = "0.12" tiny-skia = "0.11" toml = { version = "0.8", default-features = false, features = ["parse", "display"] } -ttf-parser = "0.24.1" +ttf-parser = "0.25.0" two-face = { version = "0.4.3", default-features = false, features = ["syntect-fancy"] } typed-arena = "2" unicode-bidi = "0.3.18" @@ -133,7 +133,7 @@ unicode-normalization = "0.1.24" unicode-segmentation = "1" unscanny = "0.1" ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] } -usvg = { version = "0.43", default-features = false, features = ["text"] } +usvg = { version = "0.45", default-features = false, features = ["text"] } walkdir = "2" wasmi = "0.40.0" web-sys = "0.3" diff --git a/crates/typst-pdf/src/font.rs b/crates/typst-pdf/src/font.rs index 93d75e50e..f2df2ac92 100644 --- a/crates/typst-pdf/src/font.rs +++ b/crates/typst-pdf/src/font.rs @@ -180,7 +180,7 @@ pub fn write_font_descriptor<'a>( font.to_em(global_bbox.y_max).to_font_units(), ); - let italic_angle = ttf.italic_angle().unwrap_or(0.0); + let italic_angle = ttf.italic_angle(); let ascender = metrics.ascender.to_font_units(); let descender = metrics.descender.to_font_units(); let cap_height = metrics.cap_height.to_font_units(); From 3650859ae8823f47c9f50db6ad5ed52a0477bf15 Mon Sep 17 00:00:00 2001 From: evie <50974538+mi2ebi@users.noreply.github.com> Date: Tue, 11 Mar 2025 03:00:53 -0700 Subject: [PATCH 066/172] Fix `cargo clippy` warnings (mostly about `.repeat.take` and `.next_back`) (#6038) --- crates/typst-ide/src/analyze.rs | 2 +- crates/typst-layout/src/flow/compose.rs | 2 +- crates/typst-layout/src/grid/layouter.rs | 2 +- crates/typst-layout/src/math/stretch.rs | 2 +- crates/typst-library/src/foundations/cast.rs | 2 +- crates/typst-library/src/model/numbering.rs | 2 +- crates/typst-library/src/visualize/gradient.rs | 3 +-- crates/typst-render/src/shape.rs | 8 +++++--- 8 files changed, 12 insertions(+), 11 deletions(-) diff --git a/crates/typst-ide/src/analyze.rs b/crates/typst-ide/src/analyze.rs index 7ee83e709..c493da81a 100644 --- a/crates/typst-ide/src/analyze.rs +++ b/crates/typst-ide/src/analyze.rs @@ -26,7 +26,7 @@ pub fn analyze_expr( ast::Expr::Str(v) => Value::Str(v.get().into()), _ => { if node.kind() == SyntaxKind::Contextual { - if let Some(child) = node.children().last() { + if let Some(child) = node.children().next_back() { return analyze_expr(world, &child); } } diff --git a/crates/typst-layout/src/flow/compose.rs b/crates/typst-layout/src/flow/compose.rs index 76af8f650..54dc487a3 100644 --- a/crates/typst-layout/src/flow/compose.rs +++ b/crates/typst-layout/src/flow/compose.rs @@ -115,7 +115,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { let column_height = regions.size.y; let backlog: Vec<_> = std::iter::once(&column_height) .chain(regions.backlog) - .flat_map(|&h| std::iter::repeat(h).take(self.config.columns.count)) + .flat_map(|&h| std::iter::repeat_n(h, self.config.columns.count)) .skip(1) .collect(); diff --git a/crates/typst-layout/src/grid/layouter.rs b/crates/typst-layout/src/grid/layouter.rs index af47ff72f..dc9e2238d 100644 --- a/crates/typst-layout/src/grid/layouter.rs +++ b/crates/typst-layout/src/grid/layouter.rs @@ -1469,7 +1469,7 @@ impl<'a> GridLayouter<'a> { // last height is the one for the current region. rowspan .heights - .extend(std::iter::repeat(Abs::zero()).take(amount_missing_heights)); + .extend(std::iter::repeat_n(Abs::zero(), amount_missing_heights)); // Ensure that, in this region, the rowspan will span at least // this row. diff --git a/crates/typst-layout/src/math/stretch.rs b/crates/typst-layout/src/math/stretch.rs index dafa8cbe8..f45035e27 100644 --- a/crates/typst-layout/src/math/stretch.rs +++ b/crates/typst-layout/src/math/stretch.rs @@ -302,6 +302,6 @@ fn assemble( fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator + '_ { assembly.parts.into_iter().flat_map(move |part| { let count = if part.part_flags.extender() { repeat } else { 1 }; - std::iter::repeat(part).take(count) + std::iter::repeat_n(part, count) }) } diff --git a/crates/typst-library/src/foundations/cast.rs b/crates/typst-library/src/foundations/cast.rs index 38f409c67..73645491f 100644 --- a/crates/typst-library/src/foundations/cast.rs +++ b/crates/typst-library/src/foundations/cast.rs @@ -21,7 +21,7 @@ use crate::foundations::{ /// /// Type casting works as follows: /// - [`Reflect for T`](Reflect) describes the possible Typst values for `T` -/// (for documentation and autocomplete). +/// (for documentation and autocomplete). /// - [`IntoValue for T`](IntoValue) is for conversion from `T -> Value` /// (infallible) /// - [`FromValue for T`](FromValue) is for conversion from `Value -> T` diff --git a/crates/typst-library/src/model/numbering.rs b/crates/typst-library/src/model/numbering.rs index 150506758..ada8a3965 100644 --- a/crates/typst-library/src/model/numbering.rs +++ b/crates/typst-library/src/model/numbering.rs @@ -394,7 +394,7 @@ impl NumberingKind { const SYMBOLS: &[char] = &['*', '†', '‡', '§', '¶', '‖']; let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()]; let amount = ((n - 1) / SYMBOLS.len()) + 1; - std::iter::repeat(symbol).take(amount).collect() + std::iter::repeat_n(symbol, amount).collect() } Self::Hebrew => hebrew_numeral(n), diff --git a/crates/typst-library/src/visualize/gradient.rs b/crates/typst-library/src/visualize/gradient.rs index 1a723a9f5..d59175a4e 100644 --- a/crates/typst-library/src/visualize/gradient.rs +++ b/crates/typst-library/src/visualize/gradient.rs @@ -574,8 +574,7 @@ impl Gradient { } let n = repetitions.v; - let mut stops = std::iter::repeat(self.stops_ref()) - .take(n) + let mut stops = std::iter::repeat_n(self.stops_ref(), n) .enumerate() .flat_map(|(i, stops)| { let mut stops = stops diff --git a/crates/typst-render/src/shape.rs b/crates/typst-render/src/shape.rs index ba7ed6d89..9b50d5f1f 100644 --- a/crates/typst-render/src/shape.rs +++ b/crates/typst-render/src/shape.rs @@ -69,9 +69,11 @@ pub fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Opt let dash = dash.as_ref().and_then(to_sk_dash_pattern); let bbox = shape.geometry.bbox_size(); - let offset_bbox = (!matches!(shape.geometry, Geometry::Line(..))) - .then(|| offset_bounding_box(bbox, *thickness)) - .unwrap_or(bbox); + let offset_bbox = if !matches!(shape.geometry, Geometry::Line(..)) { + offset_bounding_box(bbox, *thickness) + } else { + bbox + }; let fill_transform = (!matches!(shape.geometry, Geometry::Line(..))).then(|| { From 96f695737174449cbd9efbf4954b676b9bb35056 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 11 Mar 2025 10:18:15 +0000 Subject: [PATCH 067/172] Fix `math.root` frame size (#6021) --- crates/typst-layout/src/math/root.rs | 8 +++++--- tests/ref/math-root-frame-size-index.png | Bin 0 -> 902 bytes tests/suite/math/root.typ | 6 ++++++ 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 tests/ref/math-root-frame-size-index.png diff --git a/crates/typst-layout/src/math/root.rs b/crates/typst-layout/src/math/root.rs index a6b5c03d0..c7f41488e 100644 --- a/crates/typst-layout/src/math/root.rs +++ b/crates/typst-layout/src/math/root.rs @@ -85,14 +85,15 @@ pub fn layout_root( ascent.set_max(shift_up + index.ascent()); } - let radicand_x = sqrt_offset + sqrt.width(); + let sqrt_x = sqrt_offset.max(Abs::zero()); + let radicand_x = sqrt_x + sqrt.width(); let radicand_y = ascent - radicand.ascent(); let width = radicand_x + radicand.width(); let size = Size::new(width, ascent + descent); // The extra "- thickness" comes from the fact that the sqrt is placed // in `push_frame` with respect to its top, not its baseline. - let sqrt_pos = Point::new(sqrt_offset, radicand_y - gap - thickness); + let sqrt_pos = Point::new(sqrt_x, radicand_y - gap - thickness); let line_pos = Point::new(radicand_x, radicand_y - gap - (thickness / 2.0)); let radicand_pos = Point::new(radicand_x, radicand_y); @@ -100,7 +101,8 @@ pub fn layout_root( frame.set_baseline(ascent); if let Some(index) = index { - let index_pos = Point::new(kern_before, ascent - index.ascent() - shift_up); + let index_x = -sqrt_offset.min(Abs::zero()) + kern_before; + let index_pos = Point::new(index_x, ascent - index.ascent() - shift_up); frame.push_frame(index_pos, index); } diff --git a/tests/ref/math-root-frame-size-index.png b/tests/ref/math-root-frame-size-index.png new file mode 100644 index 0000000000000000000000000000000000000000..41d4df2e9ea40429c2bc33bccec9afeb927374d9 GIT binary patch literal 902 zcmV;119|+3P)1i8`-j-*}=qYVapPY5}A+<9-Iye>Z(xM zl~(B}0~RTzPEs)|upmOI7*X&?#8TVRhm;nvwDxJ=wudKah$qu8UY_^u`?4HSYGnS{;D#1a#Z13Z08!n*r88 zzas*c%d^rT<0UE1JOsQ4dode$m!(q&;Aa3^<$?&f*+4}cYR##7;Cd##Ew+7a`3q3q zj5{YFd5g}kMZmv%;$_3I^tSmvu*09)y0jaMei%+R)&L`)V)qa>e1fc z2Z!?`SuiZcDeUolI|Cz#zv{>_#s+nI)O@E#*-!AVuF8y9xPF=HO9c;s;l5ed5Cal# zJQM-XSS{1c+2N4{WZU#$vzgMwTYy6V(_bL~j+7(0m@G-64*|Zzc8n(8gW26auJ!|q zjdfW8u&$koRrtk}k^rxg-nLTuSpE{Y{0+6h-6$Up34j|tu`)MAwi_3KcNUXdBOhTt z?FM%K+YRhf{N`?fyIRXy5#7cX6OH$!!PEDMC|S1D}D$A#-5Jy-q_ zeAVIOc$~Tus7*}XKYL?T;y`w#qi1hI9Qy({-ZS8ZI`&n|DVVXxIZ z#cU_6HUNsg4Xn1-Gyn++s8)cR6%-i`??Jl2bjdn@gn^kO=VV3^sPiheeB%&fcht}5a3%&k>Ms!v}_2LOAUTtXCV2KT#oUK z`sUa(U;t_?d}b;#EZbcz<3Qz)0OO7-Cm!eENp(AWUQGZ|`{~+@pszX@ Date: Tue, 11 Mar 2025 23:20:41 +0300 Subject: [PATCH 068/172] Fix parallel package installation (#5979) Co-authored-by: Laurenz --- Cargo.lock | 1 + Cargo.toml | 1 + crates/typst-kit/Cargo.toml | 3 +- crates/typst-kit/src/package.rs | 84 ++++++++++++++++++++++++++++++--- 4 files changed, 82 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ac08b57ee..06dd4ab80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2896,6 +2896,7 @@ dependencies = [ "dirs", "ecow", "env_proxy", + "fastrand", "flate2", "fontdb", "native-tls", diff --git a/Cargo.toml b/Cargo.toml index 40abaaca7..4e0d3a26c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ ctrlc = "3.4.1" dirs = "6" ecow = { version = "0.2", features = ["serde"] } env_proxy = "0.4" +fastrand = "2.3" flate2 = "1" fontdb = { version = "0.23", default-features = false } fs_extra = "1.3" diff --git a/crates/typst-kit/Cargo.toml b/crates/typst-kit/Cargo.toml index 52aa407c3..e59127d71 100644 --- a/crates/typst-kit/Cargo.toml +++ b/crates/typst-kit/Cargo.toml @@ -19,6 +19,7 @@ typst-utils = { workspace = true } dirs = { workspace = true, optional = true } ecow = { workspace = true } env_proxy = { workspace = true, optional = true } +fastrand = { workspace = true, optional = true } flate2 = { workspace = true, optional = true } fontdb = { workspace = true, optional = true } native-tls = { workspace = true, optional = true } @@ -43,7 +44,7 @@ fonts = ["dep:fontdb", "fontdb/memmap", "fontdb/fontconfig"] downloads = ["dep:env_proxy", "dep:native-tls", "dep:ureq", "dep:openssl"] # Add package downloading utilities, implies `downloads` -packages = ["downloads", "dep:dirs", "dep:flate2", "dep:tar"] +packages = ["downloads", "dep:dirs", "dep:flate2", "dep:tar", "dep:fastrand"] # Embeds some fonts into the binary: # - For text: Libertinus Serif, New Computer Modern diff --git a/crates/typst-kit/src/package.rs b/crates/typst-kit/src/package.rs index 172d8740a..1a1abd607 100644 --- a/crates/typst-kit/src/package.rs +++ b/crates/typst-kit/src/package.rs @@ -1,6 +1,7 @@ //! Download and unpack packages and package indices. use std::fs; +use std::io; use std::path::{Path, PathBuf}; use ecow::eco_format; @@ -100,7 +101,7 @@ impl PackageStorage { // Download from network if it doesn't exist yet. if spec.namespace == DEFAULT_NAMESPACE { - self.download_package(spec, &dir, progress)?; + self.download_package(spec, cache_dir, progress)?; if dir.exists() { return Ok(dir); } @@ -167,7 +168,7 @@ impl PackageStorage { pub fn download_package( &self, spec: &PackageSpec, - package_dir: &Path, + cache_dir: &Path, progress: &mut dyn Progress, ) -> PackageResult<()> { assert_eq!(spec.namespace, DEFAULT_NAMESPACE); @@ -191,11 +192,52 @@ impl PackageStorage { } }; + // The directory in which the package's version lives. + let base_dir = cache_dir.join(format!("{}/{}", spec.namespace, spec.name)); + + // The place at which the specific package version will live in the end. + let package_dir = base_dir.join(format!("{}", spec.version)); + + // To prevent multiple Typst instances from interferring, we download + // into a temporary directory first and then move this directory to + // its final destination. + // + // In the `rename` function's documentation it is stated: + // > This will not work if the new name is on a different mount point. + // + // By locating the temporary directory directly next to where the + // package directory will live, we are (trying our best) making sure + // that `tempdir` and `package_dir` are on the same mount point. + let tempdir = Tempdir::create(base_dir.join(format!( + ".tmp-{}-{}", + spec.version, + fastrand::u32(..), + ))) + .map_err(|err| error("failed to create temporary package directory", err))?; + + // Decompress the archive into the temporary directory. let decompressed = flate2::read::GzDecoder::new(data.as_slice()); - tar::Archive::new(decompressed).unpack(package_dir).map_err(|err| { - fs::remove_dir_all(package_dir).ok(); - PackageError::MalformedArchive(Some(eco_format!("{err}"))) - }) + tar::Archive::new(decompressed) + .unpack(&tempdir) + .map_err(|err| PackageError::MalformedArchive(Some(eco_format!("{err}"))))?; + + // When trying to move (i.e., `rename`) the directory from one place to + // another and the target/destination directory is empty, then the + // operation will succeed (if it's atomic, or hardware doesn't fail, or + // power doesn't go off, etc.). If however the target directory is not + // empty, i.e., another instance already successfully moved the package, + // then we can safely ignore the `DirectoryNotEmpty` error. + // + // This means that we do not check the integrity of an existing moved + // package, just like we don't check the integrity if the package + // directory already existed in the first place. If situations with + // broken packages still occur even with the rename safeguard, we might + // consider more complex solutions like file locking or checksums. + match fs::rename(&tempdir, &package_dir) { + Ok(()) => Ok(()), + Err(err) if err.kind() == io::ErrorKind::DirectoryNotEmpty => Ok(()), + Err(err) => Err(error("failed to move downloaded package directory", err)), + } } } @@ -207,6 +249,36 @@ struct MinimalPackageInfo { version: PackageVersion, } +/// A temporary directory that is a automatically cleaned up. +struct Tempdir(PathBuf); + +impl Tempdir { + /// Creates a directory at the path and auto-cleans it. + fn create(path: PathBuf) -> io::Result { + std::fs::create_dir_all(&path)?; + Ok(Self(path)) + } +} + +impl Drop for Tempdir { + fn drop(&mut self) { + _ = fs::remove_dir_all(&self.0); + } +} + +impl AsRef for Tempdir { + fn as_ref(&self) -> &Path { + &self.0 + } +} + +/// Enriches an I/O error with a message and turns it into a +/// `PackageError::Other`. +#[cold] +fn error(message: &str, err: io::Error) -> PackageError { + PackageError::Other(Some(eco_format!("{message}: {err}"))) +} + #[cfg(test)] mod tests { use super::*; From 24b2f98bf9b8d7a8d8ad9f5f0585ab4317cd3666 Mon Sep 17 00:00:00 2001 From: Michael Fortunato Date: Wed, 12 Mar 2025 07:45:22 -0500 Subject: [PATCH 069/172] Fix typo in 4-template.md (#6047) --- docs/tutorial/4-template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/4-template.md b/docs/tutorial/4-template.md index 209fa5546..7542cd6e4 100644 --- a/docs/tutorial/4-template.md +++ b/docs/tutorial/4-template.md @@ -44,7 +44,7 @@ I am #amazed(color: purple)[amazed]! Templates now work by wrapping our whole document in a custom function like `amazed`. But wrapping a whole document in a giant function call would be cumbersome! Instead, we can use an "everything" show rule to achieve the same -with cleaner code. To write such a show rule, put a colon directly behind the +with cleaner code. To write such a show rule, put a colon directly after the show keyword and then provide a function. This function is given the rest of the document as a parameter. The function can then do anything with this content. Since the `amazed` function can be called with a single content argument, we can From 37bb632d2e9f1f779e15dd5c21ff1ceeadea4a17 Mon Sep 17 00:00:00 2001 From: "Kevin K." Date: Wed, 12 Mar 2025 13:45:57 +0100 Subject: [PATCH 070/172] Fix missing words and paren in docs (#6046) --- crates/typst-library/src/model/bibliography.rs | 2 +- crates/typst-library/src/model/outline.rs | 2 +- crates/typst-library/src/text/raw.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index a391e5804..b11c61789 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -94,7 +94,7 @@ pub struct BibliographyElem { /// - A path string to load a bibliography file from the given path. For /// more details about paths, see the [Paths section]($syntax/#paths). /// - Raw bytes from which the bibliography should be decoded. - /// - An array where each item is one the above. + /// - An array where each item is one of the above. #[required] #[parse( let sources = args.expect("sources")?; diff --git a/crates/typst-library/src/model/outline.rs b/crates/typst-library/src/model/outline.rs index 7ceb530f8..489c375e6 100644 --- a/crates/typst-library/src/model/outline.rs +++ b/crates/typst-library/src/model/outline.rs @@ -388,7 +388,7 @@ pub struct OutlineEntry { /// space between the entry's body and the page number. When using show /// rules to override outline entries, it is thus recommended to wrap the /// fill in a [`box`] with fractional width, i.e. - /// `{box(width: 1fr, it.fill}`. + /// `{box(width: 1fr, it.fill)}`. /// /// When using [`repeat`], the [`gap`]($repeat.gap) property can be useful /// to tweak the visual weight of the fill. diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index 1ce8bfc61..d5c07424d 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -188,7 +188,7 @@ pub struct RawElem { /// - A path string to load a syntax file from the given path. For more /// details about paths, see the [Paths section]($syntax/#paths). /// - Raw bytes from which the syntax should be decoded. - /// - An array where each item is one the above. + /// - An array where each item is one of the above. /// /// ````example /// #set raw(syntaxes: "SExpressions.sublime-syntax") From 95a7e28e25be8374f8574244cc46cf42e97b937e Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 12 Mar 2025 13:46:03 +0100 Subject: [PATCH 071/172] Make two typst-kit functions private (#6045) --- crates/typst-kit/src/package.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/typst-kit/src/package.rs b/crates/typst-kit/src/package.rs index 1a1abd607..584ec83c0 100644 --- a/crates/typst-kit/src/package.rs +++ b/crates/typst-kit/src/package.rs @@ -78,7 +78,8 @@ impl PackageStorage { self.package_path.as_deref() } - /// Make a package available in the on-disk. + /// Makes a package available on-disk and returns the path at which it is + /// located (will be either in the cache or package directory). pub fn prepare_package( &self, spec: &PackageSpec, @@ -111,7 +112,7 @@ impl PackageStorage { Err(PackageError::NotFound(spec.clone())) } - /// Try to determine the latest version of a package. + /// Tries to determine the latest version of a package. pub fn determine_latest_version( &self, spec: &VersionlessPackageSpec, @@ -144,7 +145,7 @@ impl PackageStorage { } /// Download the package index. The result of this is cached for efficiency. - pub fn download_index(&self) -> StrResult<&[serde_json::Value]> { + fn download_index(&self) -> StrResult<&[serde_json::Value]> { self.index .get_or_try_init(|| { let url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/index.json"); @@ -165,7 +166,7 @@ impl PackageStorage { /// /// # Panics /// Panics if the package spec namespace isn't `DEFAULT_NAMESPACE`. - pub fn download_package( + fn download_package( &self, spec: &PackageSpec, cache_dir: &Path, From 1b2714e1a758d6ee0f9471fd1e49cb02f6d8cde4 Mon Sep 17 00:00:00 2001 From: Wolf-SO Date: Wed, 12 Mar 2025 19:29:35 +0100 Subject: [PATCH 072/172] Update 1-writing.md to improve readability (#6040) --- docs/tutorial/1-writing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/1-writing.md b/docs/tutorial/1-writing.md index 5a9fdd4f7..acc257830 100644 --- a/docs/tutorial/1-writing.md +++ b/docs/tutorial/1-writing.md @@ -172,7 +172,7 @@ nothing else. For example, the image function expects a path to an image file. It would not make sense to pass, e.g., a paragraph of text or another image as the image's path parameter. That's why only strings are allowed here. -On the contrary, strings work wherever content is expected because text is a +In contrast, strings work wherever content is expected because text is a valid kind of content. From 91956d1f035b79d1e84318b62cce24659bb3d14d Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski <133046678+wrzian@users.noreply.github.com> Date: Mon, 24 Mar 2025 14:07:19 -0400 Subject: [PATCH 073/172] Use `std::ops::ControlFlow` in `Content::traverse` (#6053) Co-authored-by: Max Mynter --- .../typst-library/src/foundations/content.rs | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/crates/typst-library/src/foundations/content.rs b/crates/typst-library/src/foundations/content.rs index 76cd6a222..daf6c2dd9 100644 --- a/crates/typst-library/src/foundations/content.rs +++ b/crates/typst-library/src/foundations/content.rs @@ -3,7 +3,7 @@ use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use std::iter::{self, Sum}; use std::marker::PhantomData; -use std::ops::{Add, AddAssign, Deref, DerefMut}; +use std::ops::{Add, AddAssign, ControlFlow, Deref, DerefMut}; use std::sync::Arc; use comemo::Tracked; @@ -414,10 +414,11 @@ impl Content { /// Elements produced in `show` rules will not be included in the results. pub fn query(&self, selector: Selector) -> Vec { let mut results = Vec::new(); - self.traverse(&mut |element| { + self.traverse(&mut |element| -> ControlFlow<()> { if selector.matches(&element, None) { results.push(element); } + ControlFlow::Continue(()) }); results } @@ -427,54 +428,58 @@ impl Content { /// /// Elements produced in `show` rules will not be included in the results. pub fn query_first(&self, selector: &Selector) -> Option { - let mut result = None; - self.traverse(&mut |element| { - if result.is_none() && selector.matches(&element, None) { - result = Some(element); + self.traverse(&mut |element| -> ControlFlow { + if selector.matches(&element, None) { + ControlFlow::Break(element) + } else { + ControlFlow::Continue(()) } - }); - result + }) + .break_value() } /// Extracts the plain text of this content. pub fn plain_text(&self) -> EcoString { let mut text = EcoString::new(); - self.traverse(&mut |element| { + self.traverse(&mut |element| -> ControlFlow<()> { if let Some(textable) = element.with::() { textable.plain_text(&mut text); } + ControlFlow::Continue(()) }); text } /// Traverse this content. - fn traverse(&self, f: &mut F) + fn traverse(&self, f: &mut F) -> ControlFlow where - F: FnMut(Content), + F: FnMut(Content) -> ControlFlow, { - f(self.clone()); - - self.inner - .elem - .fields() - .into_iter() - .for_each(|(_, value)| walk_value(value, f)); - /// Walks a given value to find any content that matches the selector. - fn walk_value(value: Value, f: &mut F) + /// + /// Returns early if the function gives `ControlFlow::Break`. + fn walk_value(value: Value, f: &mut F) -> ControlFlow where - F: FnMut(Content), + F: FnMut(Content) -> ControlFlow, { match value { Value::Content(content) => content.traverse(f), Value::Array(array) => { for value in array { - walk_value(value, f); + walk_value(value, f)?; } + ControlFlow::Continue(()) } - _ => {} + _ => ControlFlow::Continue(()), } } + + // Call f on the element itself before recursively iterating its fields. + f(self.clone())?; + for (_, value) in self.inner.elem.fields() { + walk_value(value, f)?; + } + ControlFlow::Continue(()) } } From 636eea18bc1c3fe2acb09e59e67f38a4a0c1b323 Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Mon, 24 Mar 2025 21:08:39 +0300 Subject: [PATCH 074/172] Expand page breaks' triggers for page(height: auto) in docs (#6081) --- crates/typst-library/src/layout/page.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/layout/page.rs b/crates/typst-library/src/layout/page.rs index 0964dccd2..af6ad642d 100644 --- a/crates/typst-library/src/layout/page.rs +++ b/crates/typst-library/src/layout/page.rs @@ -75,9 +75,10 @@ pub struct PageElem { /// The height of the page. /// /// If this is set to `{auto}`, page breaks can only be triggered manually - /// by inserting a [page break]($pagebreak). Most examples throughout this - /// documentation use `{auto}` for the height of the page to dynamically - /// grow and shrink to fit their content. + /// by inserting a [page break]($pagebreak) or by adding another non-empty + /// page set rule. Most examples throughout this documentation use `{auto}` + /// for the height of the page to dynamically grow and shrink to fit their + /// content. #[resolve] #[parse( args.named("height")? From 38213ed534d8a7cd520c0265b99a345bc2966b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20S=C3=A1nchez=20Mu=C3=B1oz?= Date: Mon, 24 Mar 2025 19:16:33 +0100 Subject: [PATCH 075/172] Use `u64` instead of `usize` to store counter and enumeration item numbers, so behavior does not vary from 64-bit to 32-bit platforms (#6026) --- crates/typst-layout/src/lists.rs | 10 ++-- .../src/introspection/counter.rs | 24 ++++----- crates/typst-library/src/layout/page.rs | 2 +- crates/typst-library/src/model/enum.rs | 6 +-- crates/typst-library/src/model/numbering.rs | 49 ++++++++---------- crates/typst-pdf/src/page.rs | 14 ++--- crates/typst-syntax/src/ast.rs | 2 +- crates/typst-syntax/src/lexer.rs | 2 +- tests/ref/enum-numbering-huge.png | Bin 0 -> 900 bytes tests/ref/page-numbering-huge.png | Bin 0 -> 913 bytes tests/src/world.rs | 2 +- tests/suite/introspection/counter.typ | 10 ++++ tests/suite/layout/page.typ | 10 ++++ tests/suite/model/enum.typ | 5 ++ tests/suite/model/numbering.typ | 1 + 15 files changed, 82 insertions(+), 55 deletions(-) create mode 100644 tests/ref/enum-numbering-huge.png create mode 100644 tests/ref/page-numbering-huge.png diff --git a/crates/typst-layout/src/lists.rs b/crates/typst-layout/src/lists.rs index f8d910abf..974788a70 100644 --- a/crates/typst-layout/src/lists.rs +++ b/crates/typst-layout/src/lists.rs @@ -96,9 +96,13 @@ pub fn layout_enum( let mut cells = vec![]; let mut locator = locator.split(); - let mut number = - elem.start(styles) - .unwrap_or_else(|| if reversed { elem.children.len() } else { 1 }); + let mut number = elem.start(styles).unwrap_or_else(|| { + if reversed { + elem.children.len() as u64 + } else { + 1 + } + }); let mut parents = EnumElem::parents_in(styles); let full = elem.full(styles); diff --git a/crates/typst-library/src/introspection/counter.rs b/crates/typst-library/src/introspection/counter.rs index 5432df238..772bea963 100644 --- a/crates/typst-library/src/introspection/counter.rs +++ b/crates/typst-library/src/introspection/counter.rs @@ -229,10 +229,10 @@ impl Counter { if self.is_page() { let at_delta = engine.introspector.page(location).get().saturating_sub(at_page.get()); - at_state.step(NonZeroUsize::ONE, at_delta); + at_state.step(NonZeroUsize::ONE, at_delta as u64); let final_delta = engine.introspector.pages().get().saturating_sub(final_page.get()); - final_state.step(NonZeroUsize::ONE, final_delta); + final_state.step(NonZeroUsize::ONE, final_delta as u64); } Ok(CounterState(smallvec![at_state.first(), final_state.first()])) } @@ -250,7 +250,7 @@ impl Counter { if self.is_page() { let delta = engine.introspector.page(location).get().saturating_sub(page.get()); - state.step(NonZeroUsize::ONE, delta); + state.step(NonZeroUsize::ONE, delta as u64); } Ok(state) } @@ -319,7 +319,7 @@ impl Counter { let delta = page.get() - prev.get(); if delta > 0 { - state.step(NonZeroUsize::ONE, delta); + state.step(NonZeroUsize::ONE, delta as u64); } } @@ -500,7 +500,7 @@ impl Counter { let (mut state, page) = sequence.last().unwrap().clone(); if self.is_page() { let delta = engine.introspector.pages().get().saturating_sub(page.get()); - state.step(NonZeroUsize::ONE, delta); + state.step(NonZeroUsize::ONE, delta as u64); } Ok(state) } @@ -616,13 +616,13 @@ pub trait Count { /// Counts through elements with different levels. #[derive(Debug, Clone, PartialEq, Hash)] -pub struct CounterState(pub SmallVec<[usize; 3]>); +pub struct CounterState(pub SmallVec<[u64; 3]>); impl CounterState { /// Get the initial counter state for the key. pub fn init(page: bool) -> Self { // Special case, because pages always start at one. - Self(smallvec![usize::from(page)]) + Self(smallvec![u64::from(page)]) } /// Advance the counter and return the numbers for the given heading. @@ -645,7 +645,7 @@ impl CounterState { } /// Advance the number of the given level by the specified amount. - pub fn step(&mut self, level: NonZeroUsize, by: usize) { + pub fn step(&mut self, level: NonZeroUsize, by: u64) { let level = level.get(); while self.0.len() < level { @@ -657,7 +657,7 @@ impl CounterState { } /// Get the first number of the state. - pub fn first(&self) -> usize { + pub fn first(&self) -> u64 { self.0.first().copied().unwrap_or(1) } @@ -675,7 +675,7 @@ impl CounterState { cast! { CounterState, self => Value::Array(self.0.into_iter().map(IntoValue::into_value).collect()), - num: usize => Self(smallvec![num]), + num: u64 => Self(smallvec![num]), array: Array => Self(array .into_iter() .map(Value::cast) @@ -758,7 +758,7 @@ impl Show for Packed { #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct ManualPageCounter { physical: NonZeroUsize, - logical: usize, + logical: u64, } impl ManualPageCounter { @@ -773,7 +773,7 @@ impl ManualPageCounter { } /// Get the current logical page counter state. - pub fn logical(&self) -> usize { + pub fn logical(&self) -> u64 { self.logical } diff --git a/crates/typst-library/src/layout/page.rs b/crates/typst-library/src/layout/page.rs index af6ad642d..62e25278a 100644 --- a/crates/typst-library/src/layout/page.rs +++ b/crates/typst-library/src/layout/page.rs @@ -484,7 +484,7 @@ pub struct Page { pub supplement: Content, /// The logical page number (controlled by `counter(page)` and may thus not /// match the physical number). - pub number: usize, + pub number: u64, } impl Page { diff --git a/crates/typst-library/src/model/enum.rs b/crates/typst-library/src/model/enum.rs index a4126e72c..2d95996ab 100644 --- a/crates/typst-library/src/model/enum.rs +++ b/crates/typst-library/src/model/enum.rs @@ -129,7 +129,7 @@ pub struct EnumElem { /// [Ahead], /// ) /// ``` - pub start: Smart, + pub start: Smart, /// Whether to display the full numbering, including the numbers of /// all parent enumerations. @@ -217,7 +217,7 @@ pub struct EnumElem { #[internal] #[fold] #[ghost] - pub parents: SmallVec<[usize; 4]>, + pub parents: SmallVec<[u64; 4]>, } #[scope] @@ -274,7 +274,7 @@ impl Show for Packed { pub struct EnumItem { /// The item's number. #[positional] - pub number: Option, + pub number: Option, /// The item's body. #[required] diff --git a/crates/typst-library/src/model/numbering.rs b/crates/typst-library/src/model/numbering.rs index ada8a3965..d82c3e4cd 100644 --- a/crates/typst-library/src/model/numbering.rs +++ b/crates/typst-library/src/model/numbering.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use chinese_number::{ - from_usize_to_chinese_ten_thousand as usize_to_chinese, ChineseCase, ChineseVariant, + from_u64_to_chinese_ten_thousand as u64_to_chinese, ChineseCase, ChineseVariant, }; use comemo::Tracked; use ecow::{eco_format, EcoString, EcoVec}; @@ -85,7 +85,7 @@ pub fn numbering( /// If `numbering` is a pattern and more numbers than counting symbols are /// given, the last counting symbol with its prefix is repeated. #[variadic] - numbers: Vec, + numbers: Vec, ) -> SourceResult { numbering.apply(engine, context, &numbers) } @@ -105,7 +105,7 @@ impl Numbering { &self, engine: &mut Engine, context: Tracked, - numbers: &[usize], + numbers: &[u64], ) -> SourceResult { Ok(match self { Self::Pattern(pattern) => Value::Str(pattern.apply(numbers).into()), @@ -156,7 +156,7 @@ pub struct NumberingPattern { impl NumberingPattern { /// Apply the pattern to the given number. - pub fn apply(&self, numbers: &[usize]) -> EcoString { + pub fn apply(&self, numbers: &[u64]) -> EcoString { let mut fmt = EcoString::new(); let mut numbers = numbers.iter(); @@ -185,7 +185,7 @@ impl NumberingPattern { } /// Apply only the k-th segment of the pattern to a number. - pub fn apply_kth(&self, k: usize, number: usize) -> EcoString { + pub fn apply_kth(&self, k: usize, number: u64) -> EcoString { let mut fmt = EcoString::new(); if let Some((prefix, _)) = self.pieces.first() { fmt.push_str(prefix); @@ -379,7 +379,7 @@ impl NumberingKind { } /// Apply the numbering to the given number. - pub fn apply(self, n: usize) -> EcoString { + pub fn apply(self, n: u64) -> EcoString { match self { Self::Arabic => eco_format!("{n}"), Self::LowerRoman => roman_numeral(n, Case::Lower), @@ -392,9 +392,10 @@ impl NumberingKind { } const SYMBOLS: &[char] = &['*', '†', '‡', '§', '¶', '‖']; - let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()]; - let amount = ((n - 1) / SYMBOLS.len()) + 1; - std::iter::repeat_n(symbol, amount).collect() + let n_symbols = SYMBOLS.len() as u64; + let symbol = SYMBOLS[((n - 1) % n_symbols) as usize]; + let amount = ((n - 1) / n_symbols) + 1; + std::iter::repeat_n(symbol, amount.try_into().unwrap()).collect() } Self::Hebrew => hebrew_numeral(n), @@ -489,18 +490,16 @@ impl NumberingKind { } Self::LowerSimplifiedChinese => { - usize_to_chinese(ChineseVariant::Simple, ChineseCase::Lower, n).into() + u64_to_chinese(ChineseVariant::Simple, ChineseCase::Lower, n).into() } Self::UpperSimplifiedChinese => { - usize_to_chinese(ChineseVariant::Simple, ChineseCase::Upper, n).into() + u64_to_chinese(ChineseVariant::Simple, ChineseCase::Upper, n).into() } Self::LowerTraditionalChinese => { - usize_to_chinese(ChineseVariant::Traditional, ChineseCase::Lower, n) - .into() + u64_to_chinese(ChineseVariant::Traditional, ChineseCase::Lower, n).into() } Self::UpperTraditionalChinese => { - usize_to_chinese(ChineseVariant::Traditional, ChineseCase::Upper, n) - .into() + u64_to_chinese(ChineseVariant::Traditional, ChineseCase::Upper, n).into() } Self::EasternArabic => decimal('\u{0660}', n), @@ -512,7 +511,7 @@ impl NumberingKind { } /// Stringify an integer to a Hebrew number. -fn hebrew_numeral(mut n: usize) -> EcoString { +fn hebrew_numeral(mut n: u64) -> EcoString { if n == 0 { return '-'.into(); } @@ -566,7 +565,7 @@ fn hebrew_numeral(mut n: usize) -> EcoString { } /// Stringify an integer to a Roman numeral. -fn roman_numeral(mut n: usize, case: Case) -> EcoString { +fn roman_numeral(mut n: u64, case: Case) -> EcoString { if n == 0 { return match case { Case::Lower => 'n'.into(), @@ -622,7 +621,7 @@ fn roman_numeral(mut n: usize, case: Case) -> EcoString { /// /// [converter]: https://www.russellcottrell.com/greek/utilities/GreekNumberConverter.htm /// [numbers]: https://mathshistory.st-andrews.ac.uk/HistTopics/Greek_numbers/ -fn greek_numeral(n: usize, case: Case) -> EcoString { +fn greek_numeral(n: u64, case: Case) -> EcoString { let thousands = [ ["͵α", "͵Α"], ["͵β", "͵Β"], @@ -683,7 +682,7 @@ fn greek_numeral(n: usize, case: Case) -> EcoString { let mut decimal_digits: Vec = Vec::new(); let mut n = n; while n > 0 { - decimal_digits.push(n % 10); + decimal_digits.push((n % 10) as usize); n /= 10; } @@ -778,18 +777,16 @@ fn greek_numeral(n: usize, case: Case) -> EcoString { /// /// You might be familiar with this scheme from the way spreadsheet software /// tends to label its columns. -fn zeroless( - alphabet: [char; N_DIGITS], - mut n: usize, -) -> EcoString { +fn zeroless(alphabet: [char; N_DIGITS], mut n: u64) -> EcoString { if n == 0 { return '-'.into(); } + let n_digits = N_DIGITS as u64; let mut cs = EcoString::new(); while n > 0 { n -= 1; - cs.push(alphabet[n % N_DIGITS]); - n /= N_DIGITS; + cs.push(alphabet[(n % n_digits) as usize]); + n /= n_digits; } cs.chars().rev().collect() } @@ -797,7 +794,7 @@ fn zeroless( /// Stringify a number using a base-10 counting system with a zero digit. /// /// This function assumes that the digits occupy contiguous codepoints. -fn decimal(start: char, mut n: usize) -> EcoString { +fn decimal(start: char, mut n: u64) -> EcoString { if n == 0 { return start.into(); } diff --git a/crates/typst-pdf/src/page.rs b/crates/typst-pdf/src/page.rs index 4e95f3c70..68125d29a 100644 --- a/crates/typst-pdf/src/page.rs +++ b/crates/typst-pdf/src/page.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use std::num::NonZeroUsize; +use std::num::NonZeroU64; use ecow::EcoString; use pdf_writer::types::{ActionType, AnnotationFlags, AnnotationType, NumberingStyle}; @@ -48,7 +48,7 @@ pub fn traverse_pages( // the real (not logical) page numbers. Here, the final PDF page number // will differ, but we can at least use labels to indicate what was // the corresponding real page number in the Typst document. - (skipped_pages > 0).then(|| PdfPageLabel::arabic(i + 1)) + (skipped_pages > 0).then(|| PdfPageLabel::arabic((i + 1) as u64)) }); pages.push(Some(encoded)); } @@ -219,7 +219,7 @@ pub(crate) struct PdfPageLabel { /// /// Describes where to start counting from when setting a style. /// (Has to be greater or equal than 1) - pub offset: Option, + pub offset: Option, } /// A PDF page label number style. @@ -242,7 +242,7 @@ pub enum PdfPageLabelStyle { impl PdfPageLabel { /// Create a new `PdfNumbering` from a `Numbering` applied to a page /// number. - fn generate(numbering: &Numbering, number: usize) -> Option { + fn generate(numbering: &Numbering, number: u64) -> Option { let Numbering::Pattern(pat) = numbering else { return None; }; @@ -275,18 +275,18 @@ impl PdfPageLabel { (!prefix.is_empty()).then(|| prefix.clone()) }; - let offset = style.and(NonZeroUsize::new(number)); + let offset = style.and(NonZeroU64::new(number)); Some(PdfPageLabel { prefix, style, offset }) } /// Creates an arabic page label with the specified page number. /// For example, this will display page label `11` when given the page /// number 11. - fn arabic(number: usize) -> PdfPageLabel { + fn arabic(number: u64) -> PdfPageLabel { PdfPageLabel { prefix: None, style: Some(PdfPageLabelStyle::Arabic), - offset: NonZeroUsize::new(number), + offset: NonZeroU64::new(number), } } } diff --git a/crates/typst-syntax/src/ast.rs b/crates/typst-syntax/src/ast.rs index f79e65982..7b211bfc1 100644 --- a/crates/typst-syntax/src/ast.rs +++ b/crates/typst-syntax/src/ast.rs @@ -778,7 +778,7 @@ node! { impl<'a> EnumItem<'a> { /// The explicit numbering, if any: `23.`. - pub fn number(self) -> Option { + pub fn number(self) -> Option { self.0.children().find_map(|node| match node.kind() { SyntaxKind::EnumMarker => node.text().trim_end_matches('.').parse().ok(), _ => Option::None, diff --git a/crates/typst-syntax/src/lexer.rs b/crates/typst-syntax/src/lexer.rs index b8f2bf25f..ac69eb616 100644 --- a/crates/typst-syntax/src/lexer.rs +++ b/crates/typst-syntax/src/lexer.rs @@ -480,7 +480,7 @@ impl Lexer<'_> { self.s.eat_while(char::is_ascii_digit); let read = self.s.from(start); - if self.s.eat_if('.') && self.space_or_end() && read.parse::().is_ok() { + if self.s.eat_if('.') && self.space_or_end() && read.parse::().is_ok() { return SyntaxKind::EnumMarker; } diff --git a/tests/ref/enum-numbering-huge.png b/tests/ref/enum-numbering-huge.png new file mode 100644 index 0000000000000000000000000000000000000000..b8117e0f490d8616cdf9e6a3982223c95aebb23e GIT binary patch literal 900 zcmV-~1AF|5P)pITPE1iPJQQ38U(OOa$cY)FYC4wYFIzY*^fRHR2x{XGK;e-uL z8g4s1V7W~Q7+7NhfjIdZ9?T)a1P1&1?5CaAub;g0&ig!nf8FD^_j?16sSP45!Xhlf zj|8>=W+NCFPmZZ!Wdj{4B$7}fX)C9+wBnqhrfRY>e}^M zQbV6CP9I&!MA@S;vX75eM@wc8adN2I5>6#K(EsgtA)im4>O=F?Iz;<3MLhNPFQmwh&9F3|{v`G`V-Da5#qW3)UR8 z6`7q#r(*CX7>lS;r8wE?-prj{+A=CN(%WpC-@(l!c;g{Ff3RbROEA2h)erwpKtx!C0dNA{ zHt=1VgbBSLI{tv=mKkuhguv1ZOTW#*h#^AQ?120F@#t6=f3C_d>`7E^gl0p%f&FXg zEAE0>8Fd!*Rnh~g{;4m93x5KI8L;Uy2A(jf?)}3AKb{`m*%7G z>c6VqBFFZ4=}+3xl7B0*C#_z#jh*x2+_Q zN!Bg1ZuPxT-jYe~%OQPQZ(K}@99(Ps*_%ho+RM2_G1ZR~0F!zlbepj?XrNYhhsal5jrY1i)*9|~Z@(+zx@qmc12#fHO a4F3yJa>=#fj;+`L00004WE64bZO`02)+5xV|<5-3lAvYN9%=6;3_iP?-S3 z$2kPbDgfIjE1*+&;rp%z29>=)v(gWDaUIaB7=o`+;aV^@9en~W6{&7h&d?P+v^=j3 zq#@a_%eUUb&4FCK=MJ$eyO4D!lg zDpI`0M}1du&-%2au8lk}x=sg^aKrxYW%!Lt@?buR`^DMi*o_3}&S);- znmV%qaWt#Gs?Ne$ZMqS0oERn9#kokapyLeHy#^|C(36Zus@!742NKYhW=zMOvaBDx zwCQ3)CNkmlAhu_@HPJ;U}Te##P?%u_Q&BA2yzv zCGmZ}Ua>?ywUs1vY7_UFgCn~nYWvQJ*nVB&ffhKoO4R)cQ870C;X};`tRvI2ymo%v zW%F3~NUCQylRYNw5pkD)AYP)L+(veHYZLdkarPY&wJmP(mOLeyH70%zW z?f#v9w8&`tzLh&LA^iaSlF_a$luu(+ssd_ddT_4_To{yo0jgwea6P`>T2HUa{vfLd zMj372_wrFVU%aCYEvpBL20DD}W!YK&sJvVC33j%v~bR8E2_C(}Wo-nZY?A#VXU{AGPlrISEy~)TZ zVz9@InK6SscE`+^!5$BL-Ug*d5ZKF{EG!ZP_5$PQIs}2e$npuZFtC@F_e>Dj>mX*a nAh1_j@W#(zj~O%L|HSw=-pe=f9rb$n00000NkvXXu0mjf5Bjt~ literal 0 HcmV?d00001 diff --git a/tests/src/world.rs b/tests/src/world.rs index 5c2678328..9e0e91ad7 100644 --- a/tests/src/world.rs +++ b/tests/src/world.rs @@ -244,7 +244,7 @@ fn lines( engine: &mut Engine, context: Tracked, span: Span, - count: usize, + count: u64, #[default(Numbering::Pattern(NumberingPattern::from_str("A").unwrap()))] numbering: Numbering, ) -> SourceResult { diff --git a/tests/suite/introspection/counter.typ b/tests/suite/introspection/counter.typ index 2f095f2fb..b0657a2ad 100644 --- a/tests/suite/introspection/counter.typ +++ b/tests/suite/introspection/counter.typ @@ -164,3 +164,13 @@ B #context test(c.get(), (1,)) #c.step(level: 3) #context test(c.get(), (1, 0, 1)) + +--- counter-huge --- +// Test values greater than 32-bits +#let c = counter("c") +#c.update(100000000001) +#context test(c.get(), (100000000001,)) +#c.step() +#context test(c.get(), (100000000002,)) +#c.update(n => n + 2) +#context test(c.get(), (100000000004,)) diff --git a/tests/suite/layout/page.typ b/tests/suite/layout/page.typ index a35f19bb3..4df9f9cac 100644 --- a/tests/suite/layout/page.typ +++ b/tests/suite/layout/page.typ @@ -246,6 +246,16 @@ Look, ma, no page numbers! #set page(header: auto, footer: auto) Default page numbers now. +--- page-numbering-huge --- +#set page(margin: (bottom: 20pt, rest: 0pt)) +#let filler = lines(1) + +// Test values greater than 32-bits +#set page(numbering: "1/1") +#counter(page).update(100000000001) +#pagebreak() +#pagebreak() + --- page-marginal-style-text-set --- #set page(numbering: "1", margin: (bottom: 20pt)) #set text(red) diff --git a/tests/suite/model/enum.typ b/tests/suite/model/enum.typ index 7176b04e2..7ee4dc20c 100644 --- a/tests/suite/model/enum.typ +++ b/tests/suite/model/enum.typ @@ -134,6 +134,11 @@ a + 0. // Error: 22-28 invalid numbering pattern #set enum(numbering: "(())") +--- enum-numbering-huge --- +// Test values greater than 32-bits +100000000001. A ++ B + --- enum-number-align-unaffected --- // Alignment shouldn't affect number #set align(horizon) diff --git a/tests/suite/model/numbering.typ b/tests/suite/model/numbering.typ index ccd7cfc18..6af989ff1 100644 --- a/tests/suite/model/numbering.typ +++ b/tests/suite/model/numbering.typ @@ -49,6 +49,7 @@ 2000000001, "βΜκʹ, αʹ", 2000010001, "βΜκʹ, αΜαʹ, αʹ", 2056839184, "βΜκʹ, αΜ͵εχπγ, ͵θρπδ", + 12312398676, "βΜρκγʹ, αΜ͵ασλθ, ͵ηχοϛ", ) #t( pat: sym.Alpha, From 1e591ac8dcfb7160bd401e76b4ff39aec80db219 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 24 Mar 2025 19:17:29 +0100 Subject: [PATCH 076/172] Bump `zip` (#6091) --- Cargo.lock | 6 ++---- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 06dd4ab80..d63cec880 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3796,18 +3796,16 @@ dependencies = [ [[package]] name = "zip" -version = "2.2.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45" +checksum = "27c03817464f64e23f6f37574b4fdc8cf65925b5bfd2b0f2aedf959791941f88" dependencies = [ "arbitrary", "crc32fast", "crossbeam-utils", - "displaydoc", "flate2", "indexmap 2.7.1", "memchr", - "thiserror 2.0.11", "zopfli", ] diff --git a/Cargo.toml b/Cargo.toml index 4e0d3a26c..a14124d65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -143,7 +143,7 @@ xmlwriter = "0.1.0" xmp-writer = "0.3.1" xz2 = { version = "0.1", features = ["static"] } yaml-front-matter = "0.1" -zip = { version = "2", default-features = false, features = ["deflate"] } +zip = { version = "2.5", default-features = false, features = ["deflate"] } [profile.dev.package."*"] opt-level = 2 From 1f1c1338785dc09a43292cf106b4a23b4e1bd86e Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Mon, 24 Mar 2025 17:42:48 -0300 Subject: [PATCH 077/172] Refactor grid header and footer resolving (#5919) --- .../typst-library/src/layout/grid/resolve.rs | 2052 ++++++++++------- tests/ref/grid-footer-expand.png | Bin 365 -> 0 bytes ...rid-footer-moved-to-bottom-of-rowspans.png | Bin 0 -> 1182 bytes ...oter-top-hlines-with-only-row-pos-cell.png | Bin 0 -> 385 bytes ...-top-hlines-with-row-and-auto-pos-cell.png | Bin 0 -> 579 bytes tests/ref/grid-header-cell-with-x.png | Bin 0 -> 419 bytes tests/ref/grid-header-expand.png | Bin 2005 -> 0 bytes ...59-column-override-stays-inside-footer.png | Bin 0 -> 674 bytes tests/suite/layout/grid/footers.typ | 159 +- tests/suite/layout/grid/headers.typ | 106 +- 10 files changed, 1496 insertions(+), 821 deletions(-) delete mode 100644 tests/ref/grid-footer-expand.png create mode 100644 tests/ref/grid-footer-moved-to-bottom-of-rowspans.png create mode 100644 tests/ref/grid-footer-top-hlines-with-only-row-pos-cell.png create mode 100644 tests/ref/grid-footer-top-hlines-with-row-and-auto-pos-cell.png create mode 100644 tests/ref/grid-header-cell-with-x.png delete mode 100644 tests/ref/grid-header-expand.png create mode 100644 tests/ref/issue-5359-column-override-stays-inside-footer.png diff --git a/crates/typst-library/src/layout/grid/resolve.rs b/crates/typst-library/src/layout/grid/resolve.rs index 08d0130da..bad25b474 100644 --- a/crates/typst-library/src/layout/grid/resolve.rs +++ b/crates/typst-library/src/layout/grid/resolve.rs @@ -1,4 +1,5 @@ use std::num::NonZeroUsize; +use std::ops::Range; use std::sync::Arc; use ecow::eco_format; @@ -20,6 +21,8 @@ use typst_library::Dir; use typst_syntax::Span; use typst_utils::NonZeroExt; +use crate::introspection::SplitLocator; + /// Convert a grid to a cell grid. #[typst_macros::time(span = elem.span())] pub fn grid_to_cellgrid<'a>( @@ -57,7 +60,7 @@ pub fn grid_to_cellgrid<'a>( ResolvableGridChild::Item(grid_item_to_resolvable(item, styles)) } }); - CellGrid::resolve( + resolve_cellgrid( tracks, gutter, locator, @@ -110,7 +113,7 @@ pub fn table_to_cellgrid<'a>( ResolvableGridChild::Item(table_item_to_resolvable(item, styles)) } }); - CellGrid::resolve( + resolve_cellgrid( tracks, gutter, locator, @@ -421,12 +424,14 @@ pub struct Line { } /// A repeatable grid header. Starts at the first row. +#[derive(Debug)] pub struct Header { /// The index after the last row included in this header. pub end: usize, } /// A repeatable grid footer. Stops at the last row. +#[derive(Debug)] pub struct Footer { /// The first row included in this footer. pub start: usize, @@ -652,772 +657,6 @@ impl<'a> CellGrid<'a> { Self::new_internal(tracks, gutter, vec![], vec![], None, None, entries) } - /// Resolves and positions all cells in the grid before creating it. - /// Allows them to keep track of their final properties and positions - /// and adjust their fields accordingly. - /// Cells must implement Clone as they will be owned. Additionally, they - /// must implement Default in order to fill positions in the grid which - /// weren't explicitly specified by the user with empty cells. - #[allow(clippy::too_many_arguments)] - pub fn resolve( - tracks: Axes<&[Sizing]>, - gutter: Axes<&[Sizing]>, - locator: Locator<'a>, - children: C, - fill: &Celled>, - align: &Celled>, - inset: &Celled>>>, - stroke: &ResolvedCelled>>>>, - engine: &mut Engine, - styles: StyleChain, - span: Span, - ) -> SourceResult - where - T: ResolvableCell + Default, - I: Iterator>, - C: IntoIterator>, - C::IntoIter: ExactSizeIterator, - { - let mut locator = locator.split(); - - // Number of content columns: Always at least one. - let c = tracks.x.len().max(1); - - // Lists of lines. - // Horizontal lines are only pushed later to be able to check for row - // validity, since the amount of rows isn't known until all items were - // analyzed in the for loop below. - // We keep their spans so we can report errors later. - // The additional boolean indicates whether the hline had an automatic - // 'y' index, and is used to change the index of hlines at the top of a - // header or footer. - let mut pending_hlines: Vec<(Span, Line, bool)> = vec![]; - - // For consistency, only push vertical lines later as well. - let mut pending_vlines: Vec<(Span, Line)> = vec![]; - let has_gutter = gutter.any(|tracks| !tracks.is_empty()); - - let mut header: Option
= None; - let mut repeat_header = false; - - // Stores where the footer is supposed to end, its span, and the - // actual footer structure. - let mut footer: Option<(usize, Span, Footer)> = None; - let mut repeat_footer = false; - - // Resolves the breakability of a cell. Cells that span at least one - // auto-sized row or gutter are considered breakable. - let resolve_breakable = |y, rowspan| { - let auto = Sizing::Auto; - let zero = Sizing::Rel(Rel::zero()); - tracks - .y - .iter() - .chain(std::iter::repeat(tracks.y.last().unwrap_or(&auto))) - .skip(y) - .take(rowspan) - .any(|row| row == &Sizing::Auto) - || gutter - .y - .iter() - .chain(std::iter::repeat(gutter.y.last().unwrap_or(&zero))) - .skip(y) - .take(rowspan - 1) - .any(|row_gutter| row_gutter == &Sizing::Auto) - }; - - // We can't just use the cell's index in the 'cells' vector to - // determine its automatic position, since cells could have arbitrary - // positions, so the position of a cell in 'cells' can differ from its - // final position in 'resolved_cells' (see below). - // Therefore, we use a counter, 'auto_index', to determine the position - // of the next cell with (x: auto, y: auto). It is only stepped when - // a cell with (x: auto, y: auto), usually the vast majority, is found. - let mut auto_index: usize = 0; - - // We have to rebuild the grid to account for arbitrary positions. - // Create at least 'children.len()' positions, since there could be at - // least 'children.len()' cells (if no explicit lines were specified), - // even though some of them might be placed in arbitrary positions and - // thus cause the grid to expand. - // Additionally, make sure we allocate up to the next multiple of 'c', - // since each row will have 'c' cells, even if the last few cells - // weren't explicitly specified by the user. - // We apply '% c' twice so that the amount of cells potentially missing - // is zero when 'children.len()' is already a multiple of 'c' (thus - // 'children.len() % c' would be zero). - let children = children.into_iter(); - let Some(child_count) = children.len().checked_add((c - children.len() % c) % c) - else { - bail!(span, "too many cells or lines were given") - }; - let mut resolved_cells: Vec> = Vec::with_capacity(child_count); - for child in children { - let mut is_header = false; - let mut is_footer = false; - let mut child_start = usize::MAX; - let mut child_end = 0; - let mut child_span = Span::detached(); - let mut start_new_row = false; - let mut first_index_of_top_hlines = usize::MAX; - let mut first_index_of_non_top_hlines = usize::MAX; - - let (header_footer_items, simple_item) = match child { - ResolvableGridChild::Header { repeat, span, items, .. } => { - if header.is_some() { - bail!(span, "cannot have more than one header"); - } - - is_header = true; - child_span = span; - repeat_header = repeat; - - // If any cell in the header is automatically positioned, - // have it skip to the next row. This is to avoid having a - // header after a partially filled row just add cells to - // that row instead of starting a new one. - // FIXME: Revise this approach when headers can start from - // arbitrary rows. - start_new_row = true; - - // Any hlines at the top of the header will start at this - // index. - first_index_of_top_hlines = pending_hlines.len(); - - (Some(items), None) - } - ResolvableGridChild::Footer { repeat, span, items, .. } => { - if footer.is_some() { - bail!(span, "cannot have more than one footer"); - } - - is_footer = true; - child_span = span; - repeat_footer = repeat; - - // If any cell in the footer is automatically positioned, - // have it skip to the next row. This is to avoid having a - // footer after a partially filled row just add cells to - // that row instead of starting a new one. - start_new_row = true; - - // Any hlines at the top of the footer will start at this - // index. - first_index_of_top_hlines = pending_hlines.len(); - - (Some(items), None) - } - ResolvableGridChild::Item(item) => (None, Some(item)), - }; - - let items = header_footer_items - .into_iter() - .flatten() - .chain(simple_item.into_iter()); - for item in items { - let cell = match item { - ResolvableGridItem::HLine { - y, - start, - end, - stroke, - span, - position, - } => { - let has_auto_y = y.is_auto(); - let y = y.unwrap_or_else(|| { - // Avoid placing the hline inside consecutive - // rowspans occupying all columns, as it'd just - // disappear, at least when there's no column - // gutter. - skip_auto_index_through_fully_merged_rows( - &resolved_cells, - &mut auto_index, - c, - ); - - // When no 'y' is specified for the hline, we place - // it under the latest automatically positioned - // cell. - // The current value of the auto index is always - // the index of the latest automatically positioned - // cell placed plus one (that's what we do in - // 'resolve_cell_position'), so we subtract 1 to - // get that cell's index, and place the hline below - // its row. The exception is when the auto_index is - // 0, meaning no automatically positioned cell was - // placed yet. In that case, we place the hline at - // the top of the table. - // - // Exceptionally, the hline will be placed before - // the minimum auto index if the current auto index - // from previous iterations is smaller than the - // minimum it should have for the current grid - // child. Effectively, this means that a hline at - // the start of a header will always appear above - // that header's first row. Similarly for footers. - auto_index - .checked_sub(1) - .map_or(0, |last_auto_index| last_auto_index / c + 1) - }); - if end.is_some_and(|end| end.get() < start) { - bail!(span, "line cannot end before it starts"); - } - let line = Line { index: y, start, end, stroke, position }; - - // Since the amount of rows is dynamic, delay placing - // hlines until after all cells were placed so we can - // properly verify if they are valid. Note that we - // can't place hlines even if we already know they - // would be in a valid row, since it's possible that we - // pushed pending hlines in the same row as this one in - // previous iterations, and we need to ensure that - // hlines from previous iterations are pushed to the - // final vector of hlines first - the order of hlines - // must be kept, as this matters when determining which - // one "wins" in case of conflict. Pushing the current - // hline before we push pending hlines later would - // change their order! - pending_hlines.push((span, line, has_auto_y)); - continue; - } - ResolvableGridItem::VLine { - x, - start, - end, - stroke, - span, - position, - } => { - let x = x.unwrap_or_else(|| { - // When no 'x' is specified for the vline, we place - // it after the latest automatically positioned - // cell. - // The current value of the auto index is always - // the index of the latest automatically positioned - // cell placed plus one (that's what we do in - // 'resolve_cell_position'), so we subtract 1 to - // get that cell's index, and place the vline after - // its column. The exception is when the auto_index - // is 0, meaning no automatically positioned cell - // was placed yet. In that case, we place the vline - // to the left of the table. - // - // Exceptionally, a vline is also placed to the - // left of the table if we should start a new row - // for the next automatically positioned cell. - // For example, this means that a vline at - // the beginning of a header will be placed to its - // left rather than after the previous - // automatically positioned cell. Same for footers. - auto_index - .checked_sub(1) - .filter(|_| !start_new_row) - .map_or(0, |last_auto_index| last_auto_index % c + 1) - }); - if end.is_some_and(|end| end.get() < start) { - bail!(span, "line cannot end before it starts"); - } - let line = Line { index: x, start, end, stroke, position }; - - // For consistency with hlines, we only push vlines to - // the final vector of vlines after processing every - // cell. - pending_vlines.push((span, line)); - continue; - } - ResolvableGridItem::Cell(cell) => cell, - }; - let cell_span = cell.span(); - let colspan = cell.colspan(styles).get(); - let rowspan = cell.rowspan(styles).get(); - // Let's calculate the cell's final position based on its - // requested position. - let resolved_index = { - let cell_x = cell.x(styles); - let cell_y = cell.y(styles); - resolve_cell_position( - cell_x, - cell_y, - colspan, - rowspan, - &resolved_cells, - &mut auto_index, - &mut start_new_row, - c, - ) - .at(cell_span)? - }; - let x = resolved_index % c; - let y = resolved_index / c; - - if colspan > c - x { - bail!( - cell_span, - "cell's colspan would cause it to exceed the available column(s)"; - hint: "try placing the cell in another position or reducing its colspan" - ) - } - - let Some(largest_index) = c - .checked_mul(rowspan - 1) - .and_then(|full_rowspan_offset| { - resolved_index.checked_add(full_rowspan_offset) - }) - .and_then(|last_row_pos| last_row_pos.checked_add(colspan - 1)) - else { - bail!( - cell_span, - "cell would span an exceedingly large position"; - hint: "try reducing the cell's rowspan or colspan" - ) - }; - - // Let's resolve the cell so it can determine its own fields - // based on its final position. - let cell = cell.resolve_cell( - x, - y, - &fill.resolve(engine, styles, x, y)?, - align.resolve(engine, styles, x, y)?, - inset.resolve(engine, styles, x, y)?, - stroke.resolve(engine, styles, x, y)?, - resolve_breakable(y, rowspan), - locator.next(&cell_span), - styles, - ); - - if largest_index >= resolved_cells.len() { - // Ensure the length of the vector of resolved cells is - // always a multiple of 'c' by pushing full rows every - // time. Here, we add enough absent positions (later - // converted to empty cells) to ensure the last row in the - // new vector length is completely filled. This is - // necessary so that those positions, even if not - // explicitly used at the end, are eventually susceptible - // to show rules and receive grid styling, as they will be - // resolved as empty cells in a second loop below. - let Some(new_len) = largest_index - .checked_add(1) - .and_then(|new_len| new_len.checked_add((c - new_len % c) % c)) - else { - bail!(cell_span, "cell position too large") - }; - - // Here, the cell needs to be placed in a position which - // doesn't exist yet in the grid (out of bounds). We will - // add enough absent positions for this to be possible. - // They must be absent as no cells actually occupy them - // (they can be overridden later); however, if no cells - // occupy them as we finish building the grid, then such - // positions will be replaced by empty cells. - resolved_cells.resize_with(new_len, || None); - } - - // The vector is large enough to contain the cell, so we can - // just index it directly to access the position it will be - // placed in. However, we still need to ensure we won't try to - // place a cell where there already is one. - let slot = &mut resolved_cells[resolved_index]; - if slot.is_some() { - bail!( - cell_span, - "attempted to place a second cell at column {x}, row {y}"; - hint: "try specifying your cells in a different order" - ); - } - - *slot = Some(Entry::Cell(cell)); - - // Now, if the cell spans more than one row or column, we fill - // the spanned positions in the grid with Entry::Merged - // pointing to the original cell as its parent. - for rowspan_offset in 0..rowspan { - let spanned_y = y + rowspan_offset; - let first_row_index = resolved_index + c * rowspan_offset; - for (colspan_offset, slot) in resolved_cells[first_row_index..] - [..colspan] - .iter_mut() - .enumerate() - { - let spanned_x = x + colspan_offset; - if spanned_x == x && spanned_y == y { - // This is the parent cell. - continue; - } - if slot.is_some() { - bail!( - cell_span, - "cell would span a previously placed cell at column {spanned_x}, row {spanned_y}"; - hint: "try specifying your cells in a different order or reducing the cell's rowspan or colspan" - ) - } - *slot = Some(Entry::Merged { parent: resolved_index }); - } - } - - if is_header || is_footer { - // Ensure each cell in a header or footer is fully - // contained within it. - child_start = child_start.min(y); - child_end = child_end.max(y + rowspan); - - if start_new_row && child_start <= auto_index.div_ceil(c) { - // No need to start a new row as we already include - // the row of the next automatically positioned cell in - // the header or footer. - start_new_row = false; - } - - if !start_new_row { - // From now on, upcoming hlines won't be at the top of - // the child, as the first automatically positioned - // cell was placed. - first_index_of_non_top_hlines = - first_index_of_non_top_hlines.min(pending_hlines.len()); - } - } - } - - if (is_header || is_footer) && child_start == usize::MAX { - // Empty header/footer: consider the header/footer to be - // at the next empty row after the latest auto index. - auto_index = find_next_empty_row(&resolved_cells, auto_index, c); - child_start = auto_index.div_ceil(c); - child_end = child_start + 1; - - if resolved_cells.len() <= c * child_start { - // Ensure the automatically chosen row actually exists. - resolved_cells.resize_with(c * (child_start + 1), || None); - } - } - - if is_header { - if child_start != 0 { - bail!( - child_span, - "header must start at the first row"; - hint: "remove any rows before the header" - ); - } - - header = Some(Header { - // Later on, we have to correct this number in case there - // is gutter. But only once all cells have been analyzed - // and the header has fully expanded in the fixup loop - // below. - end: child_end, - }); - } - - if is_footer { - // Only check if the footer is at the end later, once we know - // the final amount of rows. - footer = Some(( - child_end, - child_span, - Footer { - // Later on, we have to correct this number in case there - // is gutter, but only once all cells have been analyzed - // and the header's and footer's exact boundaries are - // known. That is because the gutter row immediately - // before the footer might not be included as part of - // the footer if it is contained within the header. - start: child_start, - }, - )); - } - - if is_header || is_footer { - let amount_hlines = pending_hlines.len(); - for (_, top_hline, has_auto_y) in pending_hlines - .get_mut( - first_index_of_top_hlines - ..first_index_of_non_top_hlines.min(amount_hlines), - ) - .unwrap_or(&mut []) - { - if *has_auto_y { - // Move this hline to the top of the child, as it was - // placed before the first automatically positioned cell - // and had an automatic index. - top_hline.index = child_start; - } - } - - // Next automatically positioned cell goes under this header. - // FIXME: Consider only doing this if the header has any fully - // automatically positioned cells. Otherwise, - // `resolve_cell_position` should be smart enough to skip - // upcoming headers. - // Additionally, consider that cells with just an 'x' override - // could end up going too far back and making previous - // non-header rows into header rows (maybe they should be - // placed at the first row that is fully empty or something). - // Nothing we can do when both 'x' and 'y' were overridden, of - // course. - // None of the above are concerns for now, as headers must - // start at the first row. - auto_index = auto_index.max(c * child_end); - } - } - - // If the user specified cells occupying less rows than the given rows, - // we shall expand the grid so that it has at least the given amount of - // rows. - let Some(expected_total_cells) = c.checked_mul(tracks.y.len()) else { - bail!(span, "too many rows were specified"); - }; - let missing_cells = expected_total_cells.saturating_sub(resolved_cells.len()); - - // Fixup phase (final step in cell grid generation): - // 1. Replace absent entries by resolved empty cells, and produce a - // vector of 'Entry' from 'Option'. - // 2. Add enough empty cells to the end of the grid such that it has at - // least the given amount of rows. - // 3. If any cells were added to the header's rows after the header's - // creation, ensure the header expands enough to accommodate them - // across all of their spanned rows. Same for the footer. - // 4. If any cells before the footer try to span it, error. - let resolved_cells = resolved_cells - .into_iter() - .chain(std::iter::repeat_with(|| None).take(missing_cells)) - .enumerate() - .map(|(i, cell)| { - if let Some(cell) = cell { - if let Some(parent_cell) = cell.as_cell() { - if let Some(header) = &mut header - { - let y = i / c; - if y < header.end { - // Ensure the header expands enough such that - // all cells inside it, even those added later, - // are fully contained within the header. - // FIXME: check if start < y < end when start can - // be != 0. - // FIXME: when start can be != 0, decide what - // happens when a cell after the header placed - // above it tries to span the header (either - // error or expand upwards). - header.end = header.end.max(y + parent_cell.rowspan.get()); - } - } - - if let Some((end, footer_span, footer)) = &mut footer { - let x = i % c; - let y = i / c; - let cell_end = y + parent_cell.rowspan.get(); - if y < footer.start && cell_end > footer.start { - // Don't allow a cell before the footer to span - // it. Surely, we could move the footer to - // start at where this cell starts, so this is - // more of a design choice, as it's unlikely - // for the user to intentionally include a cell - // before the footer spanning it but not - // being repeated with it. - bail!( - *footer_span, - "footer would conflict with a cell placed before it at column {x} row {y}"; - hint: "try reducing that cell's rowspan or moving the footer" - ); - } - if y >= footer.start && y < *end { - // Expand the footer to include all rows - // spanned by this cell, as it is inside the - // footer. - *end = (*end).max(cell_end); - } - } - } - - Ok(cell) - } else { - let x = i % c; - let y = i / c; - - // Ensure all absent entries are affected by show rules and - // grid styling by turning them into resolved empty cells. - let new_cell = T::default().resolve_cell( - x, - y, - &fill.resolve(engine, styles, x, y)?, - align.resolve(engine, styles, x, y)?, - inset.resolve(engine, styles, x, y)?, - stroke.resolve(engine, styles, x, y)?, - resolve_breakable(y, 1), - locator.next(&()), - styles, - ); - Ok(Entry::Cell(new_cell)) - } - }) - .collect::>>()?; - - // Populate the final lists of lines. - // For each line type (horizontal or vertical), we keep a vector for - // every group of lines with the same index. - let mut vlines: Vec> = vec![]; - let mut hlines: Vec> = vec![]; - let row_amount = resolved_cells.len().div_ceil(c); - - for (line_span, line, _) in pending_hlines { - let y = line.index; - if y > row_amount { - bail!(line_span, "cannot place horizontal line at invalid row {y}"); - } - if y == row_amount && line.position == LinePosition::After { - bail!( - line_span, - "cannot place horizontal line at the 'bottom' position of the bottom border (y = {y})"; - hint: "set the line's position to 'top' or place it at a smaller 'y' index" - ); - } - let line = if line.position == LinePosition::After - && (!has_gutter || y + 1 == row_amount) - { - // Just place the line on top of the next row if - // there's no gutter and the line should be placed - // after the one with given index. - // - // Note that placing after the last row is also the same as - // just placing on the grid's bottom border, even with - // gutter. - Line { - index: y + 1, - position: LinePosition::Before, - ..line - } - } else { - line - }; - let y = line.index; - - if hlines.len() <= y { - hlines.resize_with(y + 1, Vec::new); - } - hlines[y].push(line); - } - - for (line_span, line) in pending_vlines { - let x = line.index; - if x > c { - bail!(line_span, "cannot place vertical line at invalid column {x}"); - } - if x == c && line.position == LinePosition::After { - bail!( - line_span, - "cannot place vertical line at the 'end' position of the end border (x = {c})"; - hint: "set the line's position to 'start' or place it at a smaller 'x' index" - ); - } - let line = - if line.position == LinePosition::After && (!has_gutter || x + 1 == c) { - // Just place the line before the next column if - // there's no gutter and the line should be placed - // after the one with given index. - // - // Note that placing after the last column is also the - // same as just placing on the grid's end border, even - // with gutter. - Line { - index: x + 1, - position: LinePosition::Before, - ..line - } - } else { - line - }; - let x = line.index; - - if vlines.len() <= x { - vlines.resize_with(x + 1, Vec::new); - } - vlines[x].push(line); - } - - let header = header - .map(|mut header| { - // Repeat the gutter below a header (hence why we don't - // subtract 1 from the gutter case). - // Don't do this if there are no rows under the header. - if has_gutter { - // - 'header.end' is always 'last y + 1'. The header stops - // before that row. - // - Therefore, '2 * header.end' will be 2 * (last y + 1), - // which is the adjusted index of the row before which the - // header stops, meaning it will still stop right before it - // even with gutter thanks to the multiplication below. - // - This means that it will span all rows up to - // '2 * (last y + 1) - 1 = 2 * last y + 1', which equates - // to the index of the gutter row right below the header, - // which is what we want (that gutter spacing should be - // repeated across pages to maintain uniformity). - header.end *= 2; - - // If the header occupies the entire grid, ensure we don't - // include an extra gutter row when it doesn't exist, since - // the last row of the header is at the very bottom, - // therefore '2 * last y + 1' is not a valid index. - let row_amount = (2 * row_amount).saturating_sub(1); - header.end = header.end.min(row_amount); - } - header - }) - .map(|header| { - if repeat_header { - Repeatable::Repeated(header) - } else { - Repeatable::NotRepeated(header) - } - }); - - let footer = footer - .map(|(footer_end, footer_span, mut footer)| { - if footer_end != row_amount { - bail!(footer_span, "footer must end at the last row"); - } - - let header_end = - header.as_ref().map(Repeatable::unwrap).map(|header| header.end); - - if has_gutter { - // Convert the footer's start index to post-gutter coordinates. - footer.start *= 2; - - // Include the gutter right before the footer, unless there is - // none, or the gutter is already included in the header (no - // rows between the header and the footer). - if header_end != Some(footer.start) { - footer.start = footer.start.saturating_sub(1); - } - } - - if header_end.is_some_and(|header_end| header_end > footer.start) { - bail!(footer_span, "header and footer must not have common rows"); - } - - Ok(footer) - }) - .transpose()? - .map(|footer| { - if repeat_footer { - Repeatable::Repeated(footer) - } else { - Repeatable::NotRepeated(footer) - } - }); - - Ok(Self::new_internal( - tracks, - gutter, - vlines, - hlines, - header, - footer, - resolved_cells, - )) - } - /// Generates the cell grid, given the tracks and resolved entries. pub fn new_internal( tracks: Axes<&[Sizing]>, @@ -1432,14 +671,14 @@ impl<'a> CellGrid<'a> { let mut rows = vec![]; // Number of content columns: Always at least one. - let c = tracks.x.len().max(1); + let num_cols = tracks.x.len().max(1); // Number of content rows: At least as many as given, but also at least // as many as needed to place each item. - let r = { + let num_rows = { let len = entries.len(); let given = tracks.y.len(); - let needed = len / c + (len % c).clamp(0, 1); + let needed = len / num_cols + (len % num_cols).clamp(0, 1); given.max(needed) }; @@ -1451,7 +690,7 @@ impl<'a> CellGrid<'a> { }; // Collect content and gutter columns. - for x in 0..c { + for x in 0..num_cols { cols.push(get_or(tracks.x, x, auto)); if has_gutter { cols.push(get_or(gutter.x, x, zero)); @@ -1459,7 +698,7 @@ impl<'a> CellGrid<'a> { } // Collect content and gutter rows. - for y in 0..r { + for y in 0..num_rows { rows.push(get_or(tracks.y, y, auto)); if has_gutter { rows.push(get_or(gutter.y, y, zero)); @@ -1615,25 +854,1140 @@ impl<'a> CellGrid<'a> { } } +/// Resolves and positions all cells in the grid before creating it. +/// Allows them to keep track of their final properties and positions +/// and adjust their fields accordingly. +/// Cells must implement Clone as they will be owned. Additionally, they +/// must implement Default in order to fill positions in the grid which +/// weren't explicitly specified by the user with empty cells. +#[allow(clippy::too_many_arguments)] +pub fn resolve_cellgrid<'a, 'x, T, C, I>( + tracks: Axes<&'a [Sizing]>, + gutter: Axes<&'a [Sizing]>, + locator: Locator<'x>, + children: C, + fill: &'a Celled>, + align: &'a Celled>, + inset: &'a Celled>>>, + stroke: &'a ResolvedCelled>>>>, + engine: &'a mut Engine, + styles: StyleChain<'a>, + span: Span, +) -> SourceResult> +where + T: ResolvableCell + Default, + I: Iterator>, + C: IntoIterator>, + C::IntoIter: ExactSizeIterator, +{ + CellGridResolver { + tracks, + gutter, + locator: locator.split(), + fill, + align, + inset, + stroke, + engine, + styles, + span, + } + .resolve(children) +} + +struct CellGridResolver<'a, 'b, 'x> { + tracks: Axes<&'a [Sizing]>, + gutter: Axes<&'a [Sizing]>, + locator: SplitLocator<'x>, + fill: &'a Celled>, + align: &'a Celled>, + inset: &'a Celled>>>, + stroke: &'a ResolvedCelled>>>>, + engine: &'a mut Engine<'b>, + styles: StyleChain<'a>, + span: Span, +} + +#[derive(Debug, Clone, Copy)] +enum RowGroupKind { + Header, + Footer, +} + +impl RowGroupKind { + fn name(self) -> &'static str { + match self { + Self::Header => "header", + Self::Footer => "footer", + } + } +} + +struct RowGroupData { + /// The range of rows of cells inside this grid row group. The + /// first and last rows are guaranteed to have cells (an exception + /// is made when there is gutter, in which case the group range may + /// be expanded to include an additional gutter row when there is a + /// repeatable header or footer). This is `None` until the first + /// cell of the row group is placed, then it is continually adjusted + /// to fit the cells inside the row group. + /// + /// This stays as `None` for fully empty headers and footers. + range: Option>, + span: Span, + kind: RowGroupKind, + + /// Start of the range of indices of hlines at the top of the row group. + /// This is always the first index after the last hline before we started + /// building the row group - any upcoming hlines would appear at least at + /// this index. + /// + /// These hlines were auto-positioned and appeared before any auto-pos + /// cells, so they will appear at the first possible row (above the + /// first row spanned by the row group). + top_hlines_start: usize, + + /// End of the range of indices of hlines at the top of the row group. + /// + /// This starts as `None`, meaning that, if we stop the loop before we find + /// any auto-pos cells, all auto-pos hlines after the last hline (after the + /// index `top_hlines_start`) should be moved to the top of the row group. + /// + /// It becomes `Some(index of last hline at the top)` when an auto-pos cell + /// is found, as auto-pos hlines after any auto-pos cells appear below + /// them, not at the top of the row group. + top_hlines_end: Option, +} + +impl<'x> CellGridResolver<'_, '_, 'x> { + fn resolve(mut self, children: C) -> SourceResult> + where + T: ResolvableCell + Default, + I: Iterator>, + C: IntoIterator>, + C::IntoIter: ExactSizeIterator, + { + // Number of content columns: Always at least one. + let columns = self.tracks.x.len().max(1); + + // Lists of lines. + // Horizontal lines are only pushed later to be able to check for row + // validity, since the amount of rows isn't known until all items were + // analyzed in the for loop below. + // We keep their spans so we can report errors later. + // The additional boolean indicates whether the hline had an automatic + // 'y' index, and is used to change the index of hlines at the top of a + // header or footer. + let mut pending_hlines: Vec<(Span, Line, bool)> = vec![]; + + // For consistency, only push vertical lines later as well. + let mut pending_vlines: Vec<(Span, Line)> = vec![]; + let has_gutter = self.gutter.any(|tracks| !tracks.is_empty()); + + let mut header: Option
= None; + let mut repeat_header = false; + + // Stores where the footer is supposed to end, its span, and the + // actual footer structure. + let mut footer: Option<(usize, Span, Footer)> = None; + let mut repeat_footer = false; + + // We can't just use the cell's index in the 'cells' vector to + // determine its automatic position, since cells could have arbitrary + // positions, so the position of a cell in 'cells' can differ from its + // final position in 'resolved_cells' (see below). + // Therefore, we use a counter, 'auto_index', to determine the position + // of the next cell with (x: auto, y: auto). It is only stepped when + // a cell with (x: auto, y: auto), usually the vast majority, is found. + // + // Note that a separate counter ('local_auto_index') is used within + // headers and footers, as explained above its definition. Outside of + // those (when the table child being processed is a single cell), + // 'local_auto_index' will simply be an alias for 'auto_index', which + // will be updated after that cell is placed, if it is an + // automatically-positioned cell. + let mut auto_index: usize = 0; + + // We have to rebuild the grid to account for fixed cell positions. + // + // Create at least 'children.len()' positions, since there could be at + // least 'children.len()' cells (if no explicit lines were specified), + // even though some of them might be placed in fixed positions and thus + // cause the grid to expand. + // + // Additionally, make sure we allocate up to the next multiple of + // 'columns', since each row will have 'columns' cells, even if the + // last few cells weren't explicitly specified by the user. + let children = children.into_iter(); + let Some(child_count) = children.len().checked_next_multiple_of(columns) else { + bail!(self.span, "too many cells or lines were given") + }; + let mut resolved_cells: Vec> = Vec::with_capacity(child_count); + for child in children { + self.resolve_grid_child( + columns, + &mut pending_hlines, + &mut pending_vlines, + &mut header, + &mut repeat_header, + &mut footer, + &mut repeat_footer, + &mut auto_index, + &mut resolved_cells, + child, + )?; + } + + let resolved_cells = self.fixup_cells::(resolved_cells, columns)?; + + let row_amount = resolved_cells.len().div_ceil(columns); + let (hlines, vlines) = self.collect_lines( + pending_hlines, + pending_vlines, + has_gutter, + columns, + row_amount, + )?; + + let (header, footer) = self.finalize_headers_and_footers( + has_gutter, + header, + repeat_header, + footer, + repeat_footer, + row_amount, + )?; + + Ok(CellGrid::new_internal( + self.tracks, + self.gutter, + vlines, + hlines, + header, + footer, + resolved_cells, + )) + } + + /// Resolve a grid child, which can be a header, a footer (both of which + /// are row groups, and thus contain multiple grid items inside them), or + /// a grid item - a cell, an hline or a vline. + /// + /// This process consists of placing the child and any sub-items into + /// appropriate positions in the resolved grid. This is mostly relevant for + /// items without fixed positions, such that they must be placed after the + /// previous one, perhaps skipping existing cells along the way. + #[allow(clippy::too_many_arguments)] + fn resolve_grid_child( + &mut self, + columns: usize, + pending_hlines: &mut Vec<(Span, Line, bool)>, + pending_vlines: &mut Vec<(Span, Line)>, + header: &mut Option
, + repeat_header: &mut bool, + footer: &mut Option<(usize, Span, Footer)>, + repeat_footer: &mut bool, + auto_index: &mut usize, + resolved_cells: &mut Vec>>, + child: ResolvableGridChild, + ) -> SourceResult<()> + where + T: ResolvableCell + Default, + I: Iterator>, + { + // Data for the row group in this iteration. + // + // Note that cells outside headers and footers are grid children + // with a single cell inside, and thus not considered row groups, + // in which case this variable remains 'None'. + let mut row_group_data: Option = None; + + // The normal auto index should only be stepped (upon placing an + // automatically-positioned cell, to indicate the position of the + // next) outside of headers or footers, in which case the auto + // index will be updated with the local auto index. Inside headers + // and footers, however, cells can only start after the first empty + // row (as determined by 'first_available_row' below), meaning that + // the next automatically-positioned cell will be in a different + // position than it would usually be if it would be in a non-empty + // row, so we must step a local index inside headers and footers + // instead, and use a separate counter outside them. + let mut local_auto_index = *auto_index; + + // The first row in which this table group can fit. + // + // Within headers and footers, this will correspond to the first + // fully empty row available in the grid. This is because headers + // and footers always occupy entire rows, so they cannot occupy + // a non-empty row. + let mut first_available_row = 0; + + let (header_footer_items, simple_item) = match child { + ResolvableGridChild::Header { repeat, span, items, .. } => { + if header.is_some() { + bail!(span, "cannot have more than one header"); + } + + row_group_data = Some(RowGroupData { + range: None, + span, + kind: RowGroupKind::Header, + top_hlines_start: pending_hlines.len(), + top_hlines_end: None, + }); + + *repeat_header = repeat; + + first_available_row = + find_next_empty_row(resolved_cells, local_auto_index, columns); + + // If any cell in the header is automatically positioned, + // have it skip to the next empty row. This is to avoid + // having a header after a partially filled row just add + // cells to that row instead of starting a new one. + // + // Note that the first fully empty row is always after the + // latest auto-position cell, since each auto-position cell + // always occupies the first available position after the + // previous one. Therefore, this will be >= auto_index. + local_auto_index = first_available_row * columns; + + (Some(items), None) + } + ResolvableGridChild::Footer { repeat, span, items, .. } => { + if footer.is_some() { + bail!(span, "cannot have more than one footer"); + } + + row_group_data = Some(RowGroupData { + range: None, + span, + kind: RowGroupKind::Footer, + top_hlines_start: pending_hlines.len(), + top_hlines_end: None, + }); + + *repeat_footer = repeat; + + first_available_row = + find_next_empty_row(resolved_cells, local_auto_index, columns); + + local_auto_index = first_available_row * columns; + + (Some(items), None) + } + ResolvableGridChild::Item(item) => (None, Some(item)), + }; + + let items = header_footer_items.into_iter().flatten().chain(simple_item); + for item in items { + let cell = match item { + ResolvableGridItem::HLine { y, start, end, stroke, span, position } => { + let has_auto_y = y.is_auto(); + let y = y.unwrap_or_else(|| { + // Avoid placing the hline inside consecutive + // rowspans occupying all columns, as it'd just + // disappear, at least when there's no column + // gutter. + skip_auto_index_through_fully_merged_rows( + resolved_cells, + &mut local_auto_index, + columns, + ); + + // When no 'y' is specified for the hline, we place + // it under the latest automatically positioned + // cell. + // The current value of the auto index is always + // the index of the latest automatically positioned + // cell placed plus one (that's what we do in + // 'resolve_cell_position'), so we subtract 1 to + // get that cell's index, and place the hline below + // its row. The exception is when the auto_index is + // 0, meaning no automatically positioned cell was + // placed yet. In that case, we place the hline at + // the top of the table. + // + // Exceptionally, the hline will be placed before + // the minimum auto index if the current auto index + // from previous iterations is smaller than the + // minimum it should have for the current grid + // child. Effectively, this means that a hline at + // the start of a header will always appear above + // that header's first row. Similarly for footers. + local_auto_index + .checked_sub(1) + .map_or(0, |last_auto_index| last_auto_index / columns + 1) + }); + if end.is_some_and(|end| end.get() < start) { + bail!(span, "line cannot end before it starts"); + } + let line = Line { index: y, start, end, stroke, position }; + + // Since the amount of rows is dynamic, delay placing + // hlines until after all cells were placed so we can + // properly verify if they are valid. Note that we + // can't place hlines even if we already know they + // would be in a valid row, since it's possible that we + // pushed pending hlines in the same row as this one in + // previous iterations, and we need to ensure that + // hlines from previous iterations are pushed to the + // final vector of hlines first - the order of hlines + // must be kept, as this matters when determining which + // one "wins" in case of conflict. Pushing the current + // hline before we push pending hlines later would + // change their order! + pending_hlines.push((span, line, has_auto_y)); + continue; + } + ResolvableGridItem::VLine { x, start, end, stroke, span, position } => { + let x = x.unwrap_or_else(|| { + // When no 'x' is specified for the vline, we place + // it after the latest automatically positioned + // cell. + // The current value of the auto index is always + // the index of the latest automatically positioned + // cell placed plus one (that's what we do in + // 'resolve_cell_position'), so we subtract 1 to + // get that cell's index, and place the vline after + // its column. The exception is when the auto_index + // is 0, meaning no automatically positioned cell + // was placed yet. In that case, we place the vline + // to the left of the table. + // + // Exceptionally, a vline is also placed to the + // left of the table when specified at the start + // of a row group, such as a header or footer, that + // is, when no automatically-positioned cells have + // been specified for that group yet. + // For example, this means that a vline at + // the beginning of a header will be placed to its + // left rather than after the previous + // automatically positioned cell. Same for footers. + local_auto_index + .checked_sub(1) + .filter(|_| local_auto_index > first_available_row * columns) + .map_or(0, |last_auto_index| last_auto_index % columns + 1) + }); + if end.is_some_and(|end| end.get() < start) { + bail!(span, "line cannot end before it starts"); + } + let line = Line { index: x, start, end, stroke, position }; + + // For consistency with hlines, we only push vlines to + // the final vector of vlines after processing every + // cell. + pending_vlines.push((span, line)); + continue; + } + ResolvableGridItem::Cell(cell) => cell, + }; + let cell_span = cell.span(); + let colspan = cell.colspan(self.styles).get(); + let rowspan = cell.rowspan(self.styles).get(); + // Let's calculate the cell's final position based on its + // requested position. + let resolved_index = { + let cell_x = cell.x(self.styles); + let cell_y = cell.y(self.styles); + resolve_cell_position( + cell_x, + cell_y, + colspan, + rowspan, + header.as_ref(), + footer.as_ref(), + resolved_cells, + &mut local_auto_index, + first_available_row, + columns, + row_group_data.is_some(), + ) + .at(cell_span)? + }; + let x = resolved_index % columns; + let y = resolved_index / columns; + + if colspan > columns - x { + bail!( + cell_span, + "cell's colspan would cause it to exceed the available column(s)"; + hint: "try placing the cell in another position or reducing its colspan" + ) + } + + let Some(largest_index) = columns + .checked_mul(rowspan - 1) + .and_then(|full_rowspan_offset| { + resolved_index.checked_add(full_rowspan_offset) + }) + .and_then(|last_row_pos| last_row_pos.checked_add(colspan - 1)) + else { + bail!( + cell_span, + "cell would span an exceedingly large position"; + hint: "try reducing the cell's rowspan or colspan" + ) + }; + + // Cell's header or footer must expand to include the cell's + // occupied positions, if possible. + if let Some(RowGroupData { + range: group_range, kind, top_hlines_end, .. + }) = &mut row_group_data + { + *group_range = Some( + expand_row_group( + resolved_cells, + group_range.as_ref(), + *kind, + first_available_row, + y, + rowspan, + columns, + ) + .at(cell_span)?, + ); + + if top_hlines_end.is_none() + && local_auto_index > first_available_row * columns + { + // Auto index was moved, so upcoming auto-pos hlines should + // no longer appear at the top. + *top_hlines_end = Some(pending_hlines.len()); + } + } + + // Let's resolve the cell so it can determine its own fields + // based on its final position. + let cell = self.resolve_cell(cell, x, y, rowspan, cell_span)?; + + if largest_index >= resolved_cells.len() { + // Ensure the length of the vector of resolved cells is + // always a multiple of 'columns' by pushing full rows every + // time. Here, we add enough absent positions (later + // converted to empty cells) to ensure the last row in the + // new vector length is completely filled. This is + // necessary so that those positions, even if not + // explicitly used at the end, are eventually susceptible + // to show rules and receive grid styling, as they will be + // resolved as empty cells in a second loop below. + let Some(new_len) = largest_index + .checked_add(1) + .and_then(|new_len| new_len.checked_next_multiple_of(columns)) + else { + bail!(cell_span, "cell position too large") + }; + + // Here, the cell needs to be placed in a position which + // doesn't exist yet in the grid (out of bounds). We will + // add enough absent positions for this to be possible. + // They must be absent as no cells actually occupy them + // (they can be overridden later); however, if no cells + // occupy them as we finish building the grid, then such + // positions will be replaced by empty cells. + resolved_cells.resize_with(new_len, || None); + } + + // The vector is large enough to contain the cell, so we can + // just index it directly to access the position it will be + // placed in. However, we still need to ensure we won't try to + // place a cell where there already is one. + let slot = &mut resolved_cells[resolved_index]; + if slot.is_some() { + bail!( + cell_span, + "attempted to place a second cell at column {x}, row {y}"; + hint: "try specifying your cells in a different order" + ); + } + + *slot = Some(Entry::Cell(cell)); + + // Now, if the cell spans more than one row or column, we fill + // the spanned positions in the grid with Entry::Merged + // pointing to the original cell as its parent. + for rowspan_offset in 0..rowspan { + let spanned_y = y + rowspan_offset; + let first_row_index = resolved_index + columns * rowspan_offset; + for (colspan_offset, slot) in + resolved_cells[first_row_index..][..colspan].iter_mut().enumerate() + { + let spanned_x = x + colspan_offset; + if spanned_x == x && spanned_y == y { + // This is the parent cell. + continue; + } + if slot.is_some() { + bail!( + cell_span, + "cell would span a previously placed cell at column {spanned_x}, row {spanned_y}"; + hint: "try specifying your cells in a different order or reducing the cell's rowspan or colspan" + ) + } + *slot = Some(Entry::Merged { parent: resolved_index }); + } + } + } + + if let Some(row_group) = row_group_data { + let group_range = match row_group.range { + Some(group_range) => group_range, + + None => { + // Empty header/footer: consider the header/footer to be + // at the next empty row after the latest auto index. + local_auto_index = first_available_row * columns; + let group_start = first_available_row; + let group_end = group_start + 1; + + if resolved_cells.len() <= columns * group_start { + // Ensure the automatically chosen row actually exists. + resolved_cells.resize_with(columns * (group_start + 1), || None); + } + + // Even though this header or footer is fully empty, we add one + // default cell to maintain the invariant that each header and + // footer has at least one 'Some(...)' cell at its first row + // and at least one at its last row (here they are the same + // row, of course). This invariant is important to ensure + // 'find_next_empty_row' will skip through any existing headers + // and footers without having to loop through them each time. + // Cells themselves, unfortunately, still have to. + assert!(resolved_cells[local_auto_index].is_none()); + resolved_cells[local_auto_index] = + Some(Entry::Cell(self.resolve_cell( + T::default(), + 0, + first_available_row, + 1, + Span::detached(), + )?)); + + group_start..group_end + } + }; + + let top_hlines_end = row_group.top_hlines_end.unwrap_or(pending_hlines.len()); + for (_, top_hline, has_auto_y) in pending_hlines + .get_mut(row_group.top_hlines_start..top_hlines_end) + .unwrap_or(&mut []) + { + if *has_auto_y { + // Move this hline to the top of the child, as it was + // placed before the first automatically positioned cell + // and had an automatic index. + top_hline.index = group_range.start; + } + } + + match row_group.kind { + RowGroupKind::Header => { + if group_range.start != 0 { + bail!( + row_group.span, + "header must start at the first row"; + hint: "remove any rows before the header" + ); + } + + *header = Some(Header { + // Later on, we have to correct this number in case there + // is gutter. But only once all cells have been analyzed + // and the header has fully expanded in the fixup loop + // below. + end: group_range.end, + }); + } + + RowGroupKind::Footer => { + // Only check if the footer is at the end later, once we know + // the final amount of rows. + *footer = Some(( + group_range.end, + row_group.span, + Footer { + // Later on, we have to correct this number in case there + // is gutter, but only once all cells have been analyzed + // and the header's and footer's exact boundaries are + // known. That is because the gutter row immediately + // before the footer might not be included as part of + // the footer if it is contained within the header. + start: group_range.start, + }, + )); + } + } + } else { + // The child was a single cell outside headers or footers. + // Therefore, 'local_auto_index' for this table child was + // simply an alias for 'auto_index', so we update it as needed. + *auto_index = local_auto_index; + } + + Ok(()) + } + + /// Fixup phase (final step in cell grid generation): + /// + /// 1. Replace absent entries by resolved empty cells, producing a vector + /// of `Entry` from `Option`. + /// + /// 2. Add enough empty cells to the end of the grid such that it has at + /// least the given amount of rows (must be a multiple of `columns`, + /// and all rows before the last cell must have cells, empty or not, + /// even if the user didn't specify those cells). + /// + /// That is necessary, for example, to ensure even unspecified cells + /// can be affected by show rules and grid-wide styling. + fn fixup_cells( + &mut self, + resolved_cells: Vec>>, + columns: usize, + ) -> SourceResult>> + where + T: ResolvableCell + Default, + { + let Some(expected_total_cells) = columns.checked_mul(self.tracks.y.len()) else { + bail!(self.span, "too many rows were specified"); + }; + let missing_cells = expected_total_cells.saturating_sub(resolved_cells.len()); + + resolved_cells + .into_iter() + .chain(std::iter::repeat_with(|| None).take(missing_cells)) + .enumerate() + .map(|(i, cell)| { + if let Some(cell) = cell { + Ok(cell) + } else { + let x = i % columns; + let y = i / columns; + + Ok(Entry::Cell(self.resolve_cell( + T::default(), + x, + y, + 1, + Span::detached(), + )?)) + } + }) + .collect::>>() + } + + /// Takes the list of pending lines and evaluates a final list of hlines + /// and vlines (in that order in the returned tuple), detecting invalid + /// line positions in the process. + /// + /// For each line type (horizontal and vertical respectively), returns a + /// vector containing one inner vector for every group of lines with the + /// same index. + /// + /// For example, an hline above the second row (y = 1) is inside the inner + /// vector at position 1 of the first vector (hlines) returned by this + /// function. + #[allow(clippy::type_complexity)] + fn collect_lines( + &self, + pending_hlines: Vec<(Span, Line, bool)>, + pending_vlines: Vec<(Span, Line)>, + has_gutter: bool, + columns: usize, + row_amount: usize, + ) -> SourceResult<(Vec>, Vec>)> { + let mut hlines: Vec> = vec![]; + let mut vlines: Vec> = vec![]; + + for (line_span, line, _) in pending_hlines { + let y = line.index; + if y > row_amount { + bail!(line_span, "cannot place horizontal line at invalid row {y}"); + } + if y == row_amount && line.position == LinePosition::After { + bail!( + line_span, + "cannot place horizontal line at the 'bottom' position of the bottom border (y = {y})"; + hint: "set the line's position to 'top' or place it at a smaller 'y' index" + ); + } + let line = if line.position == LinePosition::After + && (!has_gutter || y + 1 == row_amount) + { + // Just place the line on top of the next row if + // there's no gutter and the line should be placed + // after the one with given index. + // + // Note that placing after the last row is also the same as + // just placing on the grid's bottom border, even with + // gutter. + Line { + index: y + 1, + position: LinePosition::Before, + ..line + } + } else { + line + }; + let y = line.index; + + if hlines.len() <= y { + hlines.resize_with(y + 1, Vec::new); + } + hlines[y].push(line); + } + + for (line_span, line) in pending_vlines { + let x = line.index; + if x > columns { + bail!(line_span, "cannot place vertical line at invalid column {x}"); + } + if x == columns && line.position == LinePosition::After { + bail!( + line_span, + "cannot place vertical line at the 'end' position of the end border (x = {columns})"; + hint: "set the line's position to 'start' or place it at a smaller 'x' index" + ); + } + let line = if line.position == LinePosition::After + && (!has_gutter || x + 1 == columns) + { + // Just place the line before the next column if + // there's no gutter and the line should be placed + // after the one with given index. + // + // Note that placing after the last column is also the + // same as just placing on the grid's end border, even + // with gutter. + Line { + index: x + 1, + position: LinePosition::Before, + ..line + } + } else { + line + }; + let x = line.index; + + if vlines.len() <= x { + vlines.resize_with(x + 1, Vec::new); + } + vlines[x].push(line); + } + + Ok((hlines, vlines)) + } + + /// Generate the final headers and footers: + /// + /// 1. Convert gutter-ignorant to gutter-aware indices if necessary; + /// 2. Expand the header downwards (or footer upwards) to also include + /// an adjacent gutter row to be repeated alongside that header or + /// footer, if there is gutter; + /// 3. Wrap headers and footers in the correct [`Repeatable`] variant. + #[allow(clippy::type_complexity)] + fn finalize_headers_and_footers( + &self, + has_gutter: bool, + header: Option
, + repeat_header: bool, + footer: Option<(usize, Span, Footer)>, + repeat_footer: bool, + row_amount: usize, + ) -> SourceResult<(Option>, Option>)> { + let header = header + .map(|mut header| { + // Repeat the gutter below a header (hence why we don't + // subtract 1 from the gutter case). + // Don't do this if there are no rows under the header. + if has_gutter { + // - 'header.end' is always 'last y + 1'. The header stops + // before that row. + // - Therefore, '2 * header.end' will be 2 * (last y + 1), + // which is the adjusted index of the row before which the + // header stops, meaning it will still stop right before it + // even with gutter thanks to the multiplication below. + // - This means that it will span all rows up to + // '2 * (last y + 1) - 1 = 2 * last y + 1', which equates + // to the index of the gutter row right below the header, + // which is what we want (that gutter spacing should be + // repeated across pages to maintain uniformity). + header.end *= 2; + + // If the header occupies the entire grid, ensure we don't + // include an extra gutter row when it doesn't exist, since + // the last row of the header is at the very bottom, + // therefore '2 * last y + 1' is not a valid index. + let row_amount = (2 * row_amount).saturating_sub(1); + header.end = header.end.min(row_amount); + } + header + }) + .map(|header| { + if repeat_header { + Repeatable::Repeated(header) + } else { + Repeatable::NotRepeated(header) + } + }); + + let footer = footer + .map(|(footer_end, footer_span, mut footer)| { + if footer_end != row_amount { + bail!(footer_span, "footer must end at the last row"); + } + + let header_end = + header.as_ref().map(Repeatable::unwrap).map(|header| header.end); + + if has_gutter { + // Convert the footer's start index to post-gutter coordinates. + footer.start *= 2; + + // Include the gutter right before the footer, unless there is + // none, or the gutter is already included in the header (no + // rows between the header and the footer). + if header_end != Some(footer.start) { + footer.start = footer.start.saturating_sub(1); + } + } + + Ok(footer) + }) + .transpose()? + .map(|footer| { + if repeat_footer { + Repeatable::Repeated(footer) + } else { + Repeatable::NotRepeated(footer) + } + }); + + Ok((header, footer)) + } + + /// Resolves the cell's fields based on grid-wide properties. + fn resolve_cell( + &mut self, + cell: T, + x: usize, + y: usize, + rowspan: usize, + cell_span: Span, + ) -> SourceResult> + where + T: ResolvableCell + Default, + { + // Resolve the breakability of a cell. Cells that span at least one + // auto-sized row or gutter are considered breakable. + let breakable = { + let auto = Sizing::Auto; + let zero = Sizing::Rel(Rel::zero()); + self.tracks + .y + .iter() + .chain(std::iter::repeat(self.tracks.y.last().unwrap_or(&auto))) + .skip(y) + .take(rowspan) + .any(|row| row == &Sizing::Auto) + || self + .gutter + .y + .iter() + .chain(std::iter::repeat(self.gutter.y.last().unwrap_or(&zero))) + .skip(y) + .take(rowspan - 1) + .any(|row_gutter| row_gutter == &Sizing::Auto) + }; + + Ok(cell.resolve_cell( + x, + y, + &self.fill.resolve(self.engine, self.styles, x, y)?, + self.align.resolve(self.engine, self.styles, x, y)?, + self.inset.resolve(self.engine, self.styles, x, y)?, + self.stroke.resolve(self.engine, self.styles, x, y)?, + breakable, + self.locator.next(&cell_span), + self.styles, + )) + } +} + +/// Given the existing range of a row group (header or footer), tries to expand +/// it to fit the new cell placed inside it. If the newly-expanded row group +/// would conflict with existing cells or other row groups, an error is +/// returned. Otherwise, the new `start..end` range of rows in the row group is +/// returned. +fn expand_row_group( + resolved_cells: &[Option>], + group_range: Option<&Range>, + group_kind: RowGroupKind, + first_available_row: usize, + cell_y: usize, + rowspan: usize, + columns: usize, +) -> HintedStrResult> { + // Ensure each cell in a header or footer is fully contained within it by + // expanding the header or footer towards this new cell. + let (new_group_start, new_group_end) = group_range + .map_or((cell_y, cell_y + rowspan), |r| { + (r.start.min(cell_y), r.end.max(cell_y + rowspan)) + }); + + // This check might be unnecessary with the loop below, but let's keep it + // here for full correctness. + // + // Quickly detect the case: + // y = 0 => occupied + // y = 1 => empty + // y = 2 => header + // and header tries to expand to y = 0 - invalid, as + // 'y = 1' is the earliest row it can occupy. + if new_group_start < first_available_row { + bail!( + "cell would cause {} to expand to non-empty row {}", + group_kind.name(), + first_available_row.saturating_sub(1); + hint: "try moving its cells to available rows" + ); + } + + let new_rows = + group_range.map_or((new_group_start..new_group_end).chain(0..0), |r| { + // NOTE: 'r.end' is one row AFTER the row group's last row, so it + // makes sense to check it if 'new_group_end > r.end', that is, if + // the row group is going to expand. It is NOT a duplicate check, + // as we hadn't checked it before (in a previous run, it was + // 'new_group_end' at the exclusive end of the range)! + // + // NOTE: To keep types the same, we have to always return + // '(range).chain(range)', which justifies chaining an empty + // range above. + (new_group_start..r.start).chain(r.end..new_group_end) + }); + + // The check above isn't enough, however, even when the header is expanding + // upwards, as it might expand upwards towards an occupied row after the + // first empty row, e.g. + // + // y = 0 => occupied + // y = 1 => empty (first_available_row = 1) + // y = 2 => occupied + // y = 3 => header + // + // Here, we should bail if the header tries to expand upwards, regardless + // of the fact that the conflicting row (y = 2) comes after the first + // available row. + // + // Note that expanding upwards is only possible when row-positioned cells + // are specified, in one of the following cases: + // + // 1. We place e.g. 'table.cell(y: 3)' followed by 'table.cell(y: 2)' + // (earlier row => upwards); + // + // 2. We place e.g. 'table.cell(y: 3)' followed by '[a]' (auto-pos cell + // favors 'first_available_row', so the header tries to expand upwards to + // place the cell at 'y = 1' and conflicts at 'y = 2') or + // 'table.cell(x: 1)' (same deal). + // + // Of course, we also need to check for downward expansion as usual as + // there could be a non-empty row below the header, but the upward case is + // highlighted as it was checked separately before (and also to explain + // what kind of situation we are preventing with this check). + // + // Note that simply checking for non-empty rows like below not only + // prevents conflicts with top-level cells (outside of headers and + // footers), but also prevents conflicts with other headers or footers, + // since we have an invariant that even empty headers and footers must + // contain at least one 'Some(...)' position in 'resolved_cells'. More + // precisely, each header and footer has at least one 'Some(...)' cell at + // 'group_range.start' and at 'group_range.end - 1' - non-empty headers and + // footers don't span any unnecessary rows. Therefore, we don't have to + // loop over headers and footers, only check if the new rows are empty. + for new_y in new_rows { + if let Some(new_row @ [_non_empty, ..]) = resolved_cells + .get(new_y * columns..) + .map(|cells| &cells[..columns.min(cells.len())]) + { + if new_row.iter().any(Option::is_some) { + bail!( + "cell would cause {} to expand to non-empty row {new_y}", + group_kind.name(); + hint: "try moving its cells to available rows", + ) + } + } else { + // Received 'None' or an empty slice, so we are expanding the + // header or footer into new rows, which is always valid and cannot + // conflict with existing cells. (Note that we only resize + // 'resolved_cells' after this function is called, so, if this + // header or footer is at the bottom of the table so far, this loop + // will end quite early, regardless of where this cell was placed + // or of its rowspan value.) + break; + } + } + + Ok(new_group_start..new_group_end) +} + +/// Check if a cell's fixed row would conflict with a header or footer. +fn check_for_conflicting_cell_row( + header: Option<&Header>, + footer: Option<&(usize, Span, Footer)>, + cell_y: usize, + rowspan: usize, +) -> HintedStrResult<()> { + if let Some(header) = header { + // TODO: check start (right now zero, always satisfied) + if cell_y < header.end { + bail!( + "cell would conflict with header spanning the same position"; + hint: "try moving the cell or the header" + ); + } + } + + if let Some((footer_end, _, footer)) = footer { + // NOTE: y + rowspan >, not >=, footer.start, to check if the rowspan + // enters the footer. For example, consider a rowspan of 1: if + // `y + 1 = footer.start` holds, that means `y < footer.start`, and it + // only occupies one row (`y`), so the cell is actually not in + // conflict. + if cell_y < *footer_end && cell_y + rowspan > footer.start { + bail!( + "cell would conflict with footer spanning the same position"; + hint: "try reducing the cell's rowspan or moving the footer" + ); + } + } + + Ok(()) +} + /// Given a cell's requested x and y, the vector with the resolved cell /// positions, the `auto_index` counter (determines the position of the next /// `(auto, auto)` cell) and the amount of columns in the grid, returns the /// final index of this cell in the vector of resolved cells. /// -/// The `start_new_row` parameter is used to ensure that, if this cell is -/// fully automatically positioned, it should start a new, empty row. This is -/// useful for headers and footers, which must start at their own rows, without -/// interference from previous cells. +/// The `first_available_row` parameter is used by headers and footers to +/// indicate the first empty row available. Any rows before those should +/// not be picked by cells with `auto` row positioning, since headers and +/// footers occupy entire rows, and may not conflict with cells outside them. #[allow(clippy::too_many_arguments)] fn resolve_cell_position( cell_x: Smart, cell_y: Smart, colspan: usize, rowspan: usize, + header: Option<&Header>, + footer: Option<&(usize, Span, Footer)>, resolved_cells: &[Option], auto_index: &mut usize, - start_new_row: &mut bool, + first_available_row: usize, columns: usize, + in_row_group: bool, ) -> HintedStrResult { // Translates a (x, y) position to the equivalent index in the final cell vector. // Errors if the position would be too large. @@ -1648,29 +2002,24 @@ fn resolve_cell_position( (Smart::Auto, Smart::Auto) => { // Let's find the first available position starting from the // automatic position counter, searching in row-major order. - let mut resolved_index = *auto_index; - if *start_new_row { - resolved_index = - find_next_empty_row(resolved_cells, resolved_index, columns); - - // Next cell won't have to start a new row if we just did that, - // in principle. - *start_new_row = false; - } else { - while let Some(Some(_)) = resolved_cells.get(resolved_index) { - // Skip any non-absent cell positions (`Some(None)`) to - // determine where this cell will be placed. An out of - // bounds position (thus `None`) is also a valid new - // position (only requires expanding the vector). - resolved_index += 1; - } - } + // Note that the counter ignores any cells with fixed positions, + // but automatically-positioned cells will avoid conflicts by + // simply skipping existing cells, headers and footers. + let resolved_index = find_next_available_position::( + header, + footer, + resolved_cells, + columns, + *auto_index, + )?; // Ensure the next cell with automatic position will be // placed after this one (maybe not immediately after). // // The calculation below also affects the position of the upcoming - // automatically-positioned lines. + // automatically-positioned lines, as they are placed below + // (horizontal lines) or to the right (vertical lines) of the cell + // that would be placed at 'auto_index'. *auto_index = if colspan == columns { // The cell occupies all columns, so no cells can be placed // after it until all of its rows have been spanned. @@ -1692,24 +2041,46 @@ fn resolve_cell_position( } if let Smart::Custom(cell_y) = cell_y { // Cell has chosen its exact position. + // + // Ensure it doesn't conflict with an existing header or + // footer (but only if it isn't already in one, otherwise there + // will already be a separate check). + if !in_row_group { + check_for_conflicting_cell_row(header, footer, cell_y, rowspan)?; + } + cell_index(cell_x, cell_y) } else { // Cell has only chosen its column. // Let's find the first row which has that column available. - let mut resolved_y = 0; - while let Some(Some(_)) = - resolved_cells.get(cell_index(cell_x, resolved_y)?) - { - // Try each row until either we reach an absent position - // (`Some(None)`) or an out of bounds position (`None`), - // in which case we'd create a new row to place this cell in. - resolved_y += 1; - } - cell_index(cell_x, resolved_y) + // If in a header or footer, start searching by the first empty + // row / the header or footer's first row (specified through + // 'first_available_row'). Otherwise, start searching at the + // first row. + let initial_index = cell_index(cell_x, first_available_row)?; + + // Try each row until either we reach an absent position at the + // requested column ('Some(None)') or an out of bounds position + // ('None'), in which case we'd create a new row to place this + // cell in. + find_next_available_position::( + header, + footer, + resolved_cells, + columns, + initial_index, + ) } } // Cell has only chosen its row, not its column. (Smart::Auto, Smart::Custom(cell_y)) => { + // Ensure it doesn't conflict with an existing header or + // footer (but only if it isn't already in one, otherwise there + // will already be a separate check). + if !in_row_group { + check_for_conflicting_cell_row(header, footer, cell_y, rowspan)?; + } + // Let's find the first column which has that row available. let first_row_pos = cell_index(0, cell_y)?; let last_row_pos = first_row_pos @@ -1736,14 +2107,73 @@ fn resolve_cell_position( } } -/// Computes the index of the first cell in the next empty row in the grid, -/// starting with the given initial index. +/// Finds the first available position after the initial index in the resolved +/// grid of cells. Skips any non-absent positions (positions which already +/// have cells specified by the user) as well as any headers and footers. +#[inline] +fn find_next_available_position( + header: Option<&Header>, + footer: Option<&(usize, Span, Footer)>, + resolved_cells: &[Option>], + columns: usize, + initial_index: usize, +) -> HintedStrResult { + let mut resolved_index = initial_index; + + loop { + if let Some(Some(_)) = resolved_cells.get(resolved_index) { + // Skip any non-absent cell positions (`Some(None)`) to + // determine where this cell will be placed. An out of + // bounds position (thus `None`) is also a valid new + // position (only requires expanding the vector). + if SKIP_ROWS { + // Skip one row at a time (cell chose its column, so we don't + // change it). + resolved_index = + resolved_index.checked_add(columns).ok_or_else(|| { + HintedString::from(eco_format!("cell position too large")) + })?; + } else { + // Ensure we don't run unnecessary checks in the hot path + // (for fully automatically-positioned cells). Memory usage + // would become impractically large before this overflows. + resolved_index += 1; + } + } else if let Some(header) = + header.filter(|header| resolved_index < header.end * columns) + { + // Skip header (can't place a cell inside it from outside it). + resolved_index = header.end * columns; + + if SKIP_ROWS { + // Ensure the cell's chosen column is kept after the + // header. + resolved_index += initial_index % columns; + } + } else if let Some((footer_end, _, _)) = footer.filter(|(end, _, footer)| { + resolved_index >= footer.start * columns && resolved_index < *end * columns + }) { + // Skip footer, for the same reason. + resolved_index = *footer_end * columns; + + if SKIP_ROWS { + resolved_index += initial_index % columns; + } + } else { + return Ok(resolved_index); + } + } +} + +/// Computes the `y` of the next available empty row, given the auto index as +/// an initial index for search, since we know that there are no empty rows +/// before automatically-positioned cells, as they are placed sequentially. fn find_next_empty_row( resolved_cells: &[Option], - initial_index: usize, + auto_index: usize, columns: usize, ) -> usize { - let mut resolved_index = initial_index.next_multiple_of(columns); + let mut resolved_index = auto_index.next_multiple_of(columns); while resolved_cells .get(resolved_index..resolved_index + columns) .is_some_and(|row| row.iter().any(Option::is_some)) @@ -1752,7 +2182,7 @@ fn find_next_empty_row( resolved_index += columns; } - resolved_index + resolved_index / columns } /// Fully merged rows under the cell of latest auto index indicate rowspans diff --git a/tests/ref/grid-footer-expand.png b/tests/ref/grid-footer-expand.png deleted file mode 100644 index 6b173b0da98c6b54bcf934846505bd3948b575b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 365 zcmV-z0h0cSP)-VsVN4&Z3(G zVez5-0ZP>;@}K=47>ga(yS*3~AOD};KR^~wSTzt9A9FgC`s2$(`dj?z&82_;dtwKu zlDM8ge~bTa?s@Y6hUd)1|L7gZr_Sg7zv1`4XfvHH{;_bL&;J_<|2J3C+2Z!?uRK8( zw;Z6e#rsR=s&BiKI(5zfr0UGW1JmaERWcwJf2&xkGawfCulrv-AQl%d|35H6y~FFg zQ~7`-u9MrY+#gw8n9Hl54T#0p>VE4Fh{d&4i>CY?2#Y7R4^SMxatNs&n8cO+b^d@@ z{LgKGf_k&Ys)4cio8$ixX7NkY0kU{Q+p*Dcu2G9eEgrRaC|C>t-y~FZIbppJ00000 LNkvXXu0mjfBuBJ8 diff --git a/tests/ref/grid-footer-moved-to-bottom-of-rowspans.png b/tests/ref/grid-footer-moved-to-bottom-of-rowspans.png new file mode 100644 index 0000000000000000000000000000000000000000..d8a9c74f82b100a9ec4147e1b738f2517ac8bf6f GIT binary patch literal 1182 zcmV;P1Y!G$P)fzPkT{d;i?p|72VLw6On+h1uEJjEs!N#>W4Uipa>wZ*OlwK|#aA z!`9Z;!otG%`1q=-s%2$m=H}+VzrTlvhyRw4|6W!9dUF4Te*b%OkdTmcbaX;OLhS79 z^78W3)YQ__(tdt^`}_NPdV22e?rCXh|7BeI`ue4%rSZ$qN1Yz zfO!9adH-r*m6es0l$0+oFRQDo)6>)cii7`|lmCr}uCA{CkBFR{od1l5|8r~qT2O?9 zgr=sZ|42RX@bGJEYyY8}|4BdJ-`{t4cfi2Fd3kyNgMI&IUH?={|8Z%jr>FmQZU1az z|Av46i-iA|lK**f|5Hi-j)=RvyMls(xw*OB-QBRTu!e?)H8nNq>FNK9g8!9||9o}- zSWZVrM@vgfWMpL3)zyH2fYH&>OiWB}Zf>5Qp2^9{wzjrPN=nbq&&tZm|B8hFuBoA+ zq5p7b|A>PBtf>EQXScVv|4Baon3DgTm$gwv@;o)6fU8AF;nVFgN^z=?nPDx2g|6*AGfqJj6uP7)e=;-K1 zMn;#Hm;ZKd|8s2ra%yvPbB~XY|A>MAVOalucmIQY|B{T?*Vo+K++bi}*x1-|a&q?e z_WyKk{r&y3v$Ox9od24Y|DT%wotOWWj{l~i{{H^|Ze{<{&;LX@|4>H%@bCXdJpcLm z|DBlsqMaL+IwSx90uxC@K~#9!?c3E;8&MR;@r8Puq);4Mpil}_pziMO?#12R-QC@t zAVGt}f5yNI_wv9D1G_(1&iC!?%V)B8?wRBv0!2~O#*1&$Vf+4#8{48qgX3a#*}i<` z#)K%*U>#j<+_jq<=NAYjc)P;RP62@s_`KTR4+7g-$M`~E`#=AEAh6xve|?AOIuX3N z4J1voy?VxtIoYDYDakWzFIC)Vkws@L7g(VH!G?Mu_^_U-0|wTwsjtI)VxgfG1X~*; zV5Nhc~o%VXTw(IpyOUA33(%u#M^@1!#(nBaB7gBnF91Xe~y z1Vdn>n2@05@s8-5i-*k5G6&;UB;Ev02=g3jXkB&!c6S58F0M-2T(X5nM(5bZ$GCBLNHn;4YL;zk w)i^+BOfbO&Z@tacwD#s|r2j`z6tz`a1W(L`M#F}Q_5c6?07*qoM6N<$f_BrFEdT%j literal 0 HcmV?d00001 diff --git a/tests/ref/grid-footer-top-hlines-with-only-row-pos-cell.png b/tests/ref/grid-footer-top-hlines-with-only-row-pos-cell.png new file mode 100644 index 0000000000000000000000000000000000000000..f78e80c170ee4577cef8cdb8b8c683cc838d475c GIT binary patch literal 385 zcmV-{0e=38P)|If^Ty6nH)^?$kS0Cd?vdaS=- zTmPV%|3NnYpqsy8TZxH@`T6+8S2zxVg|{I5;?PaBz>0k4J&DIC7BFr|!92m}*HVNQsp1OlV+5N>Z=j9CZ-`7o|dg{!(YQf^WSf%zhI5H{;S zEV~@8%F+mtrUC-@TiByj?{|K}*%n#UDpeE;0`Hfj@JDN`T6CcokdTawjPUUAb8~ZqgoHCQGd(>$K|w)IPEJRGv^a95T$adWp3vUj-u3nM$H&Kq zhlkU_z?eBR>DbrHZD-O~TbO4HfoN^kh`B4l~Qd>9^|gkpUQjJrAySPn541gB?#*w}>O8=DVb-?$0aZtvWL z3;*Qk#ET1)>Hb_l-#*JAW_*8c-WSVjVBGJY$Fhi7&zsSnSXl*I6p__Uq6Y~+TW-s5 R_4)t+002ovPDHLkV1hjw8XEur literal 0 HcmV?d00001 diff --git a/tests/ref/grid-header-cell-with-x.png b/tests/ref/grid-header-cell-with-x.png new file mode 100644 index 0000000000000000000000000000000000000000..659826250b2c258fd7bf7dc9ab843fb84954035b GIT binary patch literal 419 zcmV;U0bKrxP)+Gt{NLp`Rf$ZKR?%;Hgt zsc7-JQv+o2j9d`;Z$I5FK9LV3zBd`t-(ruc&42#?x1qnq8UHIb4~WJ2{}=TRh{ZYo zv+n%=u0?l?Z?sN5^#A|N+})!k*QmwRv)GkO1OqS$Bl9?NX<~78>1zxid;^&mI&Ur?ZyB9PeTTvKAztH|7-Eb0kC+& zs(&?~@BX8+#mAfurT$nH(Be;TAAfpt>EHjC*Kgb%ExCrF#aSUMFaY0dWS(t5O)L)6 z3C95J0mwXs+R->3wRqHG3N8LPd&9?edp z7E@rcznl#QV6{NziIvgB;^S)$V}O|lka^3ljn2Z1T0Cm;sKtZ9VgN}LaL>$~n<)SQ N002ovPDHLkV1mqg%XI(% literal 0 HcmV?d00001 diff --git a/tests/ref/grid-header-expand.png b/tests/ref/grid-header-expand.png deleted file mode 100644 index d0fbd72ed23d726d44eef215fabe6c874199d52e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2005 zcmV;`2P*i9P)?)!Uu-tr7rRN#HrqX7>#EyhYZt|-%C_FFS{v(y1&gGJXKhswYEe#+3JQXv zj3^A3TwVkL5flLx5G?n|DJTPS4>Jrf!yMn$kkkpmMqq@sneUrPCjaDfIu&*2B-Z~XrT zcV0qEwl@AMbm?ABDGcp zc9oL|v67m(hE5Cl;pm89?~AAg!&XJOo|P;DLDV6UY(5dkF@FT?%x_&rvnjJfH3%sG z8v6`pQfWOR*x?img6~+|;OH62YWgPvvc*IlXMNoO{5gtQiVnwbFy$$njex4JMg*5k zkBxl>_ikC4xz96qS&pSF_FqkGvr8klYGW`kk1cjol>^vN{M@}E6ZY>Cs+K} zziGmQn2)c(Di zb(^@QPONzO15!J0oR%y0(7ji9GPkdZ(eL-IV+F7Kz+AuIHjeYZQ|fL2wEGenl;Y*W z>gfP*>J)>zT*oK7$N}0^s1#}w1%R>*x{yLAAcsf~<*4EK z-~ba6-9zo4!UrPlk{YLa>;_z~t?K?yQpw~vB_u^`&25}|!hNC8>Jl!=mW|(UxHW2c zHVBxZWkIDVsM4IS53tM!u<@4SoQeRTvEAJn%W_P~aFWc6+?ILXhta~izvvJ!sTcDi zQBc7OwC`hC3n2CT^k+Es)J^-RUkk`|%v%W<0LJ`8_@DB8c2%#@I_~aYcIlj-V$OMh z?a~eXU;1KlSq0|lo@*b?Noo4CEO@Q@!}Dp)X&bVWuZ|K{-)t|22kHI&nVCx6ogyA* zWdqzz?v!QTq_hg?%;?K%&MK-`u@^YtB|T+3bw`f4cb?4xU3#*Y><8VAU#O0(2&Kq{S;DRu-+3(A2)1f|U`U zm;1(%co6$+-s{C+u=Xt-tjWRuWYt$(I;^c*?RDW>z@!15z|nTV6Zne9Q&@oEVAgsU zmtQvTSTD&~qC5F*NqEaP*_3;?WgfN_w|iz}bxbL|)h%=`DPLN=z_I5|sR3bC;&rHr zmXLa(E#X3I{AC4o5fzYeQA<(x{u8;pShxHcvU0Z( z!FYz^sRICaU?8n046b_`wNZ%W3`#q7WN?s(U#cRp%A( z#xUV5rA6MgLj;QWSeInSIVViB_bO(F#pCAQnNO?^}@|K?c9TRFpmNslgy(%u#p-MlAEN%`Vz4!4C_ zH$|DKW6r2kh55m!yM!W{t>Y$dKp_lMUKidC5jK~eQHSP(EI77bYTLL!@|9EHM(s1x1oz|53eV8^u z*^;P_%A$RltyY;~d!-uZ5I*d!x6qo=`;UQ3gTcDDLBKm&Ebl_7n@&TVFJ@}B8Lb=F z;g*7c$6vHhhmRC*8}?BP^7H@JY<-ReeNZ<|{-`I!oz>-;lUn^T@7^2F`^uWd%=$^T zc;M1-aD!Ktu)aSms1D$oTe{lUNA}=w-pPirh^#K}(_+u81h0qEptGF=m&WBX!oNoN zaU1~LPaPfHC*v2(xz4i=0Ct+41D&^L$B$>g%wL3isUB( z3kxg~Hpd0pN+b6A@2n%;&!@h1ZL|&G5dPq^l&yiteU7AOtVbFU@FpkSC=YU)C)@aw nc)2#n)9cHJmG7Kw8*Ka!kjmohl`Q2d00000NkvXXu0mjfvAN$l diff --git a/tests/ref/issue-5359-column-override-stays-inside-footer.png b/tests/ref/issue-5359-column-override-stays-inside-footer.png new file mode 100644 index 0000000000000000000000000000000000000000..8339a4090d6cc71b8eff6890a9f9d37453dd5fdc GIT binary patch literal 674 zcmV;T0$u%yP)JGgviH_kPLjn0MN)2TI4$-6e70 z>7#r6!I#YdY!&|F=x#<{5{GBKPh8?RU|WOV=CnXL)@Xvk zVZQp^pX3dWNIqccSSXSMeKih+W7cE=8*ZY*>Ye(}qEW)Owbf@^^B(}XZ~#~9&^1wfE1?2<*{-}#yy{#8}*&a7nw zgo;~69{9MzQimqm?C86sH28+;fU>o24v4 zETLyi;WXeMRr2sa6^6nTrZA8D4VwpGPtrTzGXMYp07*qo IM6N<$g0lNP@&Et; literal 0 HcmV?d00001 diff --git a/tests/suite/layout/grid/footers.typ b/tests/suite/layout/grid/footers.typ index edbb36fb1..f7f1deb0a 100644 --- a/tests/suite/layout/grid/footers.typ +++ b/tests/suite/layout/grid/footers.typ @@ -83,12 +83,55 @@ grid.cell(y: 1)[c], ) ---- grid-footer-expand --- -// Ensure footer properly expands +--- grid-footer-cell-with-x --- +#grid( + columns: 2, + stroke: black, + inset: 5pt, + grid.cell(x: 1)[a], + // Error: 3-56 footer must end at the last row + grid.footer(grid.cell(x: 0)[b1], grid.cell(x: 0)[b2]), + // This should skip the footer + grid.cell(x: 1)[c] +) + +--- grid-footer-no-expand-with-col-and-row-pos-cell --- #grid( columns: 2, [a], [], [b], [], + fill: (_, y) => if calc.odd(y) { blue } else { red }, + inset: 5pt, + grid.cell(x: 1, y: 3, rowspan: 4)[b], + grid.cell(y: 2, rowspan: 2)[a], + grid.footer(), + // Error: 3-27 cell would conflict with footer spanning the same position + // Hint: 3-27 try reducing the cell's rowspan or moving the footer + grid.cell(x: 1, y: 7)[d], +) + +--- grid-footer-no-expand-with-row-pos-cell --- +#grid( + columns: 2, + [a], [], + [b], [], + fill: (_, y) => if calc.odd(y) { blue } else { red }, + inset: 5pt, + grid.cell(x: 1, y: 3, rowspan: 4)[b], + grid.cell(y: 2, rowspan: 2)[a], + grid.footer(), + // Error: 3-33 cell would conflict with footer spanning the same position + // Hint: 3-33 try reducing the cell's rowspan or moving the footer + grid.cell(y: 6, rowspan: 2)[d], +) + +--- grid-footer-moved-to-bottom-of-rowspans --- +#grid( + columns: 2, + [a], [], + [b], [], + stroke: red, + inset: 5pt, grid.cell(x: 1, y: 3, rowspan: 4)[b], grid.cell(y: 2, rowspan: 2)[a], grid.footer(), @@ -113,13 +156,13 @@ ) --- grid-footer-overlap --- -// Error: 4:3-4:19 footer would conflict with a cell placed before it at column 1 row 0 -// Hint: 4:3-4:19 try reducing that cell's rowspan or moving the footer #grid( columns: 2, grid.header(), - grid.footer([a]), - grid.cell(x: 1, y: 0, rowspan: 2)[a], + grid.footer(grid.cell(y: 2)[a]), + // Error: 3-39 cell would conflict with footer spanning the same position + // Hint: 3-39 try reducing the cell's rowspan or moving the footer + grid.cell(x: 1, y: 1, rowspan: 2)[a], ) --- grid-footer-multiple --- @@ -374,8 +417,8 @@ table.hline(stroke: red), table.vline(stroke: green), [b], + [c] ), - table.cell(x: 1, y: 3)[c] ) --- grid-footer-hline-and-vline-2 --- @@ -385,8 +428,8 @@ #table( columns: 3, inset: 1.5pt, - table.cell(y: 0)[a], table.footer( + table.cell(y: 0)[a], table.hline(stroke: red), table.hline(y: 1, stroke: aqua), table.cell(y: 0)[b], @@ -394,6 +437,38 @@ ) ) +--- grid-footer-top-hlines-with-only-row-pos-cell --- +// Top hlines should attach to the top of the footer. +#set page(margin: 2pt) +#set text(6pt) +#table( + columns: 3, + inset: 2.5pt, + table.footer( + table.hline(stroke: red), + table.vline(stroke: blue), + table.cell(x: 2, y: 2)[a], + table.hline(stroke: 3pt), + table.vline(stroke: 3pt), + ) +) + +--- grid-footer-top-hlines-with-row-and-auto-pos-cell --- +#set page(margin: 2pt) +#set text(6pt) +#table( + columns: 3, + inset: 2.5pt, + table.footer( + table.hline(stroke: red), + table.vline(stroke: blue), + table.cell(x: 2, y: 2)[a], + [b], + table.hline(stroke: 3pt), + table.vline(stroke: 3pt), + ) +) + --- grid-footer-below-rowspans --- // Footer should go below the rowspans. #set page(margin: 2pt) @@ -404,3 +479,71 @@ table.cell(rowspan: 2)[a], table.cell(rowspan: 2)[b], table.footer() ) + +--- grid-footer-row-pos-cell-inside-conflicts-with-row-before --- +#set page(margin: 2pt) +#set text(6pt) +#table( + columns: 3, + inset: 1.5pt, + table.cell(y: 0)[a], + table.footer( + table.hline(stroke: red), + table.hline(y: 1, stroke: aqua), + // Error: 5-24 cell would cause footer to expand to non-empty row 0 + // Hint: 5-24 try moving its cells to available rows + table.cell(y: 0)[b], + [c] + ) +) + +--- grid-footer-auto-pos-cell-inside-conflicts-with-row-after --- +#set page(margin: 2pt) +#set text(6pt) +#table( + columns: 2, + inset: 1.5pt, + table.cell(y: 1)[a], + table.footer( + [b], [c], + // Error: 6-7 cell would cause footer to expand to non-empty row 1 + // Hint: 6-7 try moving its cells to available rows + [d], + ), +) + +--- grid-footer-row-pos-cell-inside-conflicts-with-row-after --- +#set page(margin: 2pt) +#set text(6pt) +#table( + columns: 2, + inset: 1.5pt, + table.cell(y: 2)[a], + table.footer( + [b], [c], + // Error: 5-24 cell would cause footer to expand to non-empty row 2 + // Hint: 5-24 try moving its cells to available rows + table.cell(y: 3)[d], + ), +) + +--- grid-footer-conflicts-with-empty-header --- +#table( + columns: 2, + table.header(), + table.footer( + // Error: 5-24 cell would cause footer to expand to non-empty row 0 + // Hint: 5-24 try moving its cells to available rows + table.cell(y: 0)[a] + ), +) + +--- issue-5359-column-override-stays-inside-footer --- +#table( + columns: 3, + [Outside], + table.footer( + [A], table.cell(x: 1)[B], [C], + table.cell(x: 1)[D], + ), +) diff --git a/tests/suite/layout/grid/headers.typ b/tests/suite/layout/grid/headers.typ index cb2633765..229bce614 100644 --- a/tests/suite/layout/grid/headers.typ +++ b/tests/suite/layout/grid/headers.typ @@ -60,6 +60,16 @@ grid.cell(y: 2)[c] ) +--- grid-header-cell-with-x --- +#grid( + columns: 2, + stroke: black, + inset: 5pt, + grid.header(grid.cell(x: 0)[b1], grid.cell(x: 0)[b2]), + // This should skip the header + grid.cell(x: 1)[c] +) + --- grid-header-last-child --- // When the header is the last grid child, it shouldn't include the gutter row // after it, because there is none. @@ -273,8 +283,7 @@ ) #context count.display() ---- grid-header-expand --- -// Ensure header expands to fit cell placed in it after its declaration +--- grid-header-no-expand-with-col-and-row-pos-cell --- #set page(height: 10em) #table( columns: 2, @@ -282,9 +291,24 @@ [a], [b], [c], ), + // Error: 3-48 cell would conflict with header spanning the same position + // Hint: 3-48 try moving the cell or the header table.cell(x: 1, y: 1, rowspan: 2, lorem(80)) ) +--- grid-header-no-expand-with-row-pos-cell --- +#set page(height: 10em) +#table( + columns: 2, + table.header( + [a], [b], + [c], + ), + // Error: 3-42 cell would conflict with header spanning the same position + // Hint: 3-42 try moving the cell or the header + table.cell(y: 1, rowspan: 2, lorem(80)) +) + --- grid-nested-with-headers --- // Nested table with header should repeat both headers #set page(height: 10em) @@ -368,3 +392,81 @@ [b] ) ) + +--- grid-header-row-pos-cell-inside-conflicts-with-row-before --- +#set page(margin: 2pt) +#set text(6pt) +#table( + columns: 3, + inset: 1.5pt, + table.cell(y: 0)[a], + table.header( + table.hline(stroke: red), + table.hline(y: 1, stroke: aqua), + // Error: 5-24 cell would cause header to expand to non-empty row 0 + // Hint: 5-24 try moving its cells to available rows + table.cell(y: 0)[b], + [c] + ) +) + +--- grid-header-row-pos-cell-inside-conflicts-with-row-before-after-first-empty-row --- +#set page(margin: 2pt) +#set text(6pt) +#table( + columns: 3, + inset: 1.5pt, + // Rows: Occupied, Empty, Occupied, Empty, Empty, ... + // Should not be able to expand header from the second Empty to the second Occupied. + table.cell(y: 0)[a], + table.cell(y: 2)[a], + table.header( + table.hline(stroke: red), + table.hline(y: 3, stroke: aqua), + // Error: 5-24 cell would cause header to expand to non-empty row 2 + // Hint: 5-24 try moving its cells to available rows + table.cell(y: 2)[b], + ) +) + +--- grid-header-auto-pos-cell-inside-conflicts-with-row-after --- +#set page(margin: 2pt) +#set text(6pt) +#table( + columns: 2, + inset: 1.5pt, + table.cell(y: 1)[a], + table.header( + [b], [c], + // Error: 6-7 cell would cause header to expand to non-empty row 1 + // Hint: 6-7 try moving its cells to available rows + [d], + ), +) + +--- grid-header-row-pos-cell-inside-conflicts-with-row-after --- +#set page(margin: 2pt) +#set text(6pt) +#table( + columns: 2, + inset: 1.5pt, + table.cell(y: 2)[a], + table.header( + [b], [c], + // Error: 5-24 cell would cause header to expand to non-empty row 2 + // Hint: 5-24 try moving its cells to available rows + table.cell(y: 3)[d], + ), +) + +--- issue-5359-column-override-stays-inside-header --- +#table( + columns: 3, + [Outside], + // Error: 1:3-4:4 header must start at the first row + // Hint: 1:3-4:4 remove any rows before the header + table.header( + [A], table.cell(x: 1)[B], [C], + table.cell(x: 1)[D], + ), +) From 838a46dbb7124125947bfdafe8ddf97810c5de47 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Thu, 27 Mar 2025 11:59:32 +0100 Subject: [PATCH 078/172] Test all exif rotation types and fix two of them (#6102) --- Cargo.lock | 2 +- Cargo.toml | 2 +- .../src/visualize/image/raster.rs | 4 ++-- tests/ref/image-exif-rotation.png | Bin 0 -> 1392 bytes tests/ref/issue-870-image-rotation.png | Bin 200 -> 0 bytes tests/suite/visualize/image.typ | 19 ++++++++++++------ 6 files changed, 17 insertions(+), 10 deletions(-) create mode 100644 tests/ref/image-exif-rotation.png delete mode 100644 tests/ref/issue-870-image-rotation.png diff --git a/Cargo.lock b/Cargo.lock index d63cec880..630eade2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2803,7 +2803,7 @@ dependencies = [ [[package]] name = "typst-dev-assets" version = "0.13.1" -source = "git+https://github.com/typst/typst-dev-assets?rev=9879589#9879589f4b3247b12c5e694d0d7fa86d4d8a198e" +source = "git+https://github.com/typst/typst-dev-assets?rev=fddbf8b#fddbf8b99506bc370ac0edcd4959add603a7fc92" [[package]] name = "typst-docs" diff --git a/Cargo.toml b/Cargo.toml index a14124d65..a73241832 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" } typst-timing = { path = "crates/typst-timing", version = "0.13.1" } typst-utils = { path = "crates/typst-utils", version = "0.13.1" } typst-assets = { git = "https://github.com/typst/typst-assets", rev = "ab1295f" } -typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "9879589" } +typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "fddbf8b" } arrayvec = "0.7.4" az = "1.2" base64 = "0.22" diff --git a/crates/typst-library/src/visualize/image/raster.rs b/crates/typst-library/src/visualize/image/raster.rs index 0883fe71d..453b94066 100644 --- a/crates/typst-library/src/visualize/image/raster.rs +++ b/crates/typst-library/src/visualize/image/raster.rs @@ -325,12 +325,12 @@ fn apply_rotation(image: &mut DynamicImage, rotation: u32) { ops::flip_horizontal_in_place(image); *image = image.rotate270(); } - 6 => *image = image.rotate90(), + 6 => *image = image.rotate270(), 7 => { ops::flip_horizontal_in_place(image); *image = image.rotate90(); } - 8 => *image = image.rotate270(), + 8 => *image = image.rotate90(), _ => {} } } diff --git a/tests/ref/image-exif-rotation.png b/tests/ref/image-exif-rotation.png new file mode 100644 index 0000000000000000000000000000000000000000..a319a5c5b446afa08c7ec2084f2d1965c1405607 GIT binary patch literal 1392 zcmV-$1&{iPP)^-HEZ{nY`bQwA_ZT&Q_1tbE48? znBbGR>9Wk*f~@4B!NDnV%1DOELW0RegV0)%%~Fl=&)@m$^7Gc@(`A|6in7Q+f!1)K z&sdS!dZ)`vh|N%o-i))pA8XQNnfdAP%S?&RRgdbo&)kHr_v7r~l(_cZ>Cjx0_~q`> zVVCmK;nZrJd)o!2e!q@xo_4L@~%~6cMA#C&2 z*m$P=_4wzk$kS(< z#>U3!v(3lH$L+z^UteF_fU8kPR8QV_!%zH`B}w-S>}l_UUhLpKSJaFEb%LJUl!+ zJQJ6q_!vmMo>&pV#3#f;67ZK@F0KVsOj24Vj_7~|*(w(qIab(R9n(r;Rzj;6SbVOK8Sx%&|7HnlOV zWjWd1%GMn|0%M%KKyKWONP6YLL$?|B{6%^Be1^S#^|Hzc7sQ!b2{)B9blpPa0|YcS5sq8i@?aV=6`)Lqr;<33EN4hrX)nl$OUAV%$*D zW1~N#uwZ*ko3zX8_4~a(pHD6ACT@>&WMo8PwBhtntd^#o0)4%%@Y1e-&9qCC^nlfB z>*?-wdsNKZZ}02uVn6n-&OUp;S5}zE-P_$`vswqjO8aF)Zr(H+g#}u9xdz#`p}*mjW&sTne~kB9P9ae+DHNrqVf z27RL{?YGGp#G#gUkPO-84+*E{TFK*cL3Zea7V$NC#ur8*W#XxdpGX!QI*D1 z4NpHjCGj-Xoisj=;B$-ar18lQpAe%g9XB(sgE&yyK|1KaRCT}dSH{1&aA_{BQM!Hj y8)R5dtes#!&iD?Qt|#`M|9*johlht};_?GLgY!0Dq{rU?0000F!$v^BJ8WOKO3hZ{* zmD@UlOU(EH<6qshG)CrK(Pu0i?9!4vqHSu`1TNf85^K{H5c{he$G@cR??2!C?wOLe z*2(Vr`~Uudl;sEh?`J=dq*m7U;eS2bx#P-eeUTDojG7Dfe);~f>%aZ~fASpN;f%@^ zSCVc;=e*C@|NsB|i_6ZhIJ#J@KguS{P>+G(z@>Nge%mrGft={+>gTe~DWM4f<3&-k diff --git a/tests/suite/visualize/image.typ b/tests/suite/visualize/image.typ index 7ce0c8c0a..9a77870af 100644 --- a/tests/suite/visualize/image.typ +++ b/tests/suite/visualize/image.typ @@ -247,12 +247,6 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B format: "rgba8", ) ---- issue-870-image-rotation --- -// Ensure that EXIF rotation is applied. -// https://github.com/image-rs/image/issues/1045 -// File is from https://magnushoff.com/articles/jpeg-orientation/ -#image("/assets/images/f2t.jpg", width: 10pt) - --- issue-measure-image --- // Test that image measurement doesn't turn `inf / some-value` into 0pt. #context { @@ -267,3 +261,16 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B --- issue-3733-dpi-svg --- #set page(width: 200pt, height: 200pt, margin: 0pt) #image("/assets/images/relative.svg") + +--- image-exif-rotation --- +#let data = read("/assets/images/f2t.jpg", encoding: none) + +#let rotations = range(1, 9) +#let rotated(v) = image(data.slice(0, 49) + bytes((v,)) + data.slice(50), width: 10pt) + +#set page(width: auto) +#table( + columns: rotations.len(), + ..rotations.map(v => raw(str(v), lang: "typc")), + ..rotations.map(rotated) +) From b7a4382a73e495cf56350c1ba4f216d51b1864c7 Mon Sep 17 00:00:00 2001 From: Philipp Niedermayer Date: Fri, 28 Mar 2025 16:28:03 +0100 Subject: [PATCH 079/172] Fix typo (#6104) --- crates/typst-library/src/visualize/shape.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-library/src/visualize/shape.rs b/crates/typst-library/src/visualize/shape.rs index 439b4cd98..ff05be2be 100644 --- a/crates/typst-library/src/visualize/shape.rs +++ b/crates/typst-library/src/visualize/shape.rs @@ -106,7 +106,7 @@ pub struct RectElem { pub radius: Corners>>, /// How much to pad the rectangle's content. - /// See the [box's documentation]($box.outset) for more details. + /// See the [box's documentation]($box.inset) for more details. #[resolve] #[fold] #[default(Sides::splat(Some(Abs::pt(5.0).into())))] From 20ee446ebab2fbb23246026301e26b82647369a2 Mon Sep 17 00:00:00 2001 From: frozolotl <44589151+frozolotl@users.noreply.github.com> Date: Fri, 28 Mar 2025 16:30:30 +0100 Subject: [PATCH 080/172] Fix descriptions of color maps (#6096) --- crates/typst-library/src/visualize/color.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/visualize/color.rs b/crates/typst-library/src/visualize/color.rs index 20b0f5719..24d8305cd 100644 --- a/crates/typst-library/src/visualize/color.rs +++ b/crates/typst-library/src/visualize/color.rs @@ -148,11 +148,11 @@ static TO_SRGB: LazyLock = LazyLock::new(|| { /// | `magma` | A black to purple to yellow color map. | /// | `plasma` | A purple to pink to yellow color map. | /// | `rocket` | A black to red to white color map. | -/// | `mako` | A black to teal to yellow color map. | +/// | `mako` | A black to teal to white color map. | /// | `vlag` | A light blue to white to red color map. | -/// | `icefire` | A light teal to black to yellow color map. | +/// | `icefire` | A light teal to black to orange color map. | /// | `flare` | A orange to purple color map that is perceptually uniform. | -/// | `crest` | A blue to white to red color map. | +/// | `crest` | A light green to blue color map. | /// /// Some popular presets are not included because they are not available under a /// free licence. Others, like From efdb75558f20543af39f75fb88b3bae59b20e2e8 Mon Sep 17 00:00:00 2001 From: Matt Fellenz Date: Fri, 28 Mar 2025 18:33:16 +0100 Subject: [PATCH 081/172] IDE: complete jump-to-cursor impl (#6037) --- crates/typst-ide/src/jump.rs | 152 ++++++++++++++++++-- crates/typst-library/src/visualize/curve.rs | 64 +++++++++ 2 files changed, 206 insertions(+), 10 deletions(-) diff --git a/crates/typst-ide/src/jump.rs b/crates/typst-ide/src/jump.rs index 428335426..b29bc4a48 100644 --- a/crates/typst-ide/src/jump.rs +++ b/crates/typst-ide/src/jump.rs @@ -3,7 +3,7 @@ use std::num::NonZeroUsize; use typst::layout::{Frame, FrameItem, PagedDocument, Point, Position, Size}; use typst::model::{Destination, Url}; use typst::syntax::{FileId, LinkedNode, Side, Source, Span, SyntaxKind}; -use typst::visualize::Geometry; +use typst::visualize::{Curve, CurveItem, FillRule, Geometry}; use typst::WorldExt; use crate::IdeWorld; @@ -53,10 +53,20 @@ pub fn jump_from_click( for (mut pos, item) in frame.items().rev() { match item { FrameItem::Group(group) => { - // TODO: Handle transformation. - if let Some(span) = - jump_from_click(world, document, &group.frame, click - pos) - { + let pos = click - pos; + if let Some(clip) = &group.clip { + if !clip.contains(FillRule::NonZero, pos) { + continue; + } + } + // Realistic transforms should always be invertible. + // An example of one that isn't is a scale of 0, which would + // not be clickable anyway. + let Some(inv_transform) = group.transform.invert() else { + continue; + }; + let pos = pos.transform_inf(inv_transform); + if let Some(span) = jump_from_click(world, document, &group.frame, pos) { return Some(span); } } @@ -94,9 +104,32 @@ pub fn jump_from_click( } FrameItem::Shape(shape, span) => { - let Geometry::Rect(size) = shape.geometry else { continue }; - if is_in_rect(pos, size, click) { - return Jump::from_span(world, *span); + if shape.fill.is_some() { + let within = match &shape.geometry { + Geometry::Line(..) => false, + Geometry::Rect(size) => is_in_rect(pos, *size, click), + Geometry::Curve(curve) => { + curve.contains(shape.fill_rule, click - pos) + } + }; + if within { + return Jump::from_span(world, *span); + } + } + + if let Some(stroke) = &shape.stroke { + let within = !stroke.thickness.approx_empty() && { + // This curve is rooted at (0, 0), not `pos`. + let base_curve = match &shape.geometry { + Geometry::Line(to) => &Curve(vec![CurveItem::Line(*to)]), + Geometry::Rect(size) => &Curve::rect(*size), + Geometry::Curve(curve) => curve, + }; + base_curve.stroke_contains(stroke, click - pos) + }; + if within { + return Jump::from_span(world, *span); + } } } @@ -146,9 +179,8 @@ pub fn jump_from_cursor( fn find_in_frame(frame: &Frame, span: Span) -> Option { for (mut pos, item) in frame.items() { if let FrameItem::Group(group) = item { - // TODO: Handle transformation. if let Some(point) = find_in_frame(&group.frame, span) { - return Some(point + pos); + return Some(pos + point.transform(group.transform)); } } @@ -269,6 +301,97 @@ mod tests { test_click("$a + b$", point(28.0, 14.0), cursor(5)); } + #[test] + fn test_jump_from_click_transform_clip() { + let margin = point(10.0, 10.0); + test_click( + "#rect(width: 20pt, height: 20pt, fill: black)", + point(10.0, 10.0) + margin, + cursor(1), + ); + test_click( + "#rect(width: 60pt, height: 10pt, fill: black)", + point(5.0, 30.0) + margin, + None, + ); + test_click( + "#rotate(90deg, origin: bottom + left, rect(width: 60pt, height: 10pt, fill: black))", + point(5.0, 30.0) + margin, + cursor(38), + ); + test_click( + "#scale(x: 300%, y: 300%, origin: top + left, rect(width: 10pt, height: 10pt, fill: black))", + point(20.0, 20.0) + margin, + cursor(45), + ); + test_click( + "#box(width: 10pt, height: 10pt, clip: true, scale(x: 300%, y: 300%, \ + origin: top + left, rect(width: 10pt, height: 10pt, fill: black)))", + point(20.0, 20.0) + margin, + None, + ); + test_click( + "#box(width: 10pt, height: 10pt, clip: false, rect(width: 30pt, height: 30pt, fill: black))", + point(20.0, 20.0) + margin, + cursor(45), + ); + test_click( + "#box(width: 10pt, height: 10pt, clip: true, rect(width: 30pt, height: 30pt, fill: black))", + point(20.0, 20.0) + margin, + None, + ); + test_click( + "#rotate(90deg, origin: bottom + left)[hello world]", + point(5.0, 15.0) + margin, + cursor(40), + ); + } + + #[test] + fn test_jump_from_click_shapes() { + let margin = point(10.0, 10.0); + + test_click( + "#rect(width: 30pt, height: 30pt, fill: black)", + point(15.0, 15.0) + margin, + cursor(1), + ); + + let circle = "#circle(width: 30pt, height: 30pt, fill: black)"; + test_click(circle, point(15.0, 15.0) + margin, cursor(1)); + test_click(circle, point(1.0, 1.0) + margin, None); + + let bowtie = + "#polygon(fill: black, (0pt, 0pt), (20pt, 20pt), (20pt, 0pt), (0pt, 20pt))"; + test_click(bowtie, point(1.0, 2.0) + margin, cursor(1)); + test_click(bowtie, point(2.0, 1.0) + margin, None); + test_click(bowtie, point(19.0, 10.0) + margin, cursor(1)); + + let evenodd = r#"#polygon(fill: black, fill-rule: "even-odd", + (0pt, 10pt), (30pt, 10pt), (30pt, 20pt), (20pt, 20pt), + (20pt, 0pt), (10pt, 0pt), (10pt, 30pt), (20pt, 30pt), + (20pt, 20pt), (0pt, 20pt))"#; + test_click(evenodd, point(15.0, 15.0) + margin, None); + test_click(evenodd, point(5.0, 15.0) + margin, cursor(1)); + test_click(evenodd, point(15.0, 5.0) + margin, cursor(1)); + } + + #[test] + fn test_jump_from_click_shapes_stroke() { + let margin = point(10.0, 10.0); + + let rect = + "#place(dx: 10pt, dy: 10pt, rect(width: 10pt, height: 10pt, stroke: 5pt))"; + test_click(rect, point(15.0, 15.0) + margin, None); + test_click(rect, point(10.0, 15.0) + margin, cursor(27)); + + test_click( + "#line(angle: 45deg, length: 10pt, stroke: 2pt)", + point(2.0, 2.0) + margin, + cursor(1), + ); + } + #[test] fn test_jump_from_cursor() { let s = "*Hello* #box[ABC] World"; @@ -281,6 +404,15 @@ mod tests { test_cursor("$a + b$", -3, pos(1, 27.51, 16.83)); } + #[test] + fn test_jump_from_cursor_transform() { + test_cursor( + r#"#rotate(90deg, origin: bottom + left, [hello world])"#, + -5, + pos(1, 10.0, 16.58), + ); + } + #[test] fn test_backlink() { let s = "#footnote[Hi]"; diff --git a/crates/typst-library/src/visualize/curve.rs b/crates/typst-library/src/visualize/curve.rs index fb5151e8f..50944a516 100644 --- a/crates/typst-library/src/visualize/curve.rs +++ b/crates/typst-library/src/visualize/curve.rs @@ -10,6 +10,8 @@ use crate::foundations::{ use crate::layout::{Abs, Axes, BlockElem, Length, Point, Rel, Size}; use crate::visualize::{FillRule, Paint, Stroke}; +use super::FixedStroke; + /// A curve consisting of movements, lines, and Bézier segments. /// /// At any point in time, there is a conceptual pen or cursor. @@ -530,3 +532,65 @@ impl Curve { Size::new(max_x - min_x, max_y - min_y) } } + +impl Curve { + fn to_kurbo(&self) -> impl Iterator + '_ { + use kurbo::PathEl; + + self.0.iter().map(|item| match *item { + CurveItem::Move(point) => PathEl::MoveTo(point_to_kurbo(point)), + CurveItem::Line(point) => PathEl::LineTo(point_to_kurbo(point)), + CurveItem::Cubic(point, point1, point2) => PathEl::CurveTo( + point_to_kurbo(point), + point_to_kurbo(point1), + point_to_kurbo(point2), + ), + CurveItem::Close => PathEl::ClosePath, + }) + } + + /// When this curve is interpreted as a clip mask, would it contain `point`? + pub fn contains(&self, fill_rule: FillRule, needle: Point) -> bool { + let kurbo = kurbo::BezPath::from_vec(self.to_kurbo().collect()); + let windings = kurbo::Shape::winding(&kurbo, point_to_kurbo(needle)); + match fill_rule { + FillRule::NonZero => windings != 0, + FillRule::EvenOdd => windings % 2 != 0, + } + } + + /// When this curve is stroked with `stroke`, would the stroke contain + /// `point`? + pub fn stroke_contains(&self, stroke: &FixedStroke, needle: Point) -> bool { + let width = stroke.thickness.to_raw(); + let cap = match stroke.cap { + super::LineCap::Butt => kurbo::Cap::Butt, + super::LineCap::Round => kurbo::Cap::Round, + super::LineCap::Square => kurbo::Cap::Square, + }; + let join = match stroke.join { + super::LineJoin::Miter => kurbo::Join::Miter, + super::LineJoin::Round => kurbo::Join::Round, + super::LineJoin::Bevel => kurbo::Join::Bevel, + }; + let miter_limit = stroke.miter_limit.get(); + let mut style = kurbo::Stroke::new(width) + .with_caps(cap) + .with_join(join) + .with_miter_limit(miter_limit); + if let Some(dash) = &stroke.dash { + style = style.with_dashes( + dash.phase.to_raw(), + dash.array.iter().copied().map(Abs::to_raw), + ); + } + let opts = kurbo::StrokeOpts::default(); + let tolerance = 0.01; + let expanded = kurbo::stroke(self.to_kurbo(), &style, &opts, tolerance); + kurbo::Shape::contains(&expanded, point_to_kurbo(needle)) + } +} + +fn point_to_kurbo(point: Point) -> kurbo::Point { + kurbo::Point::new(point.x.to_raw(), point.y.to_raw()) +} From 758ee78ef57ebbaadacc50817620a540bcf8beeb Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin <35292584+Myriad-Dreamin@users.noreply.github.com> Date: Mon, 31 Mar 2025 16:08:55 +0800 Subject: [PATCH 082/172] Make `World::font` implementations safe (#6117) --- crates/typst-cli/src/world.rs | 4 +++- crates/typst-ide/src/tests.rs | 2 +- docs/src/html.rs | 2 +- tests/src/world.rs | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs index 12e80d273..2da03d4d5 100644 --- a/crates/typst-cli/src/world.rs +++ b/crates/typst-cli/src/world.rs @@ -210,7 +210,9 @@ impl World for SystemWorld { } fn font(&self, index: usize) -> Option { - self.fonts[index].get() + // comemo's validation may invoke this function with an invalid index. This is + // impossible in typst-cli but possible if a custom tool mutates the fonts. + self.fonts.get(index)?.get() } fn today(&self, offset: Option) -> Option { diff --git a/crates/typst-ide/src/tests.rs b/crates/typst-ide/src/tests.rs index 6678ab841..c6d733ca9 100644 --- a/crates/typst-ide/src/tests.rs +++ b/crates/typst-ide/src/tests.rs @@ -97,7 +97,7 @@ impl World for TestWorld { } fn font(&self, index: usize) -> Option { - Some(self.base.fonts[index].clone()) + self.base.fonts.get(index).cloned() } fn today(&self, _: Option) -> Option { diff --git a/docs/src/html.rs b/docs/src/html.rs index 9077d5c47..9c02f08e9 100644 --- a/docs/src/html.rs +++ b/docs/src/html.rs @@ -498,7 +498,7 @@ impl World for DocWorld { } fn font(&self, index: usize) -> Option { - Some(FONTS.1[index].clone()) + FONTS.1.get(index).cloned() } fn today(&self, _: Option) -> Option { diff --git a/tests/src/world.rs b/tests/src/world.rs index 9e0e91ad7..fe2bd45ea 100644 --- a/tests/src/world.rs +++ b/tests/src/world.rs @@ -67,7 +67,7 @@ impl World for TestWorld { } fn font(&self, index: usize) -> Option { - Some(self.base.fonts[index].clone()) + self.base.fonts.get(index).cloned() } fn today(&self, _: Option) -> Option { From 326bec1f0d0fc65fb26ae4c797487d82d2b18b81 Mon Sep 17 00:00:00 2001 From: Astra3 Date: Mon, 31 Mar 2025 10:16:47 +0200 Subject: [PATCH 083/172] Correcting Czech translation in `typst-library` (#6101) --- crates/typst-library/translations/cs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-library/translations/cs.txt b/crates/typst-library/translations/cs.txt index d4688ffee..e21ca3520 100644 --- a/crates/typst-library/translations/cs.txt +++ b/crates/typst-library/translations/cs.txt @@ -4,5 +4,5 @@ equation = Rovnice bibliography = Bibliografie heading = Kapitola outline = Obsah -raw = Seznam +raw = Výpis page = strana \ No newline at end of file From e60d3021a782c5977cf7de726682e19ae89abeb3 Mon Sep 17 00:00:00 2001 From: +merlan #flirora Date: Mon, 31 Mar 2025 04:17:37 -0400 Subject: [PATCH 084/172] Add env setting for ignore_system_fonts (#6092) --- crates/typst-cli/src/args.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs index d6855d100..76f647276 100644 --- a/crates/typst-cli/src/args.rs +++ b/crates/typst-cli/src/args.rs @@ -361,7 +361,7 @@ pub struct FontArgs { /// Ensures system fonts won't be searched, unless explicitly included via /// `--font-path`. - #[arg(long)] + #[arg(long, env = "TYPST_IGNORE_SYSTEM_FONTS")] pub ignore_system_fonts: bool, } From 1082181a6f789b73fbc64c4ff5bc1401ad081e76 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Mon, 31 Mar 2025 11:01:01 +0200 Subject: [PATCH 085/172] Improve french smartquotes (#5976) --- crates/typst-library/src/text/smartquote.rs | 2 +- tests/ref/smartquote-disabled-temporarily.png | Bin 2781 -> 2782 bytes tests/ref/smartquote-fr.png | Bin 2344 -> 2334 bytes tests/ref/smartquote-with-embedding-chars.png | Bin 573 -> 568 bytes tests/suite/text/smartquote.typ | 2 +- 5 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/typst-library/src/text/smartquote.rs b/crates/typst-library/src/text/smartquote.rs index f457a6371..4dda689df 100644 --- a/crates/typst-library/src/text/smartquote.rs +++ b/crates/typst-library/src/text/smartquote.rs @@ -238,7 +238,7 @@ impl<'s> SmartQuotes<'s> { "cs" | "de" | "et" | "is" | "lt" | "lv" | "sk" | "sl" => low_high, "da" => ("‘", "’", "“", "”"), "fr" | "ru" if alternative => default, - "fr" => ("‹\u{00A0}", "\u{00A0}›", "«\u{00A0}", "\u{00A0}»"), + "fr" => ("“", "”", "«\u{202F}", "\u{202F}»"), "fi" | "sv" if alternative => ("’", "’", "»", "»"), "bs" | "fi" | "sv" => ("’", "’", "”", "”"), "it" if alternative => default, diff --git a/tests/ref/smartquote-disabled-temporarily.png b/tests/ref/smartquote-disabled-temporarily.png index 4c565c01ce1d91bfbd8b443f410c56e0b9b55360..f4d08c4d12fd5a8ecbc19d2d3d5bb252b2011386 100644 GIT binary patch delta 2774 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 delta 2773 zcmV;`3M%#972Oq(B!8qyL_t(|+U?i-SCjVv$MO9U`(bD2tlR1~U1vL6TeG&(POG)f z&Vo9{BGu9=NKsKh2n0mA36YCj6RrUg?o`wW1GC_~ixm)5E+R;{)qvbWNJ32BYsI6- zW7T+Tx1GFy&G($wd7kIw^PJ~r{6ziC4%=Zn{Qm@N&zu?pFn_9g`Jt@GTY*!}+^z30 zkL~Ke9t)?{xlgsGioH!MYknBsDg{K*-0k`d;9esZ?l#1oPQU+HxTp5un0r4eL@Iu; ztPG$`%)JD_y%tQsos_$|)8qo1o^kfF4iKg7+zdJLuYu9TkV_F+6C3L&$@?gSgSn1k zYKjvrd!&0@bbkn}+X9g!0_BCFU9-~qR29LT+7u@hI#rV`ub@>TvJIx<&wCKX;xu@` zTm1{5EOel+Du{rijRB-&bamDx_G5x{q_m!h{%H^ZO(}$)_{d~M`q~^H@_AVCg=B1F?$3smRJ_EcyRo$xE<$rP!*(?Z={FcetYp%qb+dy-n_OU zVag4^EPr#%o)bW2TBg++8<&?sR!DJzp+HnF9zW!#uto)q28W;UQe$ujK%_WLS;$y? zwk%HNRmI5GT61*RN$UNTL9H)lKu%x!X_l)h+`iTS)9~DyQ#M_GJo@B8cRd=k3qql1GT}4MU&a-nsYu%&*Ny5#ocP(SdW_=1fUy05l%n^p3C5R6L+T~b(-aY26RIFk~QRht6H#V5+(?2g^_JU*J++}@-=8ZVj zZ|$Cj5x}1=G7)9XKA((o>kh0NB@ z>A|VhFqdyWW8aw(0L>D31y)xx#48LtFn_xVO_eX1@st6a&H<1|F;mNsO|Wu*qCvSx z5@TiG&LZL@X+hPWUOl?8?hDD+uI=fY`h&jO5COR73*6sZ;aE0 zZ`B9-nqmRy1c*CAjk|QjCK%xG9v)V|KZNSAsg~;CNdRny*T3J(Lz;5TEU3Y@Mt^I} z&vP=Z_Lszu2*@w>EJXA4{47(Hsn|M@zcS>Jgf_z*id<^@=0JzenM+zscCmZ z>vq#*eyj$r_*#`o=7>{!qoB6vG*IuDTFBI)811;IAw+`dP|V<1P-93sC>56gFzTN8 z8P`N#Z$7q_(64!8N|AM`3wVv8g@43-Ip)}ZamT>SNvLk77y@R>t^08pH ziS+YtRmODx0RU^D{z>8Sm9ArFw-J4S`DX1>4+GqO2_TWN<$Rb(b@Ue0dr};x&q;Mg zq{3-nB0Jdyp_=8|FOyWY0C089lfp-d&1qVPP=R2w8le(+f28*vk}~i(U4Q0*!Fn4G zstY`MZ8iQ?e5?sL)4mqp8kY9OMxv(=AVl+|a7jYUxOIDUi2sFU#Tk<5Dd6zVY^OR; z@8NYZM$h0*&U)fYBI^sLA}5?HYMoApte6|&(C5tWnc0R46Qa@pbkJy>r5 zOuFX>%Ux5$T6ka|t=n@+sm%-qX2i zRDt}XgVzDx!t&CTbq?E)m3~RQ6v$1O{&8*(w=Bk|hI9XXfYdbIS4mOCf28@$r^BHv zE+T1l-QvvVxBdzEihqXHUnQsqoP=h#VBd{KESolHmgLmVyIAFz+<0ieeYxzg9k#=E z_{V^|Cb2b{5`fxz?pAPs2ht@pKOPogcq;q|(EG_FVeQPMDQ@^>1zTfXQ-A-O7ydcgi2cQ2TIILP zW5Ni3_#wL<{}Dhbv@$JwRlxKj5*h_Uy{w4=<6DB)oiOmZf+Fr?NfwT@Cm5 zUr5;-ibuhW?3_K}eG2cAODot@!hEaOzSM<4yp+(~!cBRzO!MHCu}#NWQ>PYM-cq

pWx_=^1>sZS*2j(%ph*bl}LI}@eeaC>F?^qki9L0D{*gp47}y4)vHtf zg~{dG$(01=^6Jpu9TOLTg#CJ#w&M#%>u@R1FSfeZXn#K$E%)H{6F06wd$DdEP#V}; z^*LFA<8NQ772?=X`n#U65gqI(z`1s0e$%7iD;e@GsLQxQsZ^6LZL13>~CYKez#A_J=mV{9e-@um-E_fINs+lEh!OgPX%XZ5HGgQ zFxKq=+FfCWq1}zJgCh<%uL6p_=3f~L#&4g}E~7;J9h_OjmfLWrWsdbjna@{G1^X9K z?S+8LZg*e-y(@`4i1N)U!1QkQ&VtP8Q$x!r`ySqU@b0aC+cX1<^{HUhN@Lg&{cP&i z0Dsp%>6v(y_9X-=)R$U(0CxI~%xa!KWqRGb)6Kl_TAthZhiSk+TH8EXT4{Z{Tg{o2 zN{&Rd7Dx2~QMHwk?G=%YF$tReieUgv6UsZM_*cvs`Nha$a+0g6fr{+xDZUMp=ODE}0YzV0ea^Leu-MqNs`}MV;6=eTbBhqbmh%|m zOW&eQ%6ocwRBK|jTSTyc}<57;Nu4%=Zn b{6PN!WoyUfJv0%b00000NkvXXu0mjf8my2m diff --git a/tests/ref/smartquote-fr.png b/tests/ref/smartquote-fr.png index e28184226ac174455d34de80f4ab329c305b7bfa..6b7de7abeecd29b66aaad7467f6a787d7fb7d7a7 100644 GIT binary patch delta 2323 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 t&81yhJU5m3P_CFiB(~{ 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 diff --git a/tests/suite/text/smartquote.typ b/tests/suite/text/smartquote.typ index 4940d11b2..f2af93ceb 100644 --- a/tests/suite/text/smartquote.typ +++ b/tests/suite/text/smartquote.typ @@ -99,7 +99,7 @@ He's told some books contain questionable "example text". --- smartquote-disabled-temporarily --- // Test changing properties within text. -"She suddenly started speaking french: #text(lang: "fr")['Je suis une banane.']" Roman told me. +"She suddenly started speaking french: #text(lang: "fr", region: "CH")['Je suis une banane.']" Roman told me. Some people's thought on this would be #[#set smartquote(enabled: false); "strange."] From a64af130dc84c84442d59f322b705bded28201de Mon Sep 17 00:00:00 2001 From: +merlan #flirora Date: Mon, 31 Mar 2025 05:06:18 -0400 Subject: [PATCH 086/172] Add default parameter for array.{first, last} (#5970) --- crates/typst-library/src/foundations/array.rs | 24 ++++++++++++++----- tests/suite/foundations/array.typ | 4 ++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/crates/typst-library/src/foundations/array.rs b/crates/typst-library/src/foundations/array.rs index e81b9e645..b647473ab 100644 --- a/crates/typst-library/src/foundations/array.rs +++ b/crates/typst-library/src/foundations/array.rs @@ -172,17 +172,29 @@ impl Array { } /// Returns the first item in the array. May be used on the left-hand side - /// of an assignment. Fails with an error if the array is empty. + /// an assignment. Returns the default value if the array is empty + /// or fails with an error is no default value was specified. #[func] - pub fn first(&self) -> StrResult { - self.0.first().cloned().ok_or_else(array_is_empty) + pub fn first( + &self, + /// A default value to return if the array is empty. + #[named] + default: Option, + ) -> StrResult { + self.0.first().cloned().or(default).ok_or_else(array_is_empty) } /// Returns the last item in the array. May be used on the left-hand side of - /// an assignment. Fails with an error if the array is empty. + /// an assignment. Returns the default value if the array is empty + /// or fails with an error is no default value was specified. #[func] - pub fn last(&self) -> StrResult { - self.0.last().cloned().ok_or_else(array_is_empty) + pub fn last( + &self, + /// A default value to return if the array is empty. + #[named] + default: Option, + ) -> StrResult { + self.0.last().cloned().or(default).ok_or_else(array_is_empty) } /// Returns the item at the specified index in the array. May be used on the diff --git a/tests/suite/foundations/array.typ b/tests/suite/foundations/array.typ index 6228f471b..61b5decb3 100644 --- a/tests/suite/foundations/array.typ +++ b/tests/suite/foundations/array.typ @@ -179,6 +179,10 @@ #test((2,).last(), 2) #test((1, 2, 3).first(), 1) #test((1, 2, 3).last(), 3) +#test((1, 2).first(default: 99), 1) +#test(().first(default: 99), 99) +#test((1, 2).last(default: 99), 2) +#test(().last(default: 99), 99) --- array-first-empty --- // Error: 2-12 array is empty From 4f0fbfb7e003f6ae88c1b210fdb7b38f795fc9e4 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 31 Mar 2025 09:17:49 +0000 Subject: [PATCH 087/172] Add dotless parameter to `math.accent` (#5939) Co-authored-by: Laurenz --- crates/typst-layout/src/math/accent.rs | 6 +++-- crates/typst-library/src/math/accent.rs | 26 +++++++++++++++++++-- tests/ref/math-accent-dotless-disabled.png | Bin 0 -> 311 bytes tests/ref/math-accent-dotless-set-rule.png | Bin 0 -> 147 bytes tests/suite/math/accent.typ | 8 +++++++ 5 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 tests/ref/math-accent-dotless-disabled.png create mode 100644 tests/ref/math-accent-dotless-set-rule.png diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs index f2dfa2c45..73d821019 100644 --- a/crates/typst-layout/src/math/accent.rs +++ b/crates/typst-layout/src/math/accent.rs @@ -19,8 +19,10 @@ pub fn layout_accent( let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?; // Try to replace a glyph with its dotless variant. - if let MathFragment::Glyph(glyph) = &mut base { - glyph.make_dotless_form(ctx); + if elem.dotless(styles) { + if let MathFragment::Glyph(glyph) = &mut base { + glyph.make_dotless_form(ctx); + } } // Preserve class to preserve automatic spacing. diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs index b162c52b1..e62b63872 100644 --- a/crates/typst-library/src/math/accent.rs +++ b/crates/typst-library/src/math/accent.rs @@ -13,8 +13,8 @@ use crate::math::Mathy; /// ``` #[elem(Mathy)] pub struct AccentElem { - /// The base to which the accent is applied. - /// May consist of multiple letters. + /// The base to which the accent is applied. May consist of multiple + /// letters. /// /// ```example /// $arrow(A B C)$ @@ -51,9 +51,24 @@ pub struct AccentElem { pub accent: Accent, /// The size of the accent, relative to the width of the base. + /// + /// ```example + /// $dash(A, size: #150%)$ + /// ``` #[resolve] #[default(Rel::one())] pub size: Rel, + + /// Whether to remove the dot on top of lowercase i and j when adding a top + /// accent. + /// + /// This enables the `dtls` OpenType feature. + /// + /// ```example + /// $hat(dotless: #false, i)$ + /// ``` + #[default(true)] + pub dotless: bool, } /// An accent character. @@ -103,11 +118,18 @@ macro_rules! accents { /// The size of the accent, relative to the width of the base. #[named] size: Option>, + /// Whether to remove the dot on top of lowercase i and j when + /// adding a top accent. + #[named] + dotless: Option, ) -> Content { let mut accent = AccentElem::new(base, Accent::new($primary)); if let Some(size) = size { accent = accent.with_size(size); } + if let Some(dotless) = dotless { + accent = accent.with_dotless(dotless); + } accent.pack() } )+ diff --git a/tests/ref/math-accent-dotless-disabled.png b/tests/ref/math-accent-dotless-disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..d75ec4580253f4b791384ba7e98bf209eab338c6 GIT binary patch literal 311 zcmV-70m%M|P)w)T5`r&Y`IJ`TyHLT3Y-rdKZLobSId4 z-n?iFMBq;YO@n&RpTm$K{`C_~z5IXo4^-+iO)dUB?-`iB2#e6G3nBF5`HyI7@rvb3 z!1T(HZ(#cNiR{N<#{B)gG`09&$Qwvp2SE6L|GPmMt$S%|@rU3O|6hPSxa1g+1_^}z ze+5p?KHq6-@tdMO{{yf7|G%c^;eX3_K!Ht@{x^X_|F+?005R4eF=Tc4^998002ov JPDHLkV1kDTnQ{OC literal 0 HcmV?d00001 diff --git a/tests/ref/math-accent-dotless-set-rule.png b/tests/ref/math-accent-dotless-set-rule.png new file mode 100644 index 0000000000000000000000000000000000000000..ae5ef017aaedc711d30182cd57d05de08e9f9397 GIT binary patch literal 147 zcmeAS@N?(olHy`uVBq!ia0vp^6+kS@0VEh)%)UPdQc<2Rjv*Ddl7HAcG$dYm6xi*q zE4Q^mLFeDx!h{5!dmrnL|8C@bb+w*NqtBl|Kt{gaz(6eCR>a;wY=5+2azm5Vk88gh oH(J#EV)*&v_4zopr0MHjU3IG5A literal 0 HcmV?d00001 diff --git a/tests/suite/math/accent.typ b/tests/suite/math/accent.typ index 5be4f576f..ab0078a5f 100644 --- a/tests/suite/math/accent.typ +++ b/tests/suite/math/accent.typ @@ -42,3 +42,11 @@ $tilde(U, size: #1.1em), x^tilde(U, size: #1.1em), sscript(tilde(U, size: #1.1em macron(bb(#c)), dot(cal(#c)), diaer(upright(#c)), breve(bold(#c)), circle(bold(upright(#c))), caron(upright(sans(#c))), arrow(bold(frak(#c)))$ $test(i) \ test(j)$ + +--- math-accent-dotless-disabled --- +// Test disabling the dotless glyph variants. +$hat(i), hat(i, dotless: #false), accent(j, tilde), accent(j, tilde, dotless: #false)$ + +--- math-accent-dotless-set-rule --- +#set math.accent(dotless: false) +$ hat(i) $ From 012e14d40cb44997630cf6469a446f217f2e9057 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 31 Mar 2025 09:38:04 +0000 Subject: [PATCH 088/172] Unify layout of `vec` and `cases` with `mat` (#5934) --- crates/typst-layout/src/math/mat.rs | 195 +++++++++++----------- crates/typst-layout/src/math/shared.rs | 16 +- crates/typst-layout/src/math/underover.rs | 10 +- tests/ref/math-cases-linebreaks.png | Bin 570 -> 506 bytes tests/ref/math-equation-font.png | Bin 984 -> 1032 bytes tests/ref/math-mat-vec-cases-unity.png | Bin 0 -> 1202 bytes tests/ref/math-vec-linebreaks.png | Bin 856 -> 651 bytes tests/suite/math/cases.typ | 4 +- tests/suite/math/mat.typ | 11 +- tests/suite/math/vec.typ | 4 +- 10 files changed, 118 insertions(+), 122 deletions(-) create mode 100644 tests/ref/math-mat-vec-cases-unity.png diff --git a/crates/typst-layout/src/math/mat.rs b/crates/typst-layout/src/math/mat.rs index bf4929026..d678f8658 100644 --- a/crates/typst-layout/src/math/mat.rs +++ b/crates/typst-layout/src/math/mat.rs @@ -1,4 +1,4 @@ -use typst_library::diag::{bail, SourceResult}; +use typst_library::diag::{bail, warning, SourceResult}; use typst_library::foundations::{Content, Packed, Resolve, StyleChain}; use typst_library::layout::{ Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size, @@ -9,7 +9,7 @@ use typst_library::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape}; use typst_syntax::Span; use super::{ - alignments, delimiter_alignment, stack, style_for_denominator, AlignmentResult, + alignments, delimiter_alignment, style_for_denominator, AlignmentResult, FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL, }; @@ -23,67 +23,23 @@ pub fn layout_vec( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let delim = elem.delim(styles); - let frame = layout_vec_body( + let span = elem.span(); + + let column: Vec<&Content> = elem.children.iter().collect(); + let frame = layout_body( ctx, styles, - &elem.children, + &[column], elem.align(styles), - elem.gap(styles), LeftRightAlternator::Right, + None, + Axes::with_y(elem.gap(styles)), + span, + "elements", )?; - 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, - 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 = 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 delim = elem.delim(styles); - let frame = layout_mat_body( - ctx, - styles, - rows, - elem.align(styles), - augment, - Axes::new(elem.column_gap(styles), elem.row_gap(styles)), - elem.span(), - )?; - - layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), elem.span()) + layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span) } /// Lays out a [`CasesElem`]. @@ -93,60 +49,100 @@ pub fn layout_cases( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let delim = elem.delim(styles); - let frame = layout_vec_body( + let span = elem.span(); + + let column: Vec<&Content> = elem.children.iter().collect(); + let frame = layout_body( ctx, styles, - &elem.children, + &[column], FixedAlignment::Start, - elem.gap(styles), LeftRightAlternator::None, + None, + Axes::with_y(elem.gap(styles)), + span, + "branches", )?; + let delim = elem.delim(styles); let (open, close) = if elem.reverse(styles) { (None, delim.close()) } else { (delim.open(), None) }; - - layout_delimiters(ctx, styles, frame, open, close, elem.span()) + layout_delimiters(ctx, styles, frame, open, close, span) } -/// Layout the inner contents of a vector. -fn layout_vec_body( +/// Lays out a [`MatElem`]. +#[typst_macros::time(name = "math.mat", span = elem.span())] +pub fn layout_mat( + elem: &Packed, ctx: &mut MathContext, styles: StyleChain, - column: &[Content], - align: FixedAlignment, - row_gap: Rel, - alternator: LeftRightAlternator, -) -> SourceResult { - let gap = row_gap.relative_to(ctx.region.size.y); +) -> SourceResult<()> { + let span = elem.span(); + let rows = &elem.rows; + let ncols = rows.first().map_or(0, |row| row.len()); - let denom_style = style_for_denominator(styles); - let mut flat = vec![]; - for child in column { - // We allow linebreaks in cases and vectors, which are functionally - // identical to commas. - flat.extend(ctx.layout_into_run(child, styles.chain(&denom_style))?.rows()); + let augment = elem.augment(styles); + if let Some(aug) = &augment { + for &offset in &aug.hline.0 { + if offset == 0 || offset.unsigned_abs() >= rows.len() { + bail!( + 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() + ); + } + } + + for &offset in &aug.vline.0 { + if offset == 0 || offset.unsigned_abs() >= ncols { + bail!( + span, + "cannot draw a vertical line after column {} of a matrix with {} columns", + if offset < 0 { ncols as isize + offset } else { offset }, + ncols + ); + } + } } - // 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)))) + + // Transpose rows of the matrix into columns. + let mut row_iters: Vec<_> = rows.iter().map(|i| i.iter()).collect(); + let columns: Vec> = (0..ncols) + .map(|_| row_iters.iter_mut().map(|i| i.next().unwrap()).collect()) + .collect(); + + let frame = layout_body( + ctx, + styles, + &columns, + elem.align(styles), + LeftRightAlternator::Right, + augment, + Axes::new(elem.column_gap(styles), elem.row_gap(styles)), + span, + "cells", + )?; + + let delim = elem.delim(styles); + layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span) } -/// Layout the inner contents of a matrix. -fn layout_mat_body( +/// Layout the inner contents of a matrix, vector, or cases. +#[allow(clippy::too_many_arguments)] +fn layout_body( ctx: &mut MathContext, styles: StyleChain, - rows: &[Vec], + columns: &[Vec<&Content>], align: FixedAlignment, + alternator: LeftRightAlternator, augment: Option>, gap: Axes>, span: Span, + children: &str, ) -> SourceResult { - let ncols = rows.first().map_or(0, |row| row.len()); - let nrows = rows.len(); + let nrows = columns.first().map_or(0, |col| col.len()); + let ncols = columns.len(); if ncols == 0 || nrows == 0 { return Ok(Frame::soft(Size::zero())); } @@ -178,16 +174,11 @@ fn layout_mat_body( // 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. + let mut cols = vec![vec![]; ncols]; // 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 @@ -195,10 +186,22 @@ fn layout_mat_body( 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) { + for (column, col) in columns.iter().zip(&mut cols) { + for (cell, (ascent, descent)) in column.iter().zip(&mut heights) { + let cell_span = cell.span(); let cell = ctx.layout_into_run(cell, styles.chain(&denom_style))?; + // We ignore linebreaks in the cells as we can't differentiate + // alignment points for the whole body from ones for a specific + // cell, and multiline cells don't quite make sense at the moment. + if cell.is_multiline() { + ctx.engine.sink.warn(warning!( + cell_span, + "linebreaks are ignored in {}", children; + hint: "use commas instead to separate each line" + )); + } + ascent.set_max(cell.ascent().max(paren.ascent)); descent.set_max(cell.descent().max(paren.descent)); @@ -222,7 +225,7 @@ fn layout_mat_body( 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 cell = cell.into_line_frame(&points, alternator); let pos = Point::new( if points.is_empty() { x + align.position(rcol - cell.width()) diff --git a/crates/typst-layout/src/math/shared.rs b/crates/typst-layout/src/math/shared.rs index 5aebdacac..600c130d4 100644 --- a/crates/typst-layout/src/math/shared.rs +++ b/crates/typst-layout/src/math/shared.rs @@ -117,7 +117,6 @@ pub fn stack( gap: Abs, baseline: usize, alternator: LeftRightAlternator, - minimum_ascent_descent: Option<(Abs, Abs)>, ) -> Frame { let AlignmentResult { points, width } = alignments(&rows); let rows: Vec<_> = rows @@ -125,13 +124,9 @@ pub fn stack( .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::() + rows.iter().map(|row| row.height()).sum::() + rows.len().saturating_sub(1) as f64 * gap, )); @@ -142,14 +137,11 @@ pub fn stack( } 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); + let pos = Point::new(x, y); if i == baseline { - frame.set_baseline(y + row.baseline() + ascent_padded_part); + frame.set_baseline(y + row.baseline()); } - y += padded_height(row.height()) + gap; + y += row.height() + gap; frame.push_frame(pos, row); } diff --git a/crates/typst-layout/src/math/underover.rs b/crates/typst-layout/src/math/underover.rs index 7b3617c3e..5b6bd40eb 100644 --- a/crates/typst-layout/src/math/underover.rs +++ b/crates/typst-layout/src/math/underover.rs @@ -312,14 +312,8 @@ fn layout_underoverspreader( } }; - let frame = stack( - rows, - FixedAlignment::Center, - gap, - baseline, - LeftRightAlternator::Right, - None, - ); + let frame = + stack(rows, FixedAlignment::Center, gap, baseline, LeftRightAlternator::Right); ctx.push(FrameFragment::new(styles, frame).with_class(body_class)); Ok(()) diff --git a/tests/ref/math-cases-linebreaks.png b/tests/ref/math-cases-linebreaks.png index 543d5384c11a270a8a56f95e91e4f5ec7ac64d3f..eb4971c46fb2d2a36a8b95324d3d1e08b7d99319 100644 GIT binary patch delta 481 zcmV<70UrLk1o{JzBYy$GNklVP4Bt@3Ph~{FOY>~-6-Ym~#Fk@(%=gn7d8s##_|N9-h|Ka!jzuw_< z_^dgPGQ4DaHy6DPTURhgFj~rC+X$d z*Zcb|6!U@Jf`%y^^#b7U_EF#qbX=t@;hh41RPS2=ydL}DDms+_!Qf*H+cWR{nR?pA zq`(??MII3`luOhtmxVHZX&+S@xjHmcBS8ICMZZCNQe*yTvW{Iao?K$2M~*ie0O?xd zfm<&i8Ue;I67~?JUDnVX5>L}krf~T9e>)`Nb!69DT3Yx)FDVSiZpn3kTjVGv*L@b; z9);nE831eTfQMp?-OV?nMup+DZMjN3pfFt9L$sWP6f+Hsn<5lQAABqA!TMHEz9cRe8q zCP#{hjKH8aMcJl}i-<6+$P9wogh3C29$-Qlq}1S~m52`p2z#sqtf$p4{Ie*W^1g3T14$a%T?k;imS}Xu*AxThl5vuxwIhJd)kH^6!2pL7V0t2{+Kd2Rt|zK1 zybSY`$6yGPRKJY2V+%JoeQ>bv>DrQZjCWjjc|X8&ESNzC7Y7YIRV(jmr_iptIA~YB z-6rqaw$XMR?0-%H7M)9VA)QztpfLkw!h6e6LH9V$hchRamc!{O9Vd7bc z>fVN>mR(XB%vvNLd=(mCZyh=!Qns)PgX2aQ^RQ|gV6G-xUS(@I1Tf56%Ny$~&Kro0$&|(nW5as&2^UIJ kA*{Z0sT*Jwtb$SJ2Wbl+-x_NR>i_@%07*qoM6N<$f>qD?{Qv*} diff --git a/tests/ref/math-equation-font.png b/tests/ref/math-equation-font.png index eb84634e5a8f2c5e42a606d12505d505d63c3294..ec3c72311b4a40abad4b53074e7870c2042087db 100644 GIT binary patch delta 1023 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 N002ovPDHLkV1gW(A;bUx delta 834 zcmV-I1HJr<1=t3VBYy)QNklcHB<5?*i@o@c^IbymBXCA^i@1L`WLh=a zgU^jTb#dE4$bXZqC~OqQm3$Ew-hkXWuaCnxN=03KIU7<@`y!?cMnklSqpBe@HF;?- zVO-0hEiSIuro|u97R=L`pIQG*hV{y#uvIaNeZ9SP|Y{X{S2<~uHaTO2m z637y7d<>?bw0amLGBfQbmKmYOl2KwS2L4!7?~jkF2PnhD2f9Yi+RdowDKo;`>7%O*OBzw zDnwhnrIR@4Jm#Qgm)i;4vX#0xqJ}sJ{KQ`Uo4}>}sEg0s5nJ2LK`BGTGxVd_eQmgM zQ>lxq;&3T$H_D+)vP7I!fGfKuzZp8Iin@3@0DsaHgt%!Khu(>} Date: Tue, 1 Apr 2025 16:42:52 +0200 Subject: [PATCH 089/172] Switch PDF backend to `krilla` (#5420) Co-authored-by: Laurenz --- Cargo.lock | 200 ++++- Cargo.toml | 5 +- crates/typst-cli/src/args.rs | 32 +- crates/typst-cli/src/compile.rs | 39 +- crates/typst-layout/src/inline/shaping.rs | 40 +- crates/typst-library/src/layout/transform.rs | 14 + .../src/visualize/image/raster.rs | 27 +- crates/typst-pdf/Cargo.toml | 11 +- crates/typst-pdf/src/catalog.rs | 385 -------- crates/typst-pdf/src/color.rs | 394 --------- crates/typst-pdf/src/color_font.rs | 344 -------- crates/typst-pdf/src/content.rs | 823 ------------------ crates/typst-pdf/src/convert.rs | 661 ++++++++++++++ crates/typst-pdf/src/embed.rs | 150 +--- crates/typst-pdf/src/extg.rs | 53 -- crates/typst-pdf/src/font.rs | 278 ------ crates/typst-pdf/src/gradient.rs | 512 ----------- crates/typst-pdf/src/image.rs | 453 +++++----- crates/typst-pdf/src/lib.rs | 751 +++------------- crates/typst-pdf/src/link.rs | 94 ++ crates/typst-pdf/src/metadata.rs | 184 ++++ crates/typst-pdf/src/named_destination.rs | 86 -- crates/typst-pdf/src/outline.rs | 145 +-- crates/typst-pdf/src/page.rs | 348 ++------ crates/typst-pdf/src/paint.rs | 379 ++++++++ crates/typst-pdf/src/resources.rs | 349 -------- crates/typst-pdf/src/shape.rs | 106 +++ crates/typst-pdf/src/text.rs | 135 +++ crates/typst-pdf/src/tiling.rs | 184 ---- crates/typst-pdf/src/util.rs | 120 +++ 30 files changed, 2426 insertions(+), 4876 deletions(-) delete mode 100644 crates/typst-pdf/src/catalog.rs delete mode 100644 crates/typst-pdf/src/color.rs delete mode 100644 crates/typst-pdf/src/color_font.rs delete mode 100644 crates/typst-pdf/src/content.rs create mode 100644 crates/typst-pdf/src/convert.rs delete mode 100644 crates/typst-pdf/src/extg.rs delete mode 100644 crates/typst-pdf/src/font.rs delete mode 100644 crates/typst-pdf/src/gradient.rs create mode 100644 crates/typst-pdf/src/link.rs create mode 100644 crates/typst-pdf/src/metadata.rs delete mode 100644 crates/typst-pdf/src/named_destination.rs create mode 100644 crates/typst-pdf/src/paint.rs delete mode 100644 crates/typst-pdf/src/resources.rs create mode 100644 crates/typst-pdf/src/shape.rs create mode 100644 crates/typst-pdf/src/text.rs delete mode 100644 crates/typst-pdf/src/tiling.rs create mode 100644 crates/typst-pdf/src/util.rs diff --git a/Cargo.lock b/Cargo.lock index 630eade2f..c13c64819 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,6 +217,20 @@ name = "bytemuck" version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "byteorder" @@ -735,11 +749,12 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.35" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -749,6 +764,15 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -761,6 +785,15 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +[[package]] +name = "font-types" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d868ec188a98bb014c606072edd47e52e7ab7297db943b0b28503121e1d037bd" +dependencies = [ + "bytemuck", +] + [[package]] name = "fontconfig-parser" version = "0.5.7" @@ -829,6 +862,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "getopts" version = "0.2.21" @@ -966,7 +1008,7 @@ checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ "displaydoc", "serde", - "yoke", + "yoke 0.7.5", "zerofrom", "zerovec", ] @@ -1064,7 +1106,7 @@ dependencies = [ "stable_deref_trait", "tinystr", "writeable", - "yoke", + "yoke 0.7.5", "zerofrom", "zerovec", ] @@ -1310,6 +1352,48 @@ dependencies = [ "libc", ] +[[package]] +name = "krilla" +version = "0.3.0" +source = "git+https://github.com/LaurenzV/krilla?rev=14756f7#14756f7067cb1a80b73b712cae9f98597153e623" +dependencies = [ + "base64", + "bumpalo", + "comemo", + "flate2", + "float-cmp 0.10.0", + "fxhash", + "gif", + "image-webp", + "imagesize", + "once_cell", + "pdf-writer", + "png", + "rayon", + "rustybuzz", + "siphasher", + "skrifa", + "subsetter", + "tiny-skia-path", + "xmp-writer", + "yoke 0.8.0", + "zune-jpeg", +] + +[[package]] +name = "krilla-svg" +version = "0.3.0" +source = "git+https://github.com/LaurenzV/krilla?rev=14756f7#14756f7067cb1a80b73b712cae9f98597153e623" +dependencies = [ + "flate2", + "fontdb", + "krilla", + "png", + "resvg", + "tiny-skia", + "usvg", +] + [[package]] name = "kurbo" version = "0.11.1" @@ -1371,6 +1455,15 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "libz-rs-sys" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "902bc563b5d65ad9bba616b490842ef0651066a1a1dc3ce1087113ffcb873c8d" +dependencies = [ + "zlib-rs", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -1458,9 +1551,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" dependencies = [ "adler2", "simd-adler32", @@ -1739,8 +1832,7 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "pdf-writer" version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5df03c7d216de06f93f398ef06f1385a60f2c597bb96f8195c8d98e08a26b1d5" +source = "git+https://github.com/typst/pdf-writer?rev=0d513b9#0d513b9050d2f1a0507cabb4898aca971af6da98" dependencies = [ "bitflags 2.8.0", "itoa", @@ -1997,6 +2089,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "read-fonts" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f004ee5c610b8beb5f33273246893ac6258ec22185a6eb8ee132bccdb904cdaa" +dependencies = [ + "bytemuck", + "font-types", +] + [[package]] name = "redox_syscall" version = "0.5.8" @@ -2315,6 +2417,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "skrifa" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e7936ca3627fdb516e97aca3c8ab5103f94ae32fe5ce80a0a02edcbacb7b53" +dependencies = [ + "bytemuck", + "read-fonts", +] + [[package]] name = "slotmap" version = "1.0.7" @@ -2361,7 +2473,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" dependencies = [ - "float-cmp", + "float-cmp 0.9.0", ] [[package]] @@ -2405,27 +2517,9 @@ dependencies = [ [[package]] name = "subsetter" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f98178f34057d4d4de93d68104007c6dea4dfac930204a69ab4622daefa648" - -[[package]] -name = "svg2pdf" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e50dc062439cc1a396181059c80932a6e6bd731b130e674c597c0c8874b6df22" +source = "git+https://github.com/typst/subsetter?rev=460fdb6#460fdb66d6e0138b721b1ca9882faf15ce003246" dependencies = [ - "fontdb", - "image", - "log", - "miniz_oxide", - "once_cell", - "pdf-writer", - "resvg", - "siphasher", - "subsetter", - "tiny-skia", - "ttf-parser", - "usvg", + "fxhash", ] [[package]] @@ -3018,26 +3112,19 @@ dependencies = [ name = "typst-pdf" version = "0.13.1" dependencies = [ - "arrayvec", - "base64", "bytemuck", "comemo", "ecow", "image", - "indexmap 2.7.1", - "miniz_oxide", - "pdf-writer", + "krilla", + "krilla-svg", "serde", - "subsetter", - "svg2pdf", - "ttf-parser", "typst-assets", "typst-library", "typst-macros", "typst-syntax", "typst-timing", "typst-utils", - "xmp-writer", ] [[package]] @@ -3662,8 +3749,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "xmp-writer" version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb5954c9ca6dcc869e98d3e42760ed9dab08f3e70212b31d7ab8ae7f3b7a487" +source = "git+https://github.com/LaurenzV/xmp-writer?rev=a1cbb887#a1cbb887a84376fea4d7590d41c194a332840549" [[package]] name = "xz2" @@ -3701,7 +3787,19 @@ checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", - "yoke-derive", + "yoke-derive 0.7.5", + "zerofrom", +] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive 0.8.0", "zerofrom", ] @@ -3717,6 +3815,18 @@ dependencies = [ "synstructure", ] +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -3778,7 +3888,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" dependencies = [ "serde", - "yoke", + "yoke 0.7.5", "zerofrom", "zerovec-derive", ] @@ -3809,6 +3919,12 @@ dependencies = [ "zopfli", ] +[[package]] +name = "zlib-rs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b20717f0917c908dc63de2e44e97f1e6b126ca58d0e391cee86d504eb8fbd05" + [[package]] name = "zopfli" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index a73241832..cbe69a05d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,11 +71,12 @@ if_chain = "1" image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] } indexmap = { version = "2", features = ["serde"] } kamadak-exif = "0.6" +krilla = { git = "https://github.com/LaurenzV/krilla", rev = "14756f7", default-features = false, features = ["raster-images", "comemo", "rayon"] } +krilla-svg = { git = "https://github.com/LaurenzV/krilla", rev = "14756f7" } kurbo = "0.11" libfuzzer-sys = "0.4" lipsum = "0.9" memchr = "2" -miniz_oxide = "0.8" native-tls = "0.2" notify = "8" once_cell = "1" @@ -113,7 +114,6 @@ siphasher = "1" smallvec = { version = "1.11.1", features = ["union", "const_generics", "const_new"] } stacker = "0.1.15" subsetter = "0.2" -svg2pdf = "0.13" syn = { version = "2", features = ["full", "extra-traits"] } syntect = { version = "5", default-features = false, features = ["parsing", "regex-fancy", "plist-load", "yaml-load"] } tar = "0.4" @@ -140,7 +140,6 @@ wasmi = "0.40.0" web-sys = "0.3" xmlparser = "0.13.5" xmlwriter = "0.1.0" -xmp-writer = "0.3.1" xz2 = { version = "0.1", features = ["static"] } yaml-front-matter = "0.1" zip = { version = "2.5", default-features = false, features = ["deflate"] } diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs index 76f647276..fd0eb5f05 100644 --- a/crates/typst-cli/src/args.rs +++ b/crates/typst-cli/src/args.rs @@ -467,15 +467,45 @@ display_possible_values!(Feature); #[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)] #[allow(non_camel_case_types)] pub enum PdfStandard { + /// PDF 1.4. + #[value(name = "1.4")] + V_1_4, + /// PDF 1.5. + #[value(name = "1.5")] + V_1_5, + /// PDF 1.5. + #[value(name = "1.6")] + V_1_6, /// PDF 1.7. #[value(name = "1.7")] V_1_7, + /// PDF 2.0. + #[value(name = "2.0")] + V_2_0, + /// PDF/A-1b. + #[value(name = "a-1b")] + A_1b, /// PDF/A-2b. #[value(name = "a-2b")] A_2b, - /// PDF/A-3b. + /// PDF/A-2u. + #[value(name = "a-2u")] + A_2u, + /// PDF/A-3u. #[value(name = "a-3b")] A_3b, + /// PDF/A-3u. + #[value(name = "a-3u")] + A_3u, + /// PDF/A-4. + #[value(name = "a-4")] + A_4, + /// PDF/A-4f. + #[value(name = "a-4f")] + A_4f, + /// PDF/A-4e. + #[value(name = "a-4e")] + A_4e, } display_possible_values!(PdfStandard); diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index ae71e298c..4edb4c323 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -63,8 +63,7 @@ pub struct CompileConfig { /// Opens the output file with the default viewer or a specific program after /// compilation. pub open: Option>, - /// One (or multiple comma-separated) PDF standards that Typst will enforce - /// conformance with. + /// A list of standards the PDF should conform to. pub pdf_standards: PdfStandards, /// A path to write a Makefile rule describing the current compilation. pub make_deps: Option, @@ -130,18 +129,9 @@ impl CompileConfig { PageRanges::new(export_ranges.iter().map(|r| r.0.clone()).collect()) }); - let pdf_standards = { - let list = args - .pdf_standard - .iter() - .map(|standard| match standard { - PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7, - PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b, - PdfStandard::A_3b => typst_pdf::PdfStandard::A_3b, - }) - .collect::>(); - PdfStandards::new(&list)? - }; + let pdf_standards = PdfStandards::new( + &args.pdf_standard.iter().copied().map(Into::into).collect::>(), + )?; #[cfg(feature = "http-server")] let server = match watch { @@ -295,6 +285,7 @@ fn export_pdf(document: &PagedDocument, config: &CompileConfig) -> SourceResult< }) } }; + let options = PdfOptions { ident: Smart::Auto, timestamp, @@ -765,3 +756,23 @@ impl<'a> codespan_reporting::files::Files<'a> for SystemWorld { }) } } + +impl From for typst_pdf::PdfStandard { + fn from(standard: PdfStandard) -> Self { + match standard { + PdfStandard::V_1_4 => typst_pdf::PdfStandard::V_1_4, + PdfStandard::V_1_5 => typst_pdf::PdfStandard::V_1_5, + PdfStandard::V_1_6 => typst_pdf::PdfStandard::V_1_6, + PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7, + PdfStandard::V_2_0 => typst_pdf::PdfStandard::V_2_0, + PdfStandard::A_1b => typst_pdf::PdfStandard::A_1b, + PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b, + PdfStandard::A_2u => typst_pdf::PdfStandard::A_2u, + PdfStandard::A_3b => typst_pdf::PdfStandard::A_3b, + PdfStandard::A_3u => typst_pdf::PdfStandard::A_3u, + PdfStandard::A_4 => typst_pdf::PdfStandard::A_4, + PdfStandard::A_4f => typst_pdf::PdfStandard::A_4f, + PdfStandard::A_4e => typst_pdf::PdfStandard::A_4e, + } + } +} diff --git a/crates/typst-layout/src/inline/shaping.rs b/crates/typst-layout/src/inline/shaping.rs index 159619eb3..8236d1e36 100644 --- a/crates/typst-layout/src/inline/shaping.rs +++ b/crates/typst-layout/src/inline/shaping.rs @@ -824,12 +824,42 @@ fn shape_segment<'a>( // Add the glyph to the shaped output. if info.glyph_id != 0 && is_covered(cluster) { - // Determine the text range of the glyph. + // Assume we have the following sequence of (glyph_id, cluster): + // [(120, 0), (80, 0), (3, 3), (755, 4), (69, 4), (424, 13), + // (63, 13), (193, 25), (80, 25), (3, 31) + // + // We then want the sequence of (glyph_id, text_range) to look as follows: + // [(120, 0..3), (80, 0..3), (3, 3..4), (755, 4..13), (69, 4..13), + // (424, 13..25), (63, 13..25), (193, 25..31), (80, 25..31), (3, 31..x)] + // + // Each glyph in the same cluster should be assigned the full text + // range. This is necessary because only this way krilla can + // properly assign `ActualText` attributes in complex shaping + // scenarios. + + // The start of the glyph's text range. let start = base + cluster; - let end = base - + if ltr { i.checked_add(1) } else { i.checked_sub(1) } - .and_then(|last| infos.get(last)) - .map_or(text.len(), |info| info.cluster as usize); + + // Determine the end of the glyph's text range. + let mut k = i; + let step: isize = if ltr { 1 } else { -1 }; + let end = loop { + // If we've reached the end of the glyphs, the `end` of the + // range should be the end of the full text. + let Some((next, next_info)) = k + .checked_add_signed(step) + .and_then(|n| infos.get(n).map(|info| (n, info))) + else { + break base + text.len(); + }; + + // If the cluster doesn't match anymore, we've reached the end. + if next_info.cluster != info.cluster { + break base + next_info.cluster as usize; + } + + k = next; + }; let c = text[cluster..].chars().next().unwrap(); let script = c.script(); diff --git a/crates/typst-library/src/layout/transform.rs b/crates/typst-library/src/layout/transform.rs index 183df6098..d153d97db 100644 --- a/crates/typst-library/src/layout/transform.rs +++ b/crates/typst-library/src/layout/transform.rs @@ -307,6 +307,20 @@ impl Transform { Self { sx, sy, ..Self::identity() } } + /// A scale transform at a specific position. + pub fn scale_at(sx: Ratio, sy: Ratio, px: Abs, py: Abs) -> Self { + Self::translate(px, py) + .pre_concat(Self::scale(sx, sy)) + .pre_concat(Self::translate(-px, -py)) + } + + /// A rotate transform at a specific position. + pub fn rotate_at(angle: Angle, px: Abs, py: Abs) -> Self { + Self::translate(px, py) + .pre_concat(Self::rotate(angle)) + .pre_concat(Self::translate(-px, -py)) + } + /// A rotate transform. pub fn rotate(angle: Angle) -> Self { let cos = Ratio::new(angle.cos()); diff --git a/crates/typst-library/src/visualize/image/raster.rs b/crates/typst-library/src/visualize/image/raster.rs index 453b94066..21d5b18fc 100644 --- a/crates/typst-library/src/visualize/image/raster.rs +++ b/crates/typst-library/src/visualize/image/raster.rs @@ -3,6 +3,8 @@ use std::hash::{Hash, Hasher}; use std::io; use std::sync::Arc; +use crate::diag::{bail, StrResult}; +use crate::foundations::{cast, dict, Bytes, Cast, Dict, Smart, Value}; use ecow::{eco_format, EcoString}; use image::codecs::gif::GifDecoder; use image::codecs::jpeg::JpegDecoder; @@ -11,9 +13,6 @@ use image::{ guess_format, DynamicImage, ImageBuffer, ImageDecoder, ImageResult, Limits, Pixel, }; -use crate::diag::{bail, StrResult}; -use crate::foundations::{cast, dict, Bytes, Cast, Dict, Smart, Value}; - /// A decoded raster image. #[derive(Clone, Hash)] pub struct RasterImage(Arc); @@ -22,7 +21,8 @@ pub struct RasterImage(Arc); struct Repr { data: Bytes, format: RasterFormat, - dynamic: image::DynamicImage, + dynamic: Arc, + exif_rotation: Option, icc: Option, dpi: Option, } @@ -50,6 +50,8 @@ impl RasterImage { format: RasterFormat, icc: Smart, ) -> StrResult { + let mut exif_rot = None; + let (dynamic, icc, dpi) = match format { RasterFormat::Exchange(format) => { fn decode( @@ -85,6 +87,7 @@ impl RasterImage { // Apply rotation from EXIF metadata. if let Some(rotation) = exif.as_ref().and_then(exif_rotation) { apply_rotation(&mut dynamic, rotation); + exif_rot = Some(rotation); } // Extract pixel density. @@ -136,7 +139,14 @@ impl RasterImage { } }; - Ok(Self(Arc::new(Repr { data, format, dynamic, icc, dpi }))) + Ok(Self(Arc::new(Repr { + data, + format, + exif_rotation: exif_rot, + dynamic: Arc::new(dynamic), + icc, + dpi, + }))) } /// The raw image data. @@ -159,6 +169,11 @@ impl RasterImage { self.dynamic().height() } + /// TODO. + pub fn exif_rotation(&self) -> Option { + self.0.exif_rotation + } + /// The image's pixel density in pixels per inch, if known. /// /// This is guaranteed to be positive. @@ -167,7 +182,7 @@ impl RasterImage { } /// Access the underlying dynamic image. - pub fn dynamic(&self) -> &image::DynamicImage { + pub fn dynamic(&self) -> &Arc { &self.0.dynamic } diff --git a/crates/typst-pdf/Cargo.toml b/crates/typst-pdf/Cargo.toml index bc0da06c3..f6f08b5bc 100644 --- a/crates/typst-pdf/Cargo.toml +++ b/crates/typst-pdf/Cargo.toml @@ -19,20 +19,13 @@ typst-macros = { workspace = true } typst-syntax = { workspace = true } typst-timing = { workspace = true } typst-utils = { workspace = true } -arrayvec = { workspace = true } -base64 = { workspace = true } bytemuck = { workspace = true } comemo = { workspace = true } ecow = { workspace = true } image = { workspace = true } -indexmap = { workspace = true } -miniz_oxide = { workspace = true } -pdf-writer = { workspace = true } +krilla = { workspace = true } +krilla-svg = { workspace = true } serde = { workspace = true } -subsetter = { workspace = true } -svg2pdf = { workspace = true } -ttf-parser = { workspace = true } -xmp-writer = { workspace = true } [lints] workspace = true diff --git a/crates/typst-pdf/src/catalog.rs b/crates/typst-pdf/src/catalog.rs deleted file mode 100644 index 709b01553..000000000 --- a/crates/typst-pdf/src/catalog.rs +++ /dev/null @@ -1,385 +0,0 @@ -use std::num::NonZeroUsize; - -use ecow::eco_format; -use pdf_writer::types::Direction; -use pdf_writer::writers::PageLabel; -use pdf_writer::{Finish, Name, Pdf, Ref, Str, TextStr}; -use typst_library::diag::{bail, SourceResult}; -use typst_library::foundations::{Datetime, Smart}; -use typst_library::layout::Dir; -use typst_library::text::Lang; -use typst_syntax::Span; -use xmp_writer::{DateTime, LangId, RenditionClass, XmpWriter}; - -use crate::page::PdfPageLabel; -use crate::{hash_base64, outline, TextStrExt, Timestamp, Timezone, WithEverything}; - -/// Write the document catalog. -pub fn write_catalog( - ctx: WithEverything, - pdf: &mut Pdf, - alloc: &mut Ref, -) -> SourceResult<()> { - let lang = ctx - .resources - .languages - .iter() - .max_by_key(|(_, &count)| count) - .map(|(&l, _)| l); - - let dir = if lang.map(Lang::dir) == Some(Dir::RTL) { - Direction::R2L - } else { - Direction::L2R - }; - - // Write the outline tree. - let outline_root_id = outline::write_outline(pdf, alloc, &ctx); - - // Write the page labels. - let page_labels = write_page_labels(pdf, alloc, &ctx); - - // Write the document information. - let info_ref = alloc.bump(); - let mut info = pdf.document_info(info_ref); - let mut xmp = XmpWriter::new(); - if let Some(title) = &ctx.document.info.title { - info.title(TextStr::trimmed(title)); - xmp.title([(None, title.as_str())]); - } - - if let Some(description) = &ctx.document.info.description { - info.subject(TextStr::trimmed(description)); - xmp.description([(None, description.as_str())]); - } - - let authors = &ctx.document.info.author; - if !authors.is_empty() { - // Turns out that if the authors are given in both the document - // information dictionary and the XMP metadata, Acrobat takes a little - // bit of both: The first author from the document information - // dictionary and the remaining authors from the XMP metadata. - // - // To fix this for Acrobat, we could omit the remaining authors or all - // metadata from the document information catalog (it is optional) and - // only write XMP. However, not all other tools (including Apple - // Preview) read the XMP data. This means we do want to include all - // authors in the document information dictionary. - // - // Thus, the only alternative is to fold all authors into a single - // `` in the XMP metadata. This is, in fact, exactly what the - // PDF/A spec Part 1 section 6.7.3 has to say about the matter. It's a - // bit weird to not use the array (and it makes Acrobat show the author - // list in quotes), but there's not much we can do about that. - let joined = authors.join(", "); - info.author(TextStr::trimmed(&joined)); - xmp.creator([joined.as_str()]); - } - - let creator = eco_format!("Typst {}", env!("CARGO_PKG_VERSION")); - info.creator(TextStr(&creator)); - xmp.creator_tool(&creator); - - let keywords = &ctx.document.info.keywords; - if !keywords.is_empty() { - let joined = keywords.join(", "); - info.keywords(TextStr::trimmed(&joined)); - xmp.pdf_keywords(&joined); - } - let (date, tz) = document_date(ctx.document.info.date, ctx.options.timestamp); - if let Some(pdf_date) = date.and_then(|date| pdf_date(date, tz)) { - info.creation_date(pdf_date); - info.modified_date(pdf_date); - } - - info.finish(); - - // A unique ID for this instance of the document. Changes if anything - // changes in the frames. - let instance_id = hash_base64(&pdf.as_bytes()); - - // Determine the document's ID. It should be as stable as possible. - const PDF_VERSION: &str = "PDF-1.7"; - let doc_id = if let Smart::Custom(ident) = ctx.options.ident { - // We were provided with a stable ID. Yay! - hash_base64(&(PDF_VERSION, ident)) - } else if ctx.document.info.title.is_some() && !ctx.document.info.author.is_empty() { - // If not provided from the outside, but title and author were given, we - // compute a hash of them, which should be reasonably stable and unique. - hash_base64(&(PDF_VERSION, &ctx.document.info.title, &ctx.document.info.author)) - } else { - // The user provided no usable metadata which we can use as an `/ID`. - instance_id.clone() - }; - - xmp.document_id(&doc_id); - xmp.instance_id(&instance_id); - xmp.format("application/pdf"); - xmp.pdf_version("1.7"); - xmp.language(ctx.resources.languages.keys().map(|lang| LangId(lang.as_str()))); - xmp.num_pages(ctx.document.pages.len() as u32); - xmp.rendition_class(RenditionClass::Proof); - - if let Some(xmp_date) = date.and_then(|date| xmp_date(date, tz)) { - xmp.create_date(xmp_date); - xmp.modify_date(xmp_date); - - if ctx.options.standards.pdfa { - let mut history = xmp.history(); - history - .add_event() - .action(xmp_writer::ResourceEventAction::Saved) - .when(xmp_date) - .instance_id(&eco_format!("{instance_id}_source")); - history - .add_event() - .action(xmp_writer::ResourceEventAction::Converted) - .when(xmp_date) - .instance_id(&instance_id) - .software_agent(&creator); - } - } - - // Assert dominance. - if let Some((part, conformance)) = ctx.options.standards.pdfa_part { - let mut extension_schemas = xmp.extension_schemas(); - extension_schemas - .xmp_media_management() - .properties() - .describe_instance_id(); - extension_schemas.pdf().properties().describe_all(); - extension_schemas.finish(); - xmp.pdfa_part(part); - xmp.pdfa_conformance(conformance); - } - - let xmp_buf = xmp.finish(None); - let meta_ref = alloc.bump(); - pdf.stream(meta_ref, xmp_buf.as_bytes()) - .pair(Name(b"Type"), Name(b"Metadata")) - .pair(Name(b"Subtype"), Name(b"XML")); - - // Set IDs only now, so that we don't need to clone them. - pdf.set_file_id((doc_id.into_bytes(), instance_id.into_bytes())); - - // Write the document catalog. - let catalog_ref = alloc.bump(); - let mut catalog = pdf.catalog(catalog_ref); - catalog.pages(ctx.page_tree_ref); - catalog.viewer_preferences().direction(dir); - catalog.metadata(meta_ref); - - let has_dests = !ctx.references.named_destinations.dests.is_empty(); - let has_embeddings = !ctx.references.embedded_files.is_empty(); - - // Write the `/Names` dictionary. - if has_dests || has_embeddings { - // Write the named destination tree if there are any entries. - let mut name_dict = catalog.names(); - if has_dests { - let mut dests_name_tree = name_dict.destinations(); - let mut names = dests_name_tree.names(); - for &(name, dest_ref, ..) in &ctx.references.named_destinations.dests { - names.insert(Str(name.resolve().as_bytes()), dest_ref); - } - } - - if has_embeddings { - let mut embedded_files = name_dict.embedded_files(); - let mut names = embedded_files.names(); - for (name, file_ref) in &ctx.references.embedded_files { - names.insert(Str(name.as_bytes()), *file_ref); - } - } - } - - if has_embeddings && ctx.options.standards.pdfa { - // PDF 2.0, but ISO 19005-3 (PDF/A-3) Annex E allows it for PDF/A-3. - let mut associated_files = catalog.insert(Name(b"AF")).array().typed(); - for (_, file_ref) in ctx.references.embedded_files { - associated_files.item(file_ref).finish(); - } - } - - // Insert the page labels. - if !page_labels.is_empty() { - let mut num_tree = catalog.page_labels(); - let mut entries = num_tree.nums(); - for (n, r) in &page_labels { - entries.insert(n.get() as i32 - 1, *r); - } - } - - if let Some(outline_root_id) = outline_root_id { - catalog.outlines(outline_root_id); - } - - if let Some(lang) = lang { - catalog.lang(TextStr(lang.as_str())); - } - - if ctx.options.standards.pdfa { - catalog - .output_intents() - .push() - .subtype(pdf_writer::types::OutputIntentSubtype::PDFA) - .output_condition(TextStr("sRGB")) - .output_condition_identifier(TextStr("Custom")) - .info(TextStr("sRGB IEC61966-2.1")) - .dest_output_profile(ctx.globals.color_functions.srgb.unwrap()); - } - - catalog.finish(); - - if ctx.options.standards.pdfa && pdf.refs().count() > 8388607 { - bail!(Span::detached(), "too many PDF objects"); - } - - Ok(()) -} - -/// Write the page labels. -pub(crate) fn write_page_labels( - chunk: &mut Pdf, - alloc: &mut Ref, - ctx: &WithEverything, -) -> Vec<(NonZeroUsize, Ref)> { - // If there is no exported page labeled, we skip the writing - if !ctx.pages.iter().filter_map(Option::as_ref).any(|p| { - p.label - .as_ref() - .is_some_and(|l| l.prefix.is_some() || l.style.is_some()) - }) { - return Vec::new(); - } - - let empty_label = PdfPageLabel::default(); - let mut result = vec![]; - let mut prev: Option<&PdfPageLabel> = None; - - // Skip non-exported pages for numbering. - for (i, page) in ctx.pages.iter().filter_map(Option::as_ref).enumerate() { - let nr = NonZeroUsize::new(1 + i).unwrap(); - // If there are pages with empty labels between labeled pages, we must - // write empty PageLabel entries. - let label = page.label.as_ref().unwrap_or(&empty_label); - - if let Some(pre) = prev { - if label.prefix == pre.prefix - && label.style == pre.style - && label.offset == pre.offset.map(|n| n.saturating_add(1)) - { - prev = Some(label); - continue; - } - } - - let id = alloc.bump(); - let mut entry = chunk.indirect(id).start::(); - - // Only add what is actually provided. Don't add empty prefix string if - // it wasn't given for example. - if let Some(prefix) = &label.prefix { - entry.prefix(TextStr::trimmed(prefix)); - } - - if let Some(style) = label.style { - entry.style(style.to_pdf_numbering_style()); - } - - if let Some(offset) = label.offset { - entry.offset(offset.get() as i32); - } - - result.push((nr, id)); - prev = Some(label); - } - - result -} - -/// Resolve the document date. -/// -/// (1) If the `document.date` is set to specific `datetime` or `none`, use it. -/// (2) If the `document.date` is set to `auto` or not set, try to use the -/// date from the options. -/// (3) Otherwise, we don't write date metadata. -pub fn document_date( - document_date: Smart>, - timestamp: Option, -) -> (Option, Option) { - match (document_date, timestamp) { - (Smart::Custom(date), _) => (date, None), - (Smart::Auto, Some(timestamp)) => { - (Some(timestamp.datetime), Some(timestamp.timezone)) - } - _ => (None, None), - } -} - -/// Converts a datetime to a pdf-writer date. -pub fn pdf_date(datetime: Datetime, tz: Option) -> Option { - let year = datetime.year().filter(|&y| y >= 0)? as u16; - - let mut pdf_date = pdf_writer::Date::new(year); - - if let Some(month) = datetime.month() { - pdf_date = pdf_date.month(month); - } - - if let Some(day) = datetime.day() { - pdf_date = pdf_date.day(day); - } - - if let Some(h) = datetime.hour() { - pdf_date = pdf_date.hour(h); - } - - if let Some(m) = datetime.minute() { - pdf_date = pdf_date.minute(m); - } - - if let Some(s) = datetime.second() { - pdf_date = pdf_date.second(s); - } - - match tz { - Some(Timezone::UTC) => { - pdf_date = pdf_date.utc_offset_hour(0).utc_offset_minute(0) - } - Some(Timezone::Local { hour_offset, minute_offset }) => { - pdf_date = - pdf_date.utc_offset_hour(hour_offset).utc_offset_minute(minute_offset) - } - None => {} - } - - Some(pdf_date) -} - -/// Converts a datetime to an xmp-writer datetime. -fn xmp_date( - datetime: Datetime, - timezone: Option, -) -> Option { - let year = datetime.year().filter(|&y| y >= 0)? as u16; - let timezone = timezone.map(|tz| match tz { - Timezone::UTC => xmp_writer::Timezone::Utc, - Timezone::Local { hour_offset, minute_offset } => { - // The xmp-writer use signed integers for the minute offset, which - // can be buggy if the minute offset is negative. And because our - // minute_offset is ensured to be `0 <= minute_offset < 60`, we can - // safely cast it to a signed integer. - xmp_writer::Timezone::Local { hour: hour_offset, minute: minute_offset as i8 } - } - }); - Some(DateTime { - year, - month: datetime.month(), - day: datetime.day(), - hour: datetime.hour(), - minute: datetime.minute(), - second: datetime.second(), - timezone, - }) -} diff --git a/crates/typst-pdf/src/color.rs b/crates/typst-pdf/src/color.rs deleted file mode 100644 index 412afca9a..000000000 --- a/crates/typst-pdf/src/color.rs +++ /dev/null @@ -1,394 +0,0 @@ -use std::sync::LazyLock; - -use arrayvec::ArrayVec; -use pdf_writer::{writers, Chunk, Dict, Filter, Name, Ref}; -use typst_library::diag::{bail, SourceResult}; -use typst_library::visualize::{Color, ColorSpace, Paint}; -use typst_syntax::Span; - -use crate::{content, deflate, PdfChunk, PdfOptions, Renumber, WithResources}; - -// The names of the color spaces. -pub const SRGB: Name<'static> = Name(b"srgb"); -pub const D65_GRAY: Name<'static> = Name(b"d65gray"); -pub const LINEAR_SRGB: Name<'static> = Name(b"linearrgb"); - -// The ICC profiles. -static SRGB_ICC_DEFLATED: LazyLock> = - LazyLock::new(|| deflate(typst_assets::icc::S_RGB_V4)); -static GRAY_ICC_DEFLATED: LazyLock> = - LazyLock::new(|| deflate(typst_assets::icc::S_GREY_V4)); - -/// The color spaces present in the PDF document -#[derive(Default)] -pub struct ColorSpaces { - use_srgb: bool, - use_d65_gray: bool, - use_linear_rgb: bool, -} - -impl ColorSpaces { - /// Mark a color space as used. - pub fn mark_as_used(&mut self, color_space: ColorSpace) { - match color_space { - ColorSpace::Oklch - | ColorSpace::Oklab - | ColorSpace::Hsl - | ColorSpace::Hsv - | ColorSpace::Srgb => { - self.use_srgb = true; - } - ColorSpace::D65Gray => { - self.use_d65_gray = true; - } - ColorSpace::LinearRgb => { - self.use_linear_rgb = true; - } - ColorSpace::Cmyk => {} - } - } - - /// Write the color spaces to the PDF file. - pub fn write_color_spaces(&self, mut spaces: Dict, refs: &ColorFunctionRefs) { - if self.use_srgb { - write(ColorSpace::Srgb, spaces.insert(SRGB).start(), refs); - } - - if self.use_d65_gray { - write(ColorSpace::D65Gray, spaces.insert(D65_GRAY).start(), refs); - } - - if self.use_linear_rgb { - write(ColorSpace::LinearRgb, spaces.insert(LINEAR_SRGB).start(), refs); - } - } - - /// Write the necessary color spaces functions and ICC profiles to the - /// PDF file. - pub fn write_functions(&self, chunk: &mut Chunk, refs: &ColorFunctionRefs) { - // Write the sRGB color space. - if let Some(id) = refs.srgb { - chunk - .icc_profile(id, &SRGB_ICC_DEFLATED) - .n(3) - .range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) - .filter(Filter::FlateDecode); - } - - // Write the gray color space. - if let Some(id) = refs.d65_gray { - chunk - .icc_profile(id, &GRAY_ICC_DEFLATED) - .n(1) - .range([0.0, 1.0]) - .filter(Filter::FlateDecode); - } - } - - /// Merge two color space usage information together: a given color space is - /// considered to be used if it is used on either side. - pub fn merge(&mut self, other: &Self) { - self.use_d65_gray |= other.use_d65_gray; - self.use_linear_rgb |= other.use_linear_rgb; - self.use_srgb |= other.use_srgb; - } -} - -/// Write the color space. -pub fn write( - color_space: ColorSpace, - writer: writers::ColorSpace, - refs: &ColorFunctionRefs, -) { - match color_space { - ColorSpace::Srgb - | ColorSpace::Oklab - | ColorSpace::Hsl - | ColorSpace::Hsv - | ColorSpace::Oklch => writer.icc_based(refs.srgb.unwrap()), - ColorSpace::D65Gray => writer.icc_based(refs.d65_gray.unwrap()), - ColorSpace::LinearRgb => { - writer.cal_rgb( - [0.9505, 1.0, 1.0888], - None, - Some([1.0, 1.0, 1.0]), - Some([ - 0.4124, 0.2126, 0.0193, 0.3576, 0.715, 0.1192, 0.1805, 0.0722, 0.9505, - ]), - ); - } - ColorSpace::Cmyk => writer.device_cmyk(), - } -} - -/// Global references for color conversion functions. -/// -/// These functions are only written once (at most, they are not written if not -/// needed) in the final document, and be shared by all color space -/// dictionaries. -pub struct ColorFunctionRefs { - pub srgb: Option, - d65_gray: Option, -} - -impl Renumber for ColorFunctionRefs { - fn renumber(&mut self, offset: i32) { - if let Some(r) = &mut self.srgb { - r.renumber(offset); - } - if let Some(r) = &mut self.d65_gray { - r.renumber(offset); - } - } -} - -/// Allocate all necessary [`ColorFunctionRefs`]. -pub fn alloc_color_functions_refs( - context: &WithResources, -) -> SourceResult<(PdfChunk, ColorFunctionRefs)> { - let mut chunk = PdfChunk::new(); - let mut used_color_spaces = ColorSpaces::default(); - - if context.options.standards.pdfa { - used_color_spaces.mark_as_used(ColorSpace::Srgb); - } - - context.resources.traverse(&mut |r| { - used_color_spaces.merge(&r.colors); - Ok(()) - })?; - - let refs = ColorFunctionRefs { - srgb: if used_color_spaces.use_srgb { Some(chunk.alloc()) } else { None }, - d65_gray: if used_color_spaces.use_d65_gray { Some(chunk.alloc()) } else { None }, - }; - - Ok((chunk, refs)) -} - -/// Encodes the color into four f32s, which can be used in a PDF file. -/// Ensures that the values are in the range [0.0, 1.0]. -/// -/// # Why? -/// - Oklab: The a and b components are in the range [-0.5, 0.5] and the PDF -/// specifies (and some readers enforce) that all color values be in the range -/// [0.0, 1.0]. This means that the PostScript function and the encoded color -/// must be offset by 0.5. -/// - HSV/HSL: The hue component is in the range [0.0, 360.0] and the PDF format -/// specifies that it must be in the range [0.0, 1.0]. This means that the -/// PostScript function and the encoded color must be divided by 360.0. -pub trait ColorEncode { - /// Performs the color to PDF f32 array conversion. - fn encode(&self, color: Color) -> [f32; 4]; -} - -impl ColorEncode for ColorSpace { - fn encode(&self, color: Color) -> [f32; 4] { - match self { - ColorSpace::Oklab | ColorSpace::Oklch | ColorSpace::Hsl | ColorSpace::Hsv => { - color.to_space(ColorSpace::Srgb).to_vec4() - } - _ => color.to_space(*self).to_vec4(), - } - } -} - -/// Encodes a paint into either a fill or stroke color. -pub(super) trait PaintEncode { - /// Set the paint as the fill color. - fn set_as_fill( - &self, - ctx: &mut content::Builder, - on_text: bool, - transforms: content::Transforms, - ) -> SourceResult<()>; - - /// Set the paint as the stroke color. - fn set_as_stroke( - &self, - ctx: &mut content::Builder, - on_text: bool, - transforms: content::Transforms, - ) -> SourceResult<()>; -} - -impl PaintEncode for Paint { - fn set_as_fill( - &self, - ctx: &mut content::Builder, - on_text: bool, - transforms: content::Transforms, - ) -> SourceResult<()> { - match self { - Self::Solid(c) => c.set_as_fill(ctx, on_text, transforms), - Self::Gradient(gradient) => gradient.set_as_fill(ctx, on_text, transforms), - Self::Tiling(tiling) => tiling.set_as_fill(ctx, on_text, transforms), - } - } - - fn set_as_stroke( - &self, - ctx: &mut content::Builder, - on_text: bool, - transforms: content::Transforms, - ) -> SourceResult<()> { - match self { - Self::Solid(c) => c.set_as_stroke(ctx, on_text, transforms), - Self::Gradient(gradient) => gradient.set_as_stroke(ctx, on_text, transforms), - Self::Tiling(tiling) => tiling.set_as_stroke(ctx, on_text, transforms), - } - } -} - -impl PaintEncode for Color { - fn set_as_fill( - &self, - ctx: &mut content::Builder, - _: bool, - _: content::Transforms, - ) -> SourceResult<()> { - match self { - Color::Luma(_) => { - ctx.resources.colors.mark_as_used(ColorSpace::D65Gray); - ctx.set_fill_color_space(D65_GRAY); - - let [l, _, _, _] = ColorSpace::D65Gray.encode(*self); - ctx.content.set_fill_color([l]); - } - Color::LinearRgb(_) => { - ctx.resources.colors.mark_as_used(ColorSpace::LinearRgb); - ctx.set_fill_color_space(LINEAR_SRGB); - - let [r, g, b, _] = ColorSpace::LinearRgb.encode(*self); - ctx.content.set_fill_color([r, g, b]); - } - // Oklab & friends are encoded as RGB. - Color::Rgb(_) - | Color::Oklab(_) - | Color::Oklch(_) - | Color::Hsl(_) - | Color::Hsv(_) => { - ctx.resources.colors.mark_as_used(ColorSpace::Srgb); - ctx.set_fill_color_space(SRGB); - - let [r, g, b, _] = ColorSpace::Srgb.encode(*self); - ctx.content.set_fill_color([r, g, b]); - } - Color::Cmyk(_) => { - check_cmyk_allowed(ctx.options)?; - ctx.reset_fill_color_space(); - - let [c, m, y, k] = ColorSpace::Cmyk.encode(*self); - ctx.content.set_fill_cmyk(c, m, y, k); - } - } - Ok(()) - } - - fn set_as_stroke( - &self, - ctx: &mut content::Builder, - _: bool, - _: content::Transforms, - ) -> SourceResult<()> { - match self { - Color::Luma(_) => { - ctx.resources.colors.mark_as_used(ColorSpace::D65Gray); - ctx.set_stroke_color_space(D65_GRAY); - - let [l, _, _, _] = ColorSpace::D65Gray.encode(*self); - ctx.content.set_stroke_color([l]); - } - Color::LinearRgb(_) => { - ctx.resources.colors.mark_as_used(ColorSpace::LinearRgb); - ctx.set_stroke_color_space(LINEAR_SRGB); - - let [r, g, b, _] = ColorSpace::LinearRgb.encode(*self); - ctx.content.set_stroke_color([r, g, b]); - } - // Oklab & friends are encoded as RGB. - Color::Rgb(_) - | Color::Oklab(_) - | Color::Oklch(_) - | Color::Hsl(_) - | Color::Hsv(_) => { - ctx.resources.colors.mark_as_used(ColorSpace::Srgb); - ctx.set_stroke_color_space(SRGB); - - let [r, g, b, _] = ColorSpace::Srgb.encode(*self); - ctx.content.set_stroke_color([r, g, b]); - } - Color::Cmyk(_) => { - check_cmyk_allowed(ctx.options)?; - ctx.reset_stroke_color_space(); - - let [c, m, y, k] = ColorSpace::Cmyk.encode(*self); - ctx.content.set_stroke_cmyk(c, m, y, k); - } - } - Ok(()) - } -} - -/// Extra color space functions. -pub(super) trait ColorSpaceExt { - /// Returns the range of the color space. - fn range(self) -> &'static [f32]; - - /// Converts a color to the color space. - fn convert(self, color: Color) -> ArrayVec; -} - -impl ColorSpaceExt for ColorSpace { - fn range(self) -> &'static [f32] { - match self { - ColorSpace::D65Gray => &[0.0, 1.0], - ColorSpace::Oklab => &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0], - ColorSpace::Oklch => &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0], - ColorSpace::LinearRgb => &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0], - ColorSpace::Srgb => &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0], - ColorSpace::Cmyk => &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0], - ColorSpace::Hsl => &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0], - ColorSpace::Hsv => &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0], - } - } - - fn convert(self, color: Color) -> ArrayVec { - let components = self.encode(color); - - self.range() - .chunks(2) - .zip(components) - .map(|(range, component)| U::quantize(component, [range[0], range[1]])) - .collect() - } -} - -/// Quantizes a color component to a specific type. -pub(super) trait QuantizedColor { - fn quantize(color: f32, range: [f32; 2]) -> Self; -} - -impl QuantizedColor for u16 { - fn quantize(color: f32, [min, max]: [f32; 2]) -> Self { - let value = (color - min) / (max - min); - (value * Self::MAX as f32).round().clamp(0.0, Self::MAX as f32) as Self - } -} - -impl QuantizedColor for f32 { - fn quantize(color: f32, [min, max]: [f32; 2]) -> Self { - color.clamp(min, max) - } -} - -/// Fails with an error if PDF/A processing is enabled. -pub(super) fn check_cmyk_allowed(options: &PdfOptions) -> SourceResult<()> { - if options.standards.pdfa { - bail!( - Span::detached(), - "cmyk colors are not currently supported by PDF/A export" - ); - } - Ok(()) -} diff --git a/crates/typst-pdf/src/color_font.rs b/crates/typst-pdf/src/color_font.rs deleted file mode 100644 index 1183e966e..000000000 --- a/crates/typst-pdf/src/color_font.rs +++ /dev/null @@ -1,344 +0,0 @@ -//! OpenType fonts generally define monochrome glyphs, but they can also define -//! glyphs with colors. This is how emojis are generally implemented for -//! example. -//! -//! There are various standards to represent color glyphs, but PDF readers don't -//! support any of them natively, so Typst has to handle them manually. - -use std::collections::HashMap; - -use ecow::eco_format; -use indexmap::IndexMap; -use pdf_writer::types::UnicodeCmap; -use pdf_writer::writers::WMode; -use pdf_writer::{Filter, Finish, Name, Rect, Ref}; -use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult}; -use typst_library::foundations::Repr; -use typst_library::layout::Em; -use typst_library::text::color::glyph_frame; -use typst_library::text::{Font, Glyph, TextItemView}; - -use crate::font::{base_font_name, write_font_descriptor, CMAP_NAME, SYSTEM_INFO}; -use crate::resources::{Resources, ResourcesRefs}; -use crate::{content, EmExt, PdfChunk, PdfOptions, WithGlobalRefs}; - -/// Write color fonts in the PDF document. -/// -/// They are written as Type3 fonts, which map glyph IDs to arbitrary PDF -/// instructions. -pub fn write_color_fonts( - context: &WithGlobalRefs, -) -> SourceResult<(PdfChunk, HashMap)> { - let mut out = HashMap::new(); - let mut chunk = PdfChunk::new(); - context.resources.traverse(&mut |resources: &Resources| { - let Some(color_fonts) = &resources.color_fonts else { - return Ok(()); - }; - - for (color_font, font_slice) in color_fonts.iter() { - if out.contains_key(&font_slice) { - continue; - } - - // Allocate some IDs. - let subfont_id = chunk.alloc(); - let cmap_ref = chunk.alloc(); - let descriptor_ref = chunk.alloc(); - let widths_ref = chunk.alloc(); - - // And a map between glyph IDs and the instructions to draw this - // glyph. - let mut glyphs_to_instructions = Vec::new(); - - let start = font_slice.subfont * 256; - let end = (start + 256).min(color_font.glyphs.len()); - let glyph_count = end - start; - let subset = &color_font.glyphs[start..end]; - let mut widths = Vec::new(); - let mut gids = Vec::new(); - - let scale_factor = font_slice.font.ttf().units_per_em() as f32; - - // Write the instructions for each glyph. - for color_glyph in subset { - let instructions_stream_ref = chunk.alloc(); - let width = font_slice - .font - .advance(color_glyph.gid) - .unwrap_or(Em::new(0.0)) - .get() as f32 - * scale_factor; - widths.push(width); - chunk - .stream( - instructions_stream_ref, - color_glyph.instructions.content.wait(), - ) - .filter(Filter::FlateDecode); - - // Use this stream as instructions to draw the glyph. - glyphs_to_instructions.push(instructions_stream_ref); - gids.push(color_glyph.gid); - } - - // Determine the base font name. - gids.sort(); - let base_font = base_font_name(&font_slice.font, &gids); - - // Write the Type3 font object. - let mut pdf_font = chunk.type3_font(subfont_id); - pdf_font.name(Name(base_font.as_bytes())); - pdf_font.pair(Name(b"Resources"), color_fonts.resources.reference); - pdf_font.bbox(color_font.bbox); - pdf_font.matrix([1.0 / scale_factor, 0.0, 0.0, 1.0 / scale_factor, 0.0, 0.0]); - pdf_font.first_char(0); - pdf_font.last_char((glyph_count - 1) as u8); - pdf_font.pair(Name(b"Widths"), widths_ref); - pdf_font.to_unicode(cmap_ref); - pdf_font.font_descriptor(descriptor_ref); - - // Write the /CharProcs dictionary, that maps glyph names to - // drawing instructions. - let mut char_procs = pdf_font.char_procs(); - for (gid, instructions_ref) in glyphs_to_instructions.iter().enumerate() { - char_procs - .pair(Name(eco_format!("glyph{gid}").as_bytes()), *instructions_ref); - } - char_procs.finish(); - - // Write the /Encoding dictionary. - let names = (0..glyph_count) - .map(|gid| eco_format!("glyph{gid}")) - .collect::>(); - pdf_font - .encoding_custom() - .differences() - .consecutive(0, names.iter().map(|name| Name(name.as_bytes()))); - pdf_font.finish(); - - // Encode a CMAP to make it possible to search or copy glyphs. - let glyph_set = resources.color_glyph_sets.get(&font_slice.font).unwrap(); - let mut cmap = UnicodeCmap::new(CMAP_NAME, SYSTEM_INFO); - for (index, glyph) in subset.iter().enumerate() { - let Some(text) = glyph_set.get(&glyph.gid) else { - continue; - }; - - if !text.is_empty() { - cmap.pair_with_multiple(index as u8, text.chars()); - } - } - chunk.cmap(cmap_ref, &cmap.finish()).writing_mode(WMode::Horizontal); - - // Write the font descriptor. - write_font_descriptor( - &mut chunk, - descriptor_ref, - &font_slice.font, - &base_font, - ); - - // Write the widths array - chunk.indirect(widths_ref).array().items(widths); - - out.insert(font_slice, subfont_id); - } - - Ok(()) - })?; - - Ok((chunk, out)) -} - -/// A mapping between `Font`s and all the corresponding `ColorFont`s. -/// -/// This mapping is one-to-many because there can only be 256 glyphs in a Type 3 -/// font, and fonts generally have more color glyphs than that. -pub struct ColorFontMap { - /// The mapping itself. - map: IndexMap, - /// The resources required to render the fonts in this map. - /// - /// For example, this can be the images for glyphs based on bitmaps or SVG. - pub resources: Resources, - /// The number of font slices (groups of 256 color glyphs), across all color - /// fonts. - total_slice_count: usize, -} - -/// A collection of Type3 font, belonging to the same TTF font. -pub struct ColorFont { - /// The IDs of each sub-slice of this font. They are the numbers after "Cf" - /// in the Resources dictionaries. - slice_ids: Vec, - /// The list of all color glyphs in this family. - /// - /// The index in this vector modulo 256 corresponds to the index in one of - /// the Type3 fonts in `refs` (the `n`-th in the vector, where `n` is the - /// quotient of the index divided by 256). - pub glyphs: Vec, - /// The global bounding box of the font. - pub bbox: Rect, - /// A mapping between glyph IDs and character indices in the `glyphs` - /// vector. - glyph_indices: HashMap, -} - -/// A single color glyph. -pub struct ColorGlyph { - /// The ID of the glyph. - pub gid: u16, - /// Instructions to draw the glyph. - pub instructions: content::Encoded, -} - -impl ColorFontMap<()> { - /// Creates a new empty mapping - pub fn new() -> Self { - Self { - map: IndexMap::new(), - total_slice_count: 0, - resources: Resources::default(), - } - } - - /// For a given glyph in a TTF font, give the ID of the Type3 font and the - /// index of the glyph inside of this Type3 font. - /// - /// If this is the first occurrence of this glyph in this font, it will - /// start its encoding and add it to the list of known glyphs. - pub fn get( - &mut self, - options: &PdfOptions, - text: &TextItemView, - glyph: &Glyph, - ) -> SourceResult<(usize, u8)> { - let font = &text.item.font; - let color_font = self.map.entry(font.clone()).or_insert_with(|| { - let global_bbox = font.ttf().global_bounding_box(); - let bbox = Rect::new( - font.to_em(global_bbox.x_min).to_font_units(), - font.to_em(global_bbox.y_min).to_font_units(), - font.to_em(global_bbox.x_max).to_font_units(), - font.to_em(global_bbox.y_max).to_font_units(), - ); - ColorFont { - bbox, - slice_ids: Vec::new(), - glyphs: Vec::new(), - glyph_indices: HashMap::new(), - } - }); - - Ok(if let Some(index_of_glyph) = color_font.glyph_indices.get(&glyph.id) { - // If we already know this glyph, return it. - (color_font.slice_ids[index_of_glyph / 256], *index_of_glyph as u8) - } else { - // Otherwise, allocate a new ColorGlyph in the font, and a new Type3 font - // if needed - let index = color_font.glyphs.len(); - if index % 256 == 0 { - color_font.slice_ids.push(self.total_slice_count); - self.total_slice_count += 1; - } - - let (frame, tofu) = glyph_frame(font, glyph.id); - if options.standards.pdfa && tofu { - bail!(failed_to_convert(text, glyph)); - } - - let width = font.advance(glyph.id).unwrap_or(Em::new(0.0)).get() - * font.units_per_em(); - let instructions = content::build( - options, - &mut self.resources, - &frame, - None, - Some(width as f32), - )?; - color_font.glyphs.push(ColorGlyph { gid: glyph.id, instructions }); - color_font.glyph_indices.insert(glyph.id, index); - - (color_font.slice_ids[index / 256], index as u8) - }) - } - - /// Assign references to the resource dictionary used by this set of color - /// fonts. - pub fn with_refs(self, refs: &ResourcesRefs) -> ColorFontMap { - ColorFontMap { - map: self.map, - resources: self.resources.with_refs(refs), - total_slice_count: self.total_slice_count, - } - } -} - -impl ColorFontMap { - /// Iterate over all Type3 fonts. - /// - /// Each item of this iterator maps to a Type3 font: it contains - /// at most 256 glyphs. A same TTF font can yield multiple Type3 fonts. - pub fn iter(&self) -> ColorFontMapIter<'_, R> { - ColorFontMapIter { map: self, font_index: 0, slice_index: 0 } - } -} - -/// Iterator over a [`ColorFontMap`]. -/// -/// See [`ColorFontMap::iter`]. -pub struct ColorFontMapIter<'a, R> { - /// The map over which to iterate - map: &'a ColorFontMap, - /// The index of TTF font on which we currently iterate - font_index: usize, - /// The sub-font (slice of at most 256 glyphs) at which we currently are. - slice_index: usize, -} - -impl<'a, R> Iterator for ColorFontMapIter<'a, R> { - type Item = (&'a ColorFont, ColorFontSlice); - - fn next(&mut self) -> Option { - let (font, color_font) = self.map.map.get_index(self.font_index)?; - let slice_count = (color_font.glyphs.len() / 256) + 1; - - if self.slice_index >= slice_count { - self.font_index += 1; - self.slice_index = 0; - return self.next(); - } - - let slice = ColorFontSlice { font: font.clone(), subfont: self.slice_index }; - self.slice_index += 1; - Some((color_font, slice)) - } -} - -/// A set of at most 256 glyphs (a limit imposed on Type3 fonts by the PDF -/// specification) that represents a part of a TTF font. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct ColorFontSlice { - /// The original TTF font. - pub font: Font, - /// The index of the Type3 font, among all those that are necessary to - /// represent the subset of the TTF font we are interested in. - pub subfont: usize, -} - -/// The error when the glyph could not be converted. -#[cold] -fn failed_to_convert(text: &TextItemView, glyph: &Glyph) -> SourceDiagnostic { - let mut diag = error!( - glyph.span.0, - "the glyph for {} could not be exported", - text.glyph_text(glyph).repr() - ); - - if text.item.font.ttf().tables().cff2.is_some() { - diag.hint("CFF2 fonts are not currently supported"); - } - - diag -} diff --git a/crates/typst-pdf/src/content.rs b/crates/typst-pdf/src/content.rs deleted file mode 100644 index 8b7517f51..000000000 --- a/crates/typst-pdf/src/content.rs +++ /dev/null @@ -1,823 +0,0 @@ -//! Generic writer for PDF content. -//! -//! It is used to write page contents, color glyph instructions, and tilings. -//! -//! See also [`pdf_writer::Content`]. - -use ecow::eco_format; -use pdf_writer::types::{ - ColorSpaceOperand, LineCapStyle, LineJoinStyle, TextRenderingMode, -}; -use pdf_writer::writers::PositionedItems; -use pdf_writer::{Content, Finish, Name, Rect, Str}; -use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult}; -use typst_library::foundations::Repr; -use typst_library::layout::{ - Abs, Em, Frame, FrameItem, GroupItem, Point, Ratio, Size, Transform, -}; -use typst_library::model::Destination; -use typst_library::text::color::should_outline; -use typst_library::text::{Font, Glyph, TextItem, TextItemView}; -use typst_library::visualize::{ - Curve, CurveItem, FillRule, FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, - Shape, -}; -use typst_syntax::Span; -use typst_utils::{Deferred, Numeric, SliceExt}; - -use crate::color::PaintEncode; -use crate::color_font::ColorFontMap; -use crate::extg::ExtGState; -use crate::image::deferred_image; -use crate::resources::Resources; -use crate::{deflate_deferred, AbsExt, ContentExt, EmExt, PdfOptions, StrExt}; - -/// Encode a [`Frame`] into a content stream. -/// -/// The resources that were used in the stream will be added to `resources`. -/// -/// `color_glyph_width` should be `None` unless the `Frame` represents a [color -/// glyph]. -/// -/// [color glyph]: `crate::color_font` -pub fn build( - options: &PdfOptions, - resources: &mut Resources<()>, - frame: &Frame, - fill: Option, - color_glyph_width: Option, -) -> SourceResult { - let size = frame.size(); - let mut ctx = Builder::new(options, resources, size); - - if let Some(width) = color_glyph_width { - ctx.content.start_color_glyph(width); - } - - // Make the coordinate system start at the top-left. - ctx.transform( - // Make the Y axis go upwards - Transform::scale(Ratio::one(), -Ratio::one()) - // Also move the origin to the top left corner - .post_concat(Transform::translate(Abs::zero(), size.y)), - ); - - if let Some(fill) = fill { - let shape = Geometry::Rect(frame.size()).filled(fill); - write_shape(&mut ctx, Point::zero(), &shape)?; - } - - // Encode the frame into the content stream. - write_frame(&mut ctx, frame)?; - - Ok(Encoded { - size, - content: deflate_deferred(ctx.content.finish()), - uses_opacities: ctx.uses_opacities, - links: ctx.links, - }) -} - -/// An encoded content stream. -pub struct Encoded { - /// The dimensions of the content. - pub size: Size, - /// The actual content stream. - pub content: Deferred>, - /// Whether the content opacities. - pub uses_opacities: bool, - /// Links in the PDF coordinate system. - pub links: Vec<(Destination, Rect)>, -} - -/// An exporter for a single PDF content stream. -/// -/// Content streams are a series of PDF commands. They can reference external -/// objects only through resources. -/// -/// Content streams can be used for page contents, but also to describe color -/// glyphs and tilings. -pub struct Builder<'a, R = ()> { - /// Settings for PDF export. - pub(crate) options: &'a PdfOptions<'a>, - /// A list of all resources that are used in the content stream. - pub(crate) resources: &'a mut Resources, - /// The PDF content stream that is being built. - pub content: Content, - /// Current graphic state. - state: State, - /// Stack of saved graphic states. - saves: Vec, - /// Whether any stroke or fill was not totally opaque. - uses_opacities: bool, - /// All clickable links that are present in this content. - links: Vec<(Destination, Rect)>, -} - -impl<'a, R> Builder<'a, R> { - /// Create a new content builder. - pub fn new( - options: &'a PdfOptions<'a>, - resources: &'a mut Resources, - size: Size, - ) -> Self { - Builder { - options, - resources, - uses_opacities: false, - content: Content::new(), - state: State::new(size), - saves: vec![], - links: vec![], - } - } -} - -/// A simulated graphics state used to deduplicate graphics state changes and -/// keep track of the current transformation matrix for link annotations. -#[derive(Debug, Clone)] -struct State { - /// The transform of the current item. - transform: Transform, - /// The transform of first hard frame in the hierarchy. - container_transform: Transform, - /// The size of the first hard frame in the hierarchy. - size: Size, - /// The current font. - font: Option<(Font, Abs)>, - /// The current fill paint. - fill: Option, - /// The color space of the current fill paint. - fill_space: Option>, - /// The current external graphic state. - external_graphics_state: ExtGState, - /// The current stroke paint. - stroke: Option, - /// The color space of the current stroke paint. - stroke_space: Option>, - /// The current text rendering mode. - text_rendering_mode: TextRenderingMode, -} - -impl State { - /// Creates a new, clean state for a given `size`. - pub fn new(size: Size) -> Self { - Self { - transform: Transform::identity(), - container_transform: Transform::identity(), - size, - font: None, - fill: None, - fill_space: None, - external_graphics_state: ExtGState::default(), - stroke: None, - stroke_space: None, - text_rendering_mode: TextRenderingMode::Fill, - } - } - - /// Creates the [`Transforms`] structure for the current item. - pub fn transforms(&self, size: Size, pos: Point) -> Transforms { - Transforms { - transform: self.transform.pre_concat(Transform::translate(pos.x, pos.y)), - container_transform: self.container_transform, - container_size: self.size, - size, - } - } -} - -/// Subset of the state used to calculate the transform of gradients and tilings. -#[derive(Debug, Clone, Copy)] -pub(super) struct Transforms { - /// The transform of the current item. - pub transform: Transform, - /// The transform of first hard frame in the hierarchy. - pub container_transform: Transform, - /// The size of the first hard frame in the hierarchy. - pub container_size: Size, - /// The size of the item. - pub size: Size, -} - -impl Builder<'_, ()> { - fn save_state(&mut self) -> SourceResult<()> { - self.saves.push(self.state.clone()); - self.content.save_state_checked() - } - - fn restore_state(&mut self) { - self.content.restore_state(); - self.state = self.saves.pop().expect("missing state save"); - } - - fn set_external_graphics_state(&mut self, graphics_state: &ExtGState) { - let current_state = &self.state.external_graphics_state; - if current_state != graphics_state { - let index = self.resources.ext_gs.insert(*graphics_state); - let name = eco_format!("Gs{index}"); - self.content.set_parameters(Name(name.as_bytes())); - - self.state.external_graphics_state = *graphics_state; - if graphics_state.uses_opacities() { - self.uses_opacities = true; - } - } - } - - fn set_opacities(&mut self, stroke: Option<&FixedStroke>, fill: Option<&Paint>) { - let get_opacity = |paint: &Paint| { - let color = match paint { - Paint::Solid(color) => *color, - Paint::Gradient(_) | Paint::Tiling(_) => return 255, - }; - - color.alpha().map_or(255, |v| (v * 255.0).round() as u8) - }; - - let stroke_opacity = stroke.map_or(255, |stroke| get_opacity(&stroke.paint)); - let fill_opacity = fill.map_or(255, get_opacity); - self.set_external_graphics_state(&ExtGState { stroke_opacity, fill_opacity }); - } - - fn reset_opacities(&mut self) { - self.set_external_graphics_state(&ExtGState { - stroke_opacity: 255, - fill_opacity: 255, - }); - } - - pub fn transform(&mut self, transform: Transform) { - let Transform { sx, ky, kx, sy, tx, ty } = transform; - self.state.transform = self.state.transform.pre_concat(transform); - if self.state.container_transform.is_identity() { - self.state.container_transform = self.state.transform; - } - self.content.transform([ - sx.get() as _, - ky.get() as _, - kx.get() as _, - sy.get() as _, - tx.to_f32(), - ty.to_f32(), - ]); - } - - fn group_transform(&mut self, transform: Transform) { - self.state.container_transform = - self.state.container_transform.pre_concat(transform); - } - - fn set_font(&mut self, font: &Font, size: Abs) { - if self.state.font.as_ref().map(|(f, s)| (f, *s)) != Some((font, size)) { - let index = self.resources.fonts.insert(font.clone()); - let name = eco_format!("F{index}"); - self.content.set_font(Name(name.as_bytes()), size.to_f32()); - self.state.font = Some((font.clone(), size)); - } - } - - fn size(&mut self, size: Size) { - self.state.size = size; - } - - fn set_fill( - &mut self, - fill: &Paint, - on_text: bool, - transforms: Transforms, - ) -> SourceResult<()> { - if self.state.fill.as_ref() != Some(fill) - || matches!(self.state.fill, Some(Paint::Gradient(_))) - { - fill.set_as_fill(self, on_text, transforms)?; - self.state.fill = Some(fill.clone()); - } - Ok(()) - } - - pub fn set_fill_color_space(&mut self, space: Name<'static>) { - if self.state.fill_space != Some(space) { - self.content.set_fill_color_space(ColorSpaceOperand::Named(space)); - self.state.fill_space = Some(space); - } - } - - pub fn reset_fill_color_space(&mut self) { - self.state.fill_space = None; - } - - fn set_stroke( - &mut self, - stroke: &FixedStroke, - on_text: bool, - transforms: Transforms, - ) -> SourceResult<()> { - if self.state.stroke.as_ref() != Some(stroke) - || matches!( - self.state.stroke.as_ref().map(|s| &s.paint), - Some(Paint::Gradient(_)) - ) - { - let FixedStroke { paint, thickness, cap, join, dash, miter_limit } = stroke; - paint.set_as_stroke(self, on_text, transforms)?; - - self.content.set_line_width(thickness.to_f32()); - if self.state.stroke.as_ref().map(|s| &s.cap) != Some(cap) { - self.content.set_line_cap(to_pdf_line_cap(*cap)); - } - if self.state.stroke.as_ref().map(|s| &s.join) != Some(join) { - self.content.set_line_join(to_pdf_line_join(*join)); - } - if self.state.stroke.as_ref().map(|s| &s.dash) != Some(dash) { - if let Some(dash) = dash { - self.content.set_dash_pattern( - dash.array.iter().map(|l| l.to_f32()), - dash.phase.to_f32(), - ); - } else { - self.content.set_dash_pattern([], 0.0); - } - } - if self.state.stroke.as_ref().map(|s| &s.miter_limit) != Some(miter_limit) { - self.content.set_miter_limit(miter_limit.get() as f32); - } - self.state.stroke = Some(stroke.clone()); - } - - Ok(()) - } - - pub fn set_stroke_color_space(&mut self, space: Name<'static>) { - if self.state.stroke_space != Some(space) { - self.content.set_stroke_color_space(ColorSpaceOperand::Named(space)); - self.state.stroke_space = Some(space); - } - } - - pub fn reset_stroke_color_space(&mut self) { - self.state.stroke_space = None; - } - - fn set_text_rendering_mode(&mut self, mode: TextRenderingMode) { - if self.state.text_rendering_mode != mode { - self.content.set_text_rendering_mode(mode); - self.state.text_rendering_mode = mode; - } - } -} - -/// Encode a frame into the content stream. -pub(crate) fn write_frame(ctx: &mut Builder, frame: &Frame) -> SourceResult<()> { - for &(pos, ref item) in frame.items() { - let x = pos.x.to_f32(); - let y = pos.y.to_f32(); - match item { - FrameItem::Group(group) => write_group(ctx, pos, group)?, - FrameItem::Text(text) => write_text(ctx, pos, text)?, - FrameItem::Shape(shape, _) => write_shape(ctx, pos, shape)?, - FrameItem::Image(image, size, span) => { - write_image(ctx, x, y, image, *size, *span)? - } - FrameItem::Link(dest, size) => write_link(ctx, pos, dest, *size), - FrameItem::Tag(_) => {} - } - } - Ok(()) -} - -/// Encode a group into the content stream. -fn write_group(ctx: &mut Builder, pos: Point, group: &GroupItem) -> SourceResult<()> { - let translation = Transform::translate(pos.x, pos.y); - - ctx.save_state()?; - - if group.frame.kind().is_hard() { - ctx.group_transform( - ctx.state - .transform - .post_concat(ctx.state.container_transform.invert().unwrap()) - .pre_concat(translation) - .pre_concat(group.transform), - ); - ctx.size(group.frame.size()); - } - - ctx.transform(translation.pre_concat(group.transform)); - if let Some(clip_curve) = &group.clip { - write_curve(ctx, 0.0, 0.0, clip_curve); - ctx.content.clip_nonzero(); - ctx.content.end_path(); - } - - write_frame(ctx, &group.frame)?; - ctx.restore_state(); - - Ok(()) -} - -/// Encode a text run into the content stream. -fn write_text(ctx: &mut Builder, pos: Point, text: &TextItem) -> SourceResult<()> { - if ctx.options.standards.pdfa && text.font.info().is_last_resort() { - bail!( - Span::find(text.glyphs.iter().map(|g| g.span.0)), - "the text {} could not be displayed with any font", - &text.text, - ); - } - - let outline_glyphs = - text.glyphs.iter().filter(|g| should_outline(&text.font, g)).count(); - - if outline_glyphs == text.glyphs.len() { - write_normal_text(ctx, pos, TextItemView::full(text))?; - } else if outline_glyphs == 0 { - write_complex_glyphs(ctx, pos, TextItemView::full(text))?; - } else { - // Otherwise we need to split it into smaller text runs. - let mut offset = 0; - let mut position_in_run = Abs::zero(); - for (should_outline, sub_run) in - text.glyphs.group_by_key(|g| should_outline(&text.font, g)) - { - let end = offset + sub_run.len(); - - // Build a sub text-run - let text_item_view = TextItemView::from_glyph_range(text, offset..end); - - // Adjust the position of the run on the line - let pos = pos + Point::new(position_in_run, Abs::zero()); - position_in_run += text_item_view.width(); - offset = end; - - // Actually write the sub text-run. - if should_outline { - write_normal_text(ctx, pos, text_item_view)?; - } else { - write_complex_glyphs(ctx, pos, text_item_view)?; - } - } - } - - Ok(()) -} - -/// Encodes a text run (without any color glyph) into the content stream. -fn write_normal_text( - ctx: &mut Builder, - pos: Point, - text: TextItemView, -) -> SourceResult<()> { - let x = pos.x.to_f32(); - let y = pos.y.to_f32(); - - *ctx.resources.languages.entry(text.item.lang).or_insert(0) += text.glyph_range.len(); - - let glyph_set = ctx.resources.glyph_sets.entry(text.item.font.clone()).or_default(); - for g in text.glyphs() { - glyph_set.entry(g.id).or_insert_with(|| text.glyph_text(g)); - } - - let fill_transform = ctx.state.transforms(Size::zero(), pos); - ctx.set_fill(&text.item.fill, true, fill_transform)?; - - let stroke = text.item.stroke.as_ref().and_then(|stroke| { - if stroke.thickness.to_f32() > 0.0 { - Some(stroke) - } else { - None - } - }); - - if let Some(stroke) = stroke { - ctx.set_stroke(stroke, true, fill_transform)?; - ctx.set_text_rendering_mode(TextRenderingMode::FillStroke); - } else { - ctx.set_text_rendering_mode(TextRenderingMode::Fill); - } - - ctx.set_font(&text.item.font, text.item.size); - ctx.set_opacities(text.item.stroke.as_ref(), Some(&text.item.fill)); - ctx.content.begin_text(); - - // Position the text. - ctx.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]); - - let mut positioned = ctx.content.show_positioned(); - let mut items = positioned.items(); - let mut adjustment = Em::zero(); - let mut encoded = vec![]; - - let glyph_remapper = ctx - .resources - .glyph_remappers - .entry(text.item.font.clone()) - .or_default(); - - // Write the glyphs with kerning adjustments. - for glyph in text.glyphs() { - if ctx.options.standards.pdfa && glyph.id == 0 { - bail!(tofu(&text, glyph)); - } - - adjustment += glyph.x_offset; - - if !adjustment.is_zero() { - if !encoded.is_empty() { - show_text(&mut items, &encoded); - encoded.clear(); - } - - items.adjust(-adjustment.to_font_units()); - adjustment = Em::zero(); - } - - // In PDF, we use CIDs to index the glyphs in a font, not GIDs. What a - // CID actually refers to depends on the type of font we are embedding: - // - // - For TrueType fonts, the CIDs are defined by an external mapping. - // - For SID-keyed CFF fonts, the CID is the same as the GID in the font. - // - For CID-keyed CFF fonts, the CID refers to the CID in the font. - // - // (See in the PDF-spec for more details on this.) - // - // However, in our case: - // - We use the identity-mapping for TrueType fonts. - // - SID-keyed fonts will get converted into CID-keyed fonts by the - // subsetter. - // - CID-keyed fonts will be rewritten in a way so that the mapping - // between CID and GID is always the identity mapping, regardless of - // the mapping before. - // - // Because of this, we can always use the remapped GID as the CID, - // regardless of which type of font we are actually embedding. - let cid = glyph_remapper.remap(glyph.id); - encoded.push((cid >> 8) as u8); - encoded.push((cid & 0xff) as u8); - - if let Some(advance) = text.item.font.advance(glyph.id) { - adjustment += glyph.x_advance - advance; - } - - adjustment -= glyph.x_offset; - } - - if !encoded.is_empty() { - show_text(&mut items, &encoded); - } - - items.finish(); - positioned.finish(); - ctx.content.end_text(); - - Ok(()) -} - -/// Shows text, ensuring that each individual string doesn't exceed the -/// implementation limits. -fn show_text(items: &mut PositionedItems, encoded: &[u8]) { - for chunk in encoded.chunks(Str::PDFA_LIMIT) { - items.show(Str(chunk)); - } -} - -/// Encodes a text run made only of color glyphs into the content stream -fn write_complex_glyphs( - ctx: &mut Builder, - pos: Point, - text: TextItemView, -) -> SourceResult<()> { - let x = pos.x.to_f32(); - let y = pos.y.to_f32(); - - let mut last_font = None; - - ctx.reset_opacities(); - - ctx.content.begin_text(); - ctx.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]); - // So that the next call to ctx.set_font() will change the font to one that - // displays regular glyphs and not color glyphs. - ctx.state.font = None; - - let glyph_set = ctx - .resources - .color_glyph_sets - .entry(text.item.font.clone()) - .or_default(); - - for glyph in text.glyphs() { - if ctx.options.standards.pdfa && glyph.id == 0 { - bail!(tofu(&text, glyph)); - } - - // Retrieve the Type3 font reference and the glyph index in the font. - let color_fonts = ctx - .resources - .color_fonts - .get_or_insert_with(|| Box::new(ColorFontMap::new())); - - let (font, index) = color_fonts.get(ctx.options, &text, glyph)?; - - if last_font != Some(font) { - ctx.content.set_font( - Name(eco_format!("Cf{}", font).as_bytes()), - text.item.size.to_f32(), - ); - last_font = Some(font); - } - - ctx.content.show(Str(&[index])); - - glyph_set.entry(glyph.id).or_insert_with(|| text.glyph_text(glyph)); - } - ctx.content.end_text(); - - Ok(()) -} - -/// Encode a geometrical shape into the content stream. -fn write_shape(ctx: &mut Builder, pos: Point, shape: &Shape) -> SourceResult<()> { - let x = pos.x.to_f32(); - let y = pos.y.to_f32(); - - let stroke = shape.stroke.as_ref().and_then(|stroke| { - if stroke.thickness.to_f32() > 0.0 { - Some(stroke) - } else { - None - } - }); - - if shape.fill.is_none() && stroke.is_none() { - return Ok(()); - } - - if let Some(fill) = &shape.fill { - ctx.set_fill(fill, false, ctx.state.transforms(shape.geometry.bbox_size(), pos))?; - } - - if let Some(stroke) = stroke { - ctx.set_stroke( - stroke, - false, - ctx.state.transforms(shape.geometry.bbox_size(), pos), - )?; - } - - ctx.set_opacities(stroke, shape.fill.as_ref()); - - match &shape.geometry { - Geometry::Line(target) => { - let dx = target.x.to_f32(); - let dy = target.y.to_f32(); - ctx.content.move_to(x, y); - ctx.content.line_to(x + dx, y + dy); - } - Geometry::Rect(size) => { - let w = size.x.to_f32(); - let h = size.y.to_f32(); - if w.abs() > f32::EPSILON && h.abs() > f32::EPSILON { - ctx.content.rect(x, y, w, h); - } - } - Geometry::Curve(curve) => { - write_curve(ctx, x, y, curve); - } - } - - match (&shape.fill, &shape.fill_rule, stroke) { - (None, _, None) => unreachable!(), - (Some(_), FillRule::NonZero, None) => ctx.content.fill_nonzero(), - (Some(_), FillRule::EvenOdd, None) => ctx.content.fill_even_odd(), - (None, _, Some(_)) => ctx.content.stroke(), - (Some(_), FillRule::NonZero, Some(_)) => ctx.content.fill_nonzero_and_stroke(), - (Some(_), FillRule::EvenOdd, Some(_)) => ctx.content.fill_even_odd_and_stroke(), - }; - - Ok(()) -} - -/// Encode a curve into the content stream. -fn write_curve(ctx: &mut Builder, x: f32, y: f32, curve: &Curve) { - for elem in &curve.0 { - match elem { - CurveItem::Move(p) => ctx.content.move_to(x + p.x.to_f32(), y + p.y.to_f32()), - CurveItem::Line(p) => ctx.content.line_to(x + p.x.to_f32(), y + p.y.to_f32()), - CurveItem::Cubic(p1, p2, p3) => ctx.content.cubic_to( - x + p1.x.to_f32(), - y + p1.y.to_f32(), - x + p2.x.to_f32(), - y + p2.y.to_f32(), - x + p3.x.to_f32(), - y + p3.y.to_f32(), - ), - CurveItem::Close => ctx.content.close_path(), - }; - } -} - -/// Encode a vector or raster image into the content stream. -fn write_image( - ctx: &mut Builder, - x: f32, - y: f32, - image: &Image, - size: Size, - span: Span, -) -> SourceResult<()> { - let index = ctx.resources.images.insert(image.clone()); - ctx.resources.deferred_images.entry(index).or_insert_with(|| { - let (image, color_space) = - deferred_image(image.clone(), ctx.options.standards.pdfa); - if let Some(color_space) = color_space { - ctx.resources.colors.mark_as_used(color_space); - } - (image, span) - }); - - ctx.reset_opacities(); - - let name = eco_format!("Im{index}"); - let w = size.x.to_f32(); - let h = size.y.to_f32(); - ctx.content.save_state_checked()?; - ctx.content.transform([w, 0.0, 0.0, -h, x, y + h]); - - if let Some(alt) = image.alt() { - if ctx.options.standards.pdfa && alt.len() > Str::PDFA_LIMIT { - bail!(span, "the image's alt text is too long"); - } - - let mut image_span = - ctx.content.begin_marked_content_with_properties(Name(b"Span")); - let mut image_alt = image_span.properties(); - image_alt.pair(Name(b"Alt"), Str(alt.as_bytes())); - image_alt.finish(); - image_span.finish(); - - ctx.content.x_object(Name(name.as_bytes())); - ctx.content.end_marked_content(); - } else { - ctx.content.x_object(Name(name.as_bytes())); - } - - ctx.content.restore_state(); - Ok(()) -} - -/// Save a link for later writing in the annotations dictionary. -fn write_link(ctx: &mut Builder, pos: Point, dest: &Destination, size: Size) { - let mut min_x = Abs::inf(); - let mut min_y = Abs::inf(); - let mut max_x = -Abs::inf(); - let mut max_y = -Abs::inf(); - - // Compute the bounding box of the transformed link. - for point in [ - pos, - pos + Point::with_x(size.x), - pos + Point::with_y(size.y), - pos + size.to_point(), - ] { - let t = point.transform(ctx.state.transform); - min_x.set_min(t.x); - min_y.set_min(t.y); - max_x.set_max(t.x); - max_y.set_max(t.y); - } - - let x1 = min_x.to_f32(); - let x2 = max_x.to_f32(); - let y1 = max_y.to_f32(); - let y2 = min_y.to_f32(); - let rect = Rect::new(x1, y1, x2, y2); - - ctx.links.push((dest.clone(), rect)); -} - -fn to_pdf_line_cap(cap: LineCap) -> LineCapStyle { - match cap { - LineCap::Butt => LineCapStyle::ButtCap, - LineCap::Round => LineCapStyle::RoundCap, - LineCap::Square => LineCapStyle::ProjectingSquareCap, - } -} - -fn to_pdf_line_join(join: LineJoin) -> LineJoinStyle { - match join { - LineJoin::Miter => LineJoinStyle::MiterJoin, - LineJoin::Round => LineJoinStyle::RoundJoin, - LineJoin::Bevel => LineJoinStyle::BevelJoin, - } -} - -/// The error when there is a tofu glyph. -#[cold] -fn tofu(text: &TextItemView, glyph: &Glyph) -> SourceDiagnostic { - error!( - glyph.span.0, - "the text {} could not be displayed with any font", - text.glyph_text(glyph).repr(), - ) -} diff --git a/crates/typst-pdf/src/convert.rs b/crates/typst-pdf/src/convert.rs new file mode 100644 index 000000000..f5ca31730 --- /dev/null +++ b/crates/typst-pdf/src/convert.rs @@ -0,0 +1,661 @@ +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::num::NonZeroU64; + +use ecow::{eco_format, EcoVec}; +use krilla::annotation::Annotation; +use krilla::configure::{Configuration, ValidationError, Validator}; +use krilla::destination::{NamedDestination, XyzDestination}; +use krilla::embed::EmbedError; +use krilla::error::KrillaError; +use krilla::geom::PathBuilder; +use krilla::page::{PageLabel, PageSettings}; +use krilla::surface::Surface; +use krilla::{Document, SerializeSettings}; +use krilla_svg::render_svg_glyph; +use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult}; +use typst_library::foundations::NativeElement; +use typst_library::introspection::Location; +use typst_library::layout::{ + Abs, Frame, FrameItem, GroupItem, PagedDocument, Size, Transform, +}; +use typst_library::model::HeadingElem; +use typst_library::text::{Font, Lang}; +use typst_library::visualize::{Geometry, Paint}; +use typst_syntax::Span; + +use crate::embed::embed_files; +use crate::image::handle_image; +use crate::link::handle_link; +use crate::metadata::build_metadata; +use crate::outline::build_outline; +use crate::page::PageLabelExt; +use crate::shape::handle_shape; +use crate::text::handle_text; +use crate::util::{convert_path, display_font, AbsExt, TransformExt}; +use crate::PdfOptions; + +#[typst_macros::time(name = "convert document")] +pub fn convert( + typst_document: &PagedDocument, + options: &PdfOptions, +) -> SourceResult> { + let settings = SerializeSettings { + compress_content_streams: true, + no_device_cs: true, + ascii_compatible: false, + xmp_metadata: true, + cmyk_profile: None, + configuration: options.standards.config, + enable_tagging: false, + render_svg_glyph_fn: render_svg_glyph, + }; + + let mut document = Document::new_with(settings); + let page_index_converter = PageIndexConverter::new(typst_document, options); + let named_destinations = + collect_named_destinations(typst_document, &page_index_converter); + let mut gc = GlobalContext::new( + typst_document, + options, + named_destinations, + page_index_converter, + ); + + convert_pages(&mut gc, &mut document)?; + embed_files(typst_document, &mut document)?; + + document.set_outline(build_outline(&gc)); + document.set_metadata(build_metadata(&gc)); + + finish(document, gc, options.standards.config) +} + +fn convert_pages(gc: &mut GlobalContext, document: &mut Document) -> SourceResult<()> { + for (i, typst_page) in gc.document.pages.iter().enumerate() { + if gc.page_index_converter.pdf_page_index(i).is_none() { + // Don't export this page. + continue; + } else { + let mut settings = PageSettings::new( + typst_page.frame.width().to_f32(), + typst_page.frame.height().to_f32(), + ); + + if let Some(label) = typst_page + .numbering + .as_ref() + .and_then(|num| PageLabel::generate(num, typst_page.number)) + .or_else(|| { + // When some pages were ignored from export, we show a page label with + // the correct real (not logical) page number. + // This is for consistency with normal output when pages have no numbering + // and all are exported: the final PDF page numbers always correspond to + // the real (not logical) page numbers. Here, the final PDF page number + // will differ, but we can at least use labels to indicate what was + // the corresponding real page number in the Typst document. + gc.page_index_converter + .has_skipped_pages() + .then(|| PageLabel::arabic((i + 1) as u64)) + }) + { + settings = settings.with_page_label(label); + } + + let mut page = document.start_page_with(settings); + let mut surface = page.surface(); + let mut fc = FrameContext::new(typst_page.frame.size()); + + handle_frame( + &mut fc, + &typst_page.frame, + typst_page.fill_or_transparent(), + &mut surface, + gc, + )?; + + surface.finish(); + + for annotation in fc.annotations { + page.add_annotation(annotation); + } + } + } + + Ok(()) +} + +/// A state allowing us to keep track of transforms and container sizes, +/// which is mainly needed to resolve gradients and patterns correctly. +#[derive(Debug, Clone)] +pub(crate) struct State { + /// The current transform. + transform: Transform, + /// The transform of first hard frame in the hierarchy. + container_transform: Transform, + /// The size of the first hard frame in the hierarchy. + container_size: Size, +} + +impl State { + /// Creates a new, clean state for a given `size`. + fn new(size: Size) -> Self { + Self { + transform: Transform::identity(), + container_transform: Transform::identity(), + container_size: size, + } + } + + pub(crate) fn register_container(&mut self, size: Size) { + self.container_transform = self.transform; + self.container_size = size; + } + + pub(crate) fn pre_concat(&mut self, transform: Transform) { + self.transform = self.transform.pre_concat(transform); + } + + pub(crate) fn transform(&self) -> Transform { + self.transform + } + + pub(crate) fn container_transform(&self) -> Transform { + self.container_transform + } + + pub(crate) fn container_size(&self) -> Size { + self.container_size + } +} + +/// Context needed for converting a single frame. +pub(crate) struct FrameContext { + states: Vec, + annotations: Vec, +} + +impl FrameContext { + pub(crate) fn new(size: Size) -> Self { + Self { + states: vec![State::new(size)], + annotations: vec![], + } + } + + pub(crate) fn push(&mut self) { + self.states.push(self.states.last().unwrap().clone()); + } + + pub(crate) fn pop(&mut self) { + self.states.pop(); + } + + pub(crate) fn state(&self) -> &State { + self.states.last().unwrap() + } + + pub(crate) fn state_mut(&mut self) -> &mut State { + self.states.last_mut().unwrap() + } + + pub(crate) fn push_annotation(&mut self, annotation: Annotation) { + self.annotations.push(annotation); + } +} + +/// Globally needed context for converting a typst document. +pub(crate) struct GlobalContext<'a> { + /// Cache the conversion between krilla and Typst fonts (forward and backward). + pub(crate) fonts_forward: HashMap, + pub(crate) fonts_backward: HashMap, + /// Mapping between images and their span. + // Note: In theory, the same image can have multiple spans + // if it appears in the document multiple times. We just store the + // first appearance, though. + pub(crate) image_to_spans: HashMap, + /// The spans of all images that appear in the document. We use this so + /// we can give more accurate error messages. + pub(crate) image_spans: HashSet, + /// The document to convert. + pub(crate) document: &'a PagedDocument, + /// Options for PDF export. + pub(crate) options: &'a PdfOptions<'a>, + /// Mapping between locations in the document and named destinations. + pub(crate) loc_to_names: HashMap, + /// The languages used throughout the document. + pub(crate) languages: BTreeMap, + pub(crate) page_index_converter: PageIndexConverter, +} + +impl<'a> GlobalContext<'a> { + pub(crate) fn new( + document: &'a PagedDocument, + options: &'a PdfOptions, + loc_to_names: HashMap, + page_index_converter: PageIndexConverter, + ) -> GlobalContext<'a> { + Self { + fonts_forward: HashMap::new(), + fonts_backward: HashMap::new(), + document, + options, + loc_to_names, + image_to_spans: HashMap::new(), + image_spans: HashSet::new(), + languages: BTreeMap::new(), + page_index_converter, + } + } +} + +#[typst_macros::time(name = "handle page")] +pub(crate) fn handle_frame( + fc: &mut FrameContext, + frame: &Frame, + fill: Option, + surface: &mut Surface, + gc: &mut GlobalContext, +) -> SourceResult<()> { + fc.push(); + + if frame.kind().is_hard() { + fc.state_mut().register_container(frame.size()); + } + + if let Some(fill) = fill { + let shape = Geometry::Rect(frame.size()).filled(fill); + handle_shape(fc, &shape, surface, gc, Span::detached())?; + } + + for (point, item) in frame.items() { + fc.push(); + fc.state_mut().pre_concat(Transform::translate(point.x, point.y)); + + match item { + FrameItem::Group(g) => handle_group(fc, g, surface, gc)?, + FrameItem::Text(t) => handle_text(fc, t, surface, gc)?, + FrameItem::Shape(s, span) => handle_shape(fc, s, surface, gc, *span)?, + FrameItem::Image(image, size, span) => { + handle_image(gc, fc, image, *size, surface, *span)? + } + FrameItem::Link(d, s) => handle_link(fc, gc, d, *s), + FrameItem::Tag(_) => {} + } + + fc.pop(); + } + + fc.pop(); + + Ok(()) +} + +pub(crate) fn handle_group( + fc: &mut FrameContext, + group: &GroupItem, + surface: &mut Surface, + context: &mut GlobalContext, +) -> SourceResult<()> { + fc.push(); + fc.state_mut().pre_concat(group.transform); + + let clip_path = group + .clip + .as_ref() + .and_then(|p| { + let mut builder = PathBuilder::new(); + convert_path(p, &mut builder); + builder.finish() + }) + .and_then(|p| p.transform(fc.state().transform.to_krilla())); + + if let Some(clip_path) = &clip_path { + surface.push_clip_path(clip_path, &krilla::paint::FillRule::NonZero); + } + + handle_frame(fc, &group.frame, None, surface, context)?; + + if clip_path.is_some() { + surface.pop(); + } + + fc.pop(); + + Ok(()) +} + +#[typst_macros::time(name = "finish export")] +/// Finish a krilla document and handle export errors. +fn finish( + document: Document, + gc: GlobalContext, + configuration: Configuration, +) -> SourceResult> { + let validator = configuration.validator(); + + match document.finish() { + Ok(r) => Ok(r), + Err(e) => match e { + KrillaError::Font(f, s) => { + let font_str = display_font(gc.fonts_backward.get(&f).unwrap()); + bail!( + Span::detached(), + "failed to process font {font_str}: {s}"; + hint: "make sure the font is valid"; + hint: "the used font might be unsupported by Typst" + ); + } + KrillaError::Validation(ve) => { + let errors = ve + .iter() + .map(|e| convert_error(&gc, validator, e)) + .collect::>(); + Err(errors) + } + KrillaError::Image(_, loc) => { + let span = to_span(loc); + bail!(span, "failed to process image"); + } + KrillaError::SixteenBitImage(image, _) => { + let span = gc.image_to_spans.get(&image).unwrap(); + bail!( + *span, "16 bit images are not supported in this export mode"; + hint: "convert the image to 8 bit instead" + ) + } + }, + } +} + +/// Converts a krilla error into a Typst error. +fn convert_error( + gc: &GlobalContext, + validator: Validator, + error: &ValidationError, +) -> SourceDiagnostic { + let prefix = eco_format!("{} error:", validator.as_str()); + match error { + ValidationError::TooLongString => error!( + Span::detached(), + "{prefix} a PDF string is longer than 32767 characters"; + hint: "ensure title and author names are short enough" + ), + // Should in theory never occur, as krilla always trims font names. + ValidationError::TooLongName => error!( + Span::detached(), + "{prefix} a PDF name is longer than 127 characters"; + hint: "perhaps a font name is too long" + ), + + ValidationError::TooLongArray => error!( + Span::detached(), + "{prefix} a PDF array is longer than 8191 elements"; + hint: "this can happen if you have a very long text in a single line" + ), + ValidationError::TooLongDictionary => error!( + Span::detached(), + "{prefix} a PDF dictionary has more than 4095 entries"; + hint: "try reducing the complexity of your document" + ), + ValidationError::TooLargeFloat => error!( + Span::detached(), + "{prefix} a PDF floating point number is larger than the allowed limit"; + hint: "try exporting with a higher PDF version" + ), + ValidationError::TooManyIndirectObjects => error!( + Span::detached(), + "{prefix} the PDF has too many indirect objects"; + hint: "reduce the size of your document" + ), + // Can only occur if we have 27+ nested clip paths + ValidationError::TooHighQNestingLevel => error!( + Span::detached(), + "{prefix} the PDF has too high q nesting"; + hint: "reduce the number of nested containers" + ), + ValidationError::ContainsPostScript(loc) => error!( + to_span(*loc), + "{prefix} the PDF contains PostScript code"; + hint: "conic gradients are not supported in this PDF standard" + ), + ValidationError::MissingCMYKProfile => error!( + Span::detached(), + "{prefix} the PDF is missing a CMYK profile"; + hint: "CMYK colors are not yet supported in this export mode" + ), + ValidationError::ContainsNotDefGlyph(f, loc, text) => error!( + to_span(*loc), + "{prefix} the text '{text}' cannot be displayed using {}", + display_font(gc.fonts_backward.get(f).unwrap()); + hint: "try using a different font" + ), + ValidationError::InvalidCodepointMapping(_, _, cp, loc) => { + if let Some(c) = cp.map(|c| eco_format!("{:#06x}", c as u32)) { + let msg = if loc.is_some() { + "the PDF contains text with" + } else { + "the text contains" + }; + error!(to_span(*loc), "{prefix} {msg} the disallowed codepoint {c}") + } else { + // I think this code path is in theory unreachable, + // but just to be safe. + let msg = if loc.is_some() { + "the PDF contains text with missing codepoints" + } else { + "the text was not mapped to a code point" + }; + error!( + to_span(*loc), + "{prefix} {msg}"; + hint: "for complex scripts like Arabic, it might not be \ + possible to produce a compliant document" + ) + } + } + ValidationError::UnicodePrivateArea(_, _, c, loc) => { + let code_point = eco_format!("{:#06x}", *c as u32); + let msg = if loc.is_some() { "the PDF" } else { "the text" }; + error!( + to_span(*loc), + "{prefix} {msg} contains the codepoint {code_point}"; + hint: "codepoints from the Unicode private area are \ + forbidden in this export mode" + ) + } + ValidationError::Transparency(loc) => { + let span = to_span(*loc); + let hint1 = "try exporting with a different standard that \ + supports transparency"; + if loc.is_some() { + if gc.image_spans.contains(&span) { + error!( + span, "{prefix} the image contains transparency"; + hint: "{hint1}"; + hint: "or convert the image to a non-transparent one"; + hint: "you might have to convert SVGs into \ + non-transparent bitmap images" + ) + } else { + error!( + span, "{prefix} the used fill or stroke has transparency"; + hint: "{hint1}"; + hint: "or don't use colors with transparency in \ + this export mode" + ) + } + } else { + error!( + span, "{prefix} the PDF contains transparency"; + hint: "{hint1}" + ) + } + } + ValidationError::ImageInterpolation(loc) => { + let span = to_span(*loc); + if loc.is_some() { + error!( + span, "{prefix} the image has smooth scaling"; + hint: "set the `scaling` attribute to `pixelated`" + ) + } else { + error!( + span, "{prefix} an image in the PDF has smooth scaling"; + hint: "set the `scaling` attribute of all images to `pixelated`" + ) + } + } + ValidationError::EmbeddedFile(e, s) => { + // We always set the span for embedded files, so it cannot be detached. + let span = to_span(*s); + match e { + EmbedError::Existence => { + error!( + span, "{prefix} document contains an embedded file"; + hint: "embedded files are not supported in this export mode" + ) + } + EmbedError::MissingDate => { + error!( + span, "{prefix} document date is missing"; + hint: "the document must have a date when embedding files"; + hint: "`set document(date: none)` must not be used in this case" + ) + } + EmbedError::MissingDescription => { + error!(span, "{prefix} the file description is missing") + } + EmbedError::MissingMimeType => { + error!(span, "{prefix} the file mime type is missing") + } + } + } + // The below errors cannot occur yet, only once Typst supports full PDF/A + // and PDF/UA. But let's still add a message just to be on the safe side. + ValidationError::MissingAnnotationAltText => error!( + Span::detached(), + "{prefix} missing annotation alt text"; + hint: "please report this as a bug" + ), + ValidationError::MissingAltText => error!( + Span::detached(), + "{prefix} missing alt text"; + hint: "make sure your images and equations have alt text" + ), + ValidationError::NoDocumentLanguage => error!( + Span::detached(), + "{prefix} missing document language"; + hint: "set the language of the document" + ), + // Needs to be set by typst-pdf. + ValidationError::MissingHeadingTitle => error!( + Span::detached(), + "{prefix} missing heading title"; + hint: "please report this as a bug" + ), + ValidationError::MissingDocumentOutline => error!( + Span::detached(), + "{prefix} missing document outline"; + hint: "please report this as a bug" + ), + ValidationError::MissingTagging => error!( + Span::detached(), + "{prefix} missing document tags"; + hint: "please report this as a bug" + ), + ValidationError::NoDocumentTitle => error!( + Span::detached(), + "{prefix} missing document title"; + hint: "set the title of the document" + ), + ValidationError::MissingDocumentDate => error!( + Span::detached(), + "{prefix} missing document date"; + hint: "set the date of the document" + ), + } +} + +/// Convert a krilla location to a span. +fn to_span(loc: Option) -> Span { + loc.map(|l| Span::from_raw(NonZeroU64::new(l).unwrap())) + .unwrap_or(Span::detached()) +} + +fn collect_named_destinations( + document: &PagedDocument, + pic: &PageIndexConverter, +) -> HashMap { + let mut locs_to_names = HashMap::new(); + + // Find all headings that have a label and are the first among other + // headings with the same label. + let matches: Vec<_> = { + let mut seen = HashSet::new(); + document + .introspector + .query(&HeadingElem::elem().select()) + .iter() + .filter_map(|elem| elem.location().zip(elem.label())) + .filter(|&(_, label)| seen.insert(label)) + .collect() + }; + + for (loc, label) in matches { + let pos = document.introspector.position(loc); + let index = pos.page.get() - 1; + // We are subtracting 10 because the position of links e.g. to headings is always at the + // baseline and if you link directly to it, the text will not be visible + // because it is right above. + let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); + + // Only add named destination if page belonging to the position is exported. + if let Some(index) = pic.pdf_page_index(index) { + let named = NamedDestination::new( + label.resolve().to_string(), + XyzDestination::new( + index, + krilla::geom::Point::from_xy(pos.point.x.to_f32(), y.to_f32()), + ), + ); + locs_to_names.insert(loc, named); + } + } + + locs_to_names +} + +pub(crate) struct PageIndexConverter { + page_indices: HashMap, + skipped_pages: usize, +} + +impl PageIndexConverter { + pub fn new(document: &PagedDocument, options: &PdfOptions) -> Self { + let mut page_indices = HashMap::new(); + let mut skipped_pages = 0; + + for i in 0..document.pages.len() { + if options + .page_ranges + .as_ref() + .is_some_and(|ranges| !ranges.includes_page_index(i)) + { + skipped_pages += 1; + } else { + page_indices.insert(i, i - skipped_pages); + } + } + + Self { page_indices, skipped_pages } + } + + pub(crate) fn has_skipped_pages(&self) -> bool { + self.skipped_pages > 0 + } + + /// Get the PDF page index of a page index, if it's not excluded. + pub(crate) fn pdf_page_index(&self, page_index: usize) -> Option { + self.page_indices.get(&page_index).copied() + } +} diff --git a/crates/typst-pdf/src/embed.rs b/crates/typst-pdf/src/embed.rs index 597638f4b..6ed65a2b6 100644 --- a/crates/typst-pdf/src/embed.rs +++ b/crates/typst-pdf/src/embed.rs @@ -1,122 +1,54 @@ -use std::collections::BTreeMap; +use std::sync::Arc; -use ecow::EcoString; -use pdf_writer::types::AssociationKind; -use pdf_writer::{Filter, Finish, Name, Ref, Str, TextStr}; +use krilla::embed::{AssociationKind, EmbeddedFile}; +use krilla::Document; use typst_library::diag::{bail, SourceResult}; -use typst_library::foundations::{NativeElement, Packed, StyleChain}; +use typst_library::foundations::{NativeElement, StyleChain}; +use typst_library::layout::PagedDocument; use typst_library::pdf::{EmbedElem, EmbeddedFileRelationship}; -use crate::catalog::{document_date, pdf_date}; -use crate::{deflate, NameExt, PdfChunk, StrExt, WithGlobalRefs}; +pub(crate) fn embed_files( + typst_doc: &PagedDocument, + document: &mut Document, +) -> SourceResult<()> { + let elements = typst_doc.introspector.query(&EmbedElem::elem().select()); -/// Query for all [`EmbedElem`] and write them and their file specifications. -/// -/// This returns a map of embedding names and references so that we can later -/// add them to the catalog's `/Names` dictionary. -pub fn write_embedded_files( - ctx: &WithGlobalRefs, -) -> SourceResult<(PdfChunk, BTreeMap)> { - let mut chunk = PdfChunk::new(); - let mut embedded_files = BTreeMap::default(); - - let elements = ctx.document.introspector.query(&EmbedElem::elem().select()); for elem in &elements { - if !ctx.options.standards.embedded_files { - // PDF/A-2 requires embedded files to be PDF/A-1 or PDF/A-2, - // which we don't currently check. - bail!( - elem.span(), - "file embeddings are not currently supported for PDF/A-2"; - hint: "PDF/A-3 supports arbitrary embedded files" - ); - } - let embed = elem.to_packed::().unwrap(); - if embed.path.derived.len() > Str::PDFA_LIMIT { - bail!(embed.span(), "embedded file path is too long"); - } - - let id = embed_file(ctx, &mut chunk, embed)?; - if embedded_files.insert(embed.path.derived.clone(), id).is_some() { - bail!( - elem.span(), - "duplicate embedded file for path `{}`", embed.path.derived; - hint: "embedded file paths must be unique", - ); - } - } - - Ok((chunk, embedded_files)) -} - -/// Write the embedded file stream and its file specification. -fn embed_file( - ctx: &WithGlobalRefs, - chunk: &mut PdfChunk, - embed: &Packed, -) -> SourceResult { - let embedded_file_stream_ref = chunk.alloc.bump(); - let file_spec_dict_ref = chunk.alloc.bump(); - - let data = embed.data.as_slice(); - let compressed = deflate(data); - - let mut embedded_file = chunk.embedded_file(embedded_file_stream_ref, &compressed); - embedded_file.filter(Filter::FlateDecode); - - if let Some(mime_type) = embed.mime_type(StyleChain::default()) { - if mime_type.len() > Name::PDFA_LIMIT { - bail!(embed.span(), "embedded file MIME type is too long"); - } - embedded_file.subtype(Name(mime_type.as_bytes())); - } else if ctx.options.standards.pdfa { - bail!(embed.span(), "embedded files must have a MIME type in PDF/A-3"); - } - - let mut params = embedded_file.params(); - params.size(data.len() as i32); - - let (date, tz) = document_date(ctx.document.info.date, ctx.options.timestamp); - if let Some(pdf_date) = date.and_then(|date| pdf_date(date, tz)) { - params.modification_date(pdf_date); - } else if ctx.options.standards.pdfa { - bail!( - embed.span(), - "the document must have a date when embedding files in PDF/A-3"; - hint: "`set document(date: none)` must not be used in this case" - ); - } - - params.finish(); - embedded_file.finish(); - - let mut file_spec = chunk.file_spec(file_spec_dict_ref); - file_spec.path(Str(embed.path.derived.as_bytes())); - file_spec.unic_file(TextStr(&embed.path.derived)); - file_spec - .insert(Name(b"EF")) - .dict() - .pair(Name(b"F"), embedded_file_stream_ref) - .pair(Name(b"UF"), embedded_file_stream_ref); - - if ctx.options.standards.pdfa { - // PDF 2.0, but ISO 19005-3 (PDF/A-3) Annex E allows it for PDF/A-3. - file_spec.association_kind(match embed.relationship(StyleChain::default()) { - Some(EmbeddedFileRelationship::Source) => AssociationKind::Source, - Some(EmbeddedFileRelationship::Data) => AssociationKind::Data, - Some(EmbeddedFileRelationship::Alternative) => AssociationKind::Alternative, - Some(EmbeddedFileRelationship::Supplement) => AssociationKind::Supplement, + let span = embed.span(); + let derived_path = &embed.path.derived; + let path = derived_path.to_string(); + let mime_type = + embed.mime_type(StyleChain::default()).clone().map(|s| s.to_string()); + let description = embed + .description(StyleChain::default()) + .clone() + .map(|s| s.to_string()); + let association_kind = match embed.relationship(StyleChain::default()) { None => AssociationKind::Unspecified, - }); - } + Some(e) => match e { + EmbeddedFileRelationship::Source => AssociationKind::Source, + EmbeddedFileRelationship::Data => AssociationKind::Data, + EmbeddedFileRelationship::Alternative => AssociationKind::Alternative, + EmbeddedFileRelationship::Supplement => AssociationKind::Supplement, + }, + }; + let data: Arc + Send + Sync> = Arc::new(embed.data.clone()); - if let Some(description) = embed.description(StyleChain::default()) { - if description.len() > Str::PDFA_LIMIT { - bail!(embed.span(), "embedded file description is too long"); + let file = EmbeddedFile { + path, + mime_type, + description, + association_kind, + data: data.into(), + compress: true, + location: Some(span.into_raw().get()), + }; + + if document.embed_file(file).is_none() { + bail!(span, "attempted to embed file {derived_path} twice"); } - file_spec.description(TextStr(description)); } - Ok(file_spec_dict_ref) + Ok(()) } diff --git a/crates/typst-pdf/src/extg.rs b/crates/typst-pdf/src/extg.rs deleted file mode 100644 index 06617d8d2..000000000 --- a/crates/typst-pdf/src/extg.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::collections::HashMap; - -use pdf_writer::Ref; -use typst_library::diag::SourceResult; - -use crate::{PdfChunk, WithGlobalRefs}; - -/// A PDF external graphics state. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] -pub struct ExtGState { - // In the range 0-255, needs to be divided before being written into the graphics state! - pub stroke_opacity: u8, - // In the range 0-255, needs to be divided before being written into the graphics state! - pub fill_opacity: u8, -} - -impl Default for ExtGState { - fn default() -> Self { - Self { stroke_opacity: 255, fill_opacity: 255 } - } -} - -impl ExtGState { - pub fn uses_opacities(&self) -> bool { - self.stroke_opacity != 255 || self.fill_opacity != 255 - } -} - -/// Embed all used external graphics states into the PDF. -pub fn write_graphic_states( - context: &WithGlobalRefs, -) -> SourceResult<(PdfChunk, HashMap)> { - let mut chunk = PdfChunk::new(); - let mut out = HashMap::new(); - context.resources.traverse(&mut |resources| { - for external_gs in resources.ext_gs.items() { - if out.contains_key(external_gs) { - continue; - } - - let id = chunk.alloc(); - out.insert(*external_gs, id); - chunk - .ext_graphics(id) - .non_stroking_alpha(external_gs.fill_opacity as f32 / 255.0) - .stroking_alpha(external_gs.stroke_opacity as f32 / 255.0); - } - - Ok(()) - })?; - - Ok((chunk, out)) -} diff --git a/crates/typst-pdf/src/font.rs b/crates/typst-pdf/src/font.rs deleted file mode 100644 index f2df2ac92..000000000 --- a/crates/typst-pdf/src/font.rs +++ /dev/null @@ -1,278 +0,0 @@ -use std::collections::{BTreeMap, HashMap}; -use std::hash::Hash; -use std::sync::Arc; - -use ecow::{eco_format, EcoString}; -use pdf_writer::types::{CidFontType, FontFlags, SystemInfo, UnicodeCmap}; -use pdf_writer::writers::{FontDescriptor, WMode}; -use pdf_writer::{Chunk, Filter, Finish, Name, Rect, Ref, Str}; -use subsetter::GlyphRemapper; -use ttf_parser::{name_id, GlyphId, Tag}; -use typst_library::diag::{At, SourceResult}; -use typst_library::text::Font; -use typst_syntax::Span; -use typst_utils::SliceExt; - -use crate::{deflate, EmExt, NameExt, PdfChunk, WithGlobalRefs}; - -const CFF: Tag = Tag::from_bytes(b"CFF "); -const CFF2: Tag = Tag::from_bytes(b"CFF2"); - -const SUBSET_TAG_LEN: usize = 6; -const IDENTITY_H: &str = "Identity-H"; - -pub(crate) const CMAP_NAME: Name = Name(b"Custom"); -pub(crate) const SYSTEM_INFO: SystemInfo = SystemInfo { - registry: Str(b"Adobe"), - ordering: Str(b"Identity"), - supplement: 0, -}; - -/// Embed all used fonts into the PDF. -#[typst_macros::time(name = "write fonts")] -pub fn write_fonts( - context: &WithGlobalRefs, -) -> SourceResult<(PdfChunk, HashMap)> { - let mut chunk = PdfChunk::new(); - let mut out = HashMap::new(); - context.resources.traverse(&mut |resources| { - for font in resources.fonts.items() { - if out.contains_key(font) { - continue; - } - - let type0_ref = chunk.alloc(); - let cid_ref = chunk.alloc(); - let descriptor_ref = chunk.alloc(); - let cmap_ref = chunk.alloc(); - let data_ref = chunk.alloc(); - out.insert(font.clone(), type0_ref); - - let glyph_set = resources.glyph_sets.get(font).unwrap(); - let glyph_remapper = resources.glyph_remappers.get(font).unwrap(); - let ttf = font.ttf(); - - // Do we have a TrueType or CFF font? - // - // FIXME: CFF2 must be handled differently and requires PDF 2.0 - // (or we have to convert it to CFF). - let is_cff = ttf - .raw_face() - .table(CFF) - .or_else(|| ttf.raw_face().table(CFF2)) - .is_some(); - - let base_font = base_font_name(font, glyph_set); - let base_font_type0 = if is_cff { - eco_format!("{base_font}-{IDENTITY_H}") - } else { - base_font.clone() - }; - - // Write the base font object referencing the CID font. - chunk - .type0_font(type0_ref) - .base_font(Name(base_font_type0.as_bytes())) - .encoding_predefined(Name(IDENTITY_H.as_bytes())) - .descendant_font(cid_ref) - .to_unicode(cmap_ref); - - // Write the CID font referencing the font descriptor. - let mut cid = chunk.cid_font(cid_ref); - cid.subtype(if is_cff { CidFontType::Type0 } else { CidFontType::Type2 }); - cid.base_font(Name(base_font.as_bytes())); - cid.system_info(SYSTEM_INFO); - cid.font_descriptor(descriptor_ref); - cid.default_width(0.0); - if !is_cff { - cid.cid_to_gid_map_predefined(Name(b"Identity")); - } - - // Extract the widths of all glyphs. - // `remapped_gids` returns an iterator over the old GIDs in their new sorted - // order, so we can append the widths as is. - let widths = glyph_remapper - .remapped_gids() - .map(|gid| { - let width = ttf.glyph_hor_advance(GlyphId(gid)).unwrap_or(0); - font.to_em(width).to_font_units() - }) - .collect::>(); - - // Write all non-zero glyph widths. - let mut first = 0; - let mut width_writer = cid.widths(); - for (w, group) in widths.group_by_key(|&w| w) { - let end = first + group.len(); - if w != 0.0 { - let last = end - 1; - width_writer.same(first as u16, last as u16, w); - } - first = end; - } - - width_writer.finish(); - cid.finish(); - - // Write the /ToUnicode character map, which maps glyph ids back to - // unicode codepoints to enable copying out of the PDF. - let cmap = create_cmap(glyph_set, glyph_remapper); - chunk - .cmap(cmap_ref, &cmap) - .writing_mode(WMode::Horizontal) - .filter(Filter::FlateDecode); - - let subset = subset_font(font, glyph_remapper) - .map_err(|err| { - let postscript_name = font.find_name(name_id::POST_SCRIPT_NAME); - let name = postscript_name.as_deref().unwrap_or(&font.info().family); - eco_format!("failed to process font {name}: {err}") - }) - .at(Span::detached())?; - - let mut stream = chunk.stream(data_ref, &subset); - stream.filter(Filter::FlateDecode); - if is_cff { - stream.pair(Name(b"Subtype"), Name(b"CIDFontType0C")); - } - stream.finish(); - - let mut font_descriptor = - write_font_descriptor(&mut chunk, descriptor_ref, font, &base_font); - if is_cff { - font_descriptor.font_file3(data_ref); - } else { - font_descriptor.font_file2(data_ref); - } - } - - Ok(()) - })?; - - Ok((chunk, out)) -} - -/// Writes a FontDescriptor dictionary. -pub fn write_font_descriptor<'a>( - pdf: &'a mut Chunk, - descriptor_ref: Ref, - font: &'a Font, - base_font: &str, -) -> FontDescriptor<'a> { - let ttf = font.ttf(); - let metrics = font.metrics(); - let serif = font - .find_name(name_id::POST_SCRIPT_NAME) - .is_some_and(|name| name.contains("Serif")); - - let mut flags = FontFlags::empty(); - flags.set(FontFlags::SERIF, serif); - flags.set(FontFlags::FIXED_PITCH, ttf.is_monospaced()); - flags.set(FontFlags::ITALIC, ttf.is_italic()); - flags.insert(FontFlags::SYMBOLIC); - flags.insert(FontFlags::SMALL_CAP); - - let global_bbox = ttf.global_bounding_box(); - let bbox = Rect::new( - font.to_em(global_bbox.x_min).to_font_units(), - font.to_em(global_bbox.y_min).to_font_units(), - font.to_em(global_bbox.x_max).to_font_units(), - font.to_em(global_bbox.y_max).to_font_units(), - ); - - let italic_angle = ttf.italic_angle(); - let ascender = metrics.ascender.to_font_units(); - let descender = metrics.descender.to_font_units(); - let cap_height = metrics.cap_height.to_font_units(); - let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0); - - // Write the font descriptor (contains metrics about the font). - let mut font_descriptor = pdf.font_descriptor(descriptor_ref); - font_descriptor - .name(Name(base_font.as_bytes())) - .flags(flags) - .bbox(bbox) - .italic_angle(italic_angle) - .ascent(ascender) - .descent(descender) - .cap_height(cap_height) - .stem_v(stem_v); - - font_descriptor -} - -/// Subset a font to the given glyphs. -/// -/// - For a font with TrueType outlines, this produces the whole OpenType font. -/// - For a font with CFF outlines, this produces just the CFF font program. -/// -/// In both cases, this returns the already compressed data. -#[comemo::memoize] -#[typst_macros::time(name = "subset font")] -fn subset_font( - font: &Font, - glyph_remapper: &GlyphRemapper, -) -> Result>, subsetter::Error> { - let data = font.data(); - let subset = subsetter::subset(data, font.index(), glyph_remapper)?; - let mut data = subset.as_ref(); - - // Extract the standalone CFF font program if applicable. - let raw = ttf_parser::RawFace::parse(data, 0).unwrap(); - if let Some(cff) = raw.table(CFF) { - data = cff; - } - - Ok(Arc::new(deflate(data))) -} - -/// Creates the base font name for a font with a specific glyph subset. -/// Consists of a subset tag and the PostScript name of the font. -/// -/// Returns a string of length maximum 116, so that even with `-Identity-H` -/// added it does not exceed the maximum PDF/A name length of 127. -pub(crate) fn base_font_name(font: &Font, glyphs: &T) -> EcoString { - const MAX_LEN: usize = Name::PDFA_LIMIT - REST_LEN; - const REST_LEN: usize = SUBSET_TAG_LEN + 1 + 1 + IDENTITY_H.len(); - - let postscript_name = font.find_name(name_id::POST_SCRIPT_NAME); - let name = postscript_name.as_deref().unwrap_or("unknown"); - let trimmed = &name[..name.len().min(MAX_LEN)]; - - // Hash the full name (we might have trimmed) and the glyphs to produce - // a fairly unique subset tag. - let subset_tag = subset_tag(&(name, glyphs)); - - eco_format!("{subset_tag}+{trimmed}") -} - -/// Produce a unique 6 letter tag for a glyph set. -pub(crate) fn subset_tag(glyphs: &T) -> EcoString { - const BASE: u128 = 26; - let mut hash = typst_utils::hash128(&glyphs); - let mut letter = [b'A'; SUBSET_TAG_LEN]; - for l in letter.iter_mut() { - *l = b'A' + (hash % BASE) as u8; - hash /= BASE; - } - std::str::from_utf8(&letter).unwrap().into() -} - -/// Create a compressed `/ToUnicode` CMap. -#[comemo::memoize] -#[typst_macros::time(name = "create cmap")] -fn create_cmap( - glyph_set: &BTreeMap, - glyph_remapper: &GlyphRemapper, -) -> Arc> { - // Produce a reverse mapping from glyphs' CIDs to unicode strings. - let mut cmap = UnicodeCmap::new(CMAP_NAME, SYSTEM_INFO); - for (&g, text) in glyph_set.iter() { - // See commend in `write_normal_text` for why we can choose the CID this way. - let cid = glyph_remapper.get(g).unwrap(); - if !text.is_empty() { - cmap.pair_with_multiple(cid, text.chars()); - } - } - Arc::new(deflate(&cmap.finish())) -} diff --git a/crates/typst-pdf/src/gradient.rs b/crates/typst-pdf/src/gradient.rs deleted file mode 100644 index 6cd4c1ae8..000000000 --- a/crates/typst-pdf/src/gradient.rs +++ /dev/null @@ -1,512 +0,0 @@ -use std::collections::HashMap; -use std::f32::consts::{PI, TAU}; -use std::sync::Arc; - -use ecow::eco_format; -use pdf_writer::types::{ColorSpaceOperand, FunctionShadingType}; -use pdf_writer::writers::StreamShadingType; -use pdf_writer::{Filter, Finish, Name, Ref}; -use typst_library::diag::SourceResult; -use typst_library::layout::{Abs, Angle, Point, Quadrant, Ratio, Transform}; -use typst_library::visualize::{ - Color, ColorSpace, Gradient, RatioOrAngle, RelativeTo, WeightedColor, -}; -use typst_utils::Numeric; - -use crate::color::{ - self, check_cmyk_allowed, ColorSpaceExt, PaintEncode, QuantizedColor, -}; -use crate::{content, deflate, transform_to_array, AbsExt, PdfChunk, WithGlobalRefs}; - -/// A unique-transform-aspect-ratio combination that will be encoded into the -/// PDF. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct PdfGradient { - /// The transform to apply to the gradient. - pub transform: Transform, - /// The aspect ratio of the gradient. - /// Required for aspect ratio correction. - pub aspect_ratio: Ratio, - /// The gradient. - pub gradient: Gradient, - /// The corrected angle of the gradient. - pub angle: Angle, -} - -/// Writes the actual gradients (shading patterns) to the PDF. -/// This is performed once after writing all pages. -pub fn write_gradients( - context: &WithGlobalRefs, -) -> SourceResult<(PdfChunk, HashMap)> { - let mut chunk = PdfChunk::new(); - let mut out = HashMap::new(); - context.resources.traverse(&mut |resources| { - for pdf_gradient in resources.gradients.items() { - if out.contains_key(pdf_gradient) { - continue; - } - - let shading = chunk.alloc(); - out.insert(pdf_gradient.clone(), shading); - - let PdfGradient { transform, aspect_ratio, gradient, angle } = pdf_gradient; - - let color_space = if gradient.space().hue_index().is_some() { - ColorSpace::Oklab - } else { - gradient.space() - }; - - if color_space == ColorSpace::Cmyk { - check_cmyk_allowed(context.options)?; - } - - let mut shading_pattern = match &gradient { - Gradient::Linear(_) => { - let shading_function = - shading_function(gradient, &mut chunk, color_space); - let mut shading_pattern = chunk.chunk.shading_pattern(shading); - let mut shading = shading_pattern.function_shading(); - shading.shading_type(FunctionShadingType::Axial); - - color::write( - color_space, - shading.color_space(), - &context.globals.color_functions, - ); - - let (mut sin, mut cos) = (angle.sin(), angle.cos()); - - // Scale to edges of unit square. - let factor = cos.abs() + sin.abs(); - sin *= factor; - cos *= factor; - - let (x1, y1, x2, y2): (f64, f64, f64, f64) = match angle.quadrant() { - Quadrant::First => (0.0, 0.0, cos, sin), - Quadrant::Second => (1.0, 0.0, cos + 1.0, sin), - Quadrant::Third => (1.0, 1.0, cos + 1.0, sin + 1.0), - Quadrant::Fourth => (0.0, 1.0, cos, sin + 1.0), - }; - - shading - .anti_alias(gradient.anti_alias()) - .function(shading_function) - .coords([x1 as f32, y1 as f32, x2 as f32, y2 as f32]) - .extend([true; 2]); - - shading.finish(); - - shading_pattern - } - Gradient::Radial(radial) => { - let shading_function = - shading_function(gradient, &mut chunk, color_space_of(gradient)); - let mut shading_pattern = chunk.chunk.shading_pattern(shading); - let mut shading = shading_pattern.function_shading(); - shading.shading_type(FunctionShadingType::Radial); - - color::write( - color_space, - shading.color_space(), - &context.globals.color_functions, - ); - - shading - .anti_alias(gradient.anti_alias()) - .function(shading_function) - .coords([ - radial.focal_center.x.get() as f32, - radial.focal_center.y.get() as f32, - radial.focal_radius.get() as f32, - radial.center.x.get() as f32, - radial.center.y.get() as f32, - radial.radius.get() as f32, - ]) - .extend([true; 2]); - - shading.finish(); - - shading_pattern - } - Gradient::Conic(_) => { - let vertices = compute_vertex_stream(gradient, *aspect_ratio); - - let stream_shading_id = chunk.alloc(); - let mut stream_shading = - chunk.chunk.stream_shading(stream_shading_id, &vertices); - - color::write( - color_space, - stream_shading.color_space(), - &context.globals.color_functions, - ); - - let range = color_space.range(); - stream_shading - .bits_per_coordinate(16) - .bits_per_component(16) - .bits_per_flag(8) - .shading_type(StreamShadingType::CoonsPatch) - .decode( - [0.0, 1.0, 0.0, 1.0].into_iter().chain(range.iter().copied()), - ) - .anti_alias(gradient.anti_alias()) - .filter(Filter::FlateDecode); - - stream_shading.finish(); - - let mut shading_pattern = chunk.shading_pattern(shading); - shading_pattern.shading_ref(stream_shading_id); - shading_pattern - } - }; - - shading_pattern.matrix(transform_to_array(*transform)); - } - - Ok(()) - })?; - - Ok((chunk, out)) -} - -/// Writes an exponential or stitched function that expresses the gradient. -fn shading_function( - gradient: &Gradient, - chunk: &mut PdfChunk, - color_space: ColorSpace, -) -> Ref { - let function = chunk.alloc(); - let mut functions = vec![]; - let mut bounds = vec![]; - let mut encode = vec![]; - - // Create the individual gradient functions for each pair of stops. - for window in gradient.stops_ref().windows(2) { - let (first, second) = (window[0], window[1]); - - // If we have a hue index or are using Oklab, we will create several - // stops in-between to make the gradient smoother without interpolation - // issues with native color spaces. - let mut last_c = first.0; - if gradient.space().hue_index().is_some() { - for i in 0..=32 { - let t = i as f64 / 32.0; - let real_t = first.1.get() * (1.0 - t) + second.1.get() * t; - - let c = gradient.sample(RatioOrAngle::Ratio(Ratio::new(real_t))); - functions.push(single_gradient(chunk, last_c, c, color_space)); - bounds.push(real_t as f32); - encode.extend([0.0, 1.0]); - last_c = c; - } - } - - bounds.push(second.1.get() as f32); - functions.push(single_gradient(chunk, first.0, second.0, color_space)); - encode.extend([0.0, 1.0]); - } - - // Special case for gradients with only two stops. - if functions.len() == 1 { - return functions[0]; - } - - // Remove the last bound, since it's not needed for the stitching function. - bounds.pop(); - - // Create the stitching function. - chunk - .stitching_function(function) - .domain([0.0, 1.0]) - .range(color_space.range().iter().copied()) - .functions(functions) - .bounds(bounds) - .encode(encode); - - function -} - -/// Writes an exponential function that expresses a single segment (between two -/// stops) of a gradient. -fn single_gradient( - chunk: &mut PdfChunk, - first_color: Color, - second_color: Color, - color_space: ColorSpace, -) -> Ref { - let reference = chunk.alloc(); - chunk - .exponential_function(reference) - .range(color_space.range().iter().copied()) - .c0(color_space.convert(first_color)) - .c1(color_space.convert(second_color)) - .domain([0.0, 1.0]) - .n(1.0); - - reference -} - -impl PaintEncode for Gradient { - fn set_as_fill( - &self, - ctx: &mut content::Builder, - on_text: bool, - transforms: content::Transforms, - ) -> SourceResult<()> { - ctx.reset_fill_color_space(); - - let index = register_gradient(ctx, self, on_text, transforms); - let id = eco_format!("Gr{index}"); - let name = Name(id.as_bytes()); - - ctx.content.set_fill_color_space(ColorSpaceOperand::Pattern); - ctx.content.set_fill_pattern(None, name); - Ok(()) - } - - fn set_as_stroke( - &self, - ctx: &mut content::Builder, - on_text: bool, - transforms: content::Transforms, - ) -> SourceResult<()> { - ctx.reset_stroke_color_space(); - - let index = register_gradient(ctx, self, on_text, transforms); - let id = eco_format!("Gr{index}"); - let name = Name(id.as_bytes()); - - ctx.content.set_stroke_color_space(ColorSpaceOperand::Pattern); - ctx.content.set_stroke_pattern(None, name); - Ok(()) - } -} - -/// Deduplicates a gradient to a named PDF resource. -fn register_gradient( - ctx: &mut content::Builder, - gradient: &Gradient, - on_text: bool, - mut transforms: content::Transforms, -) -> usize { - // Edge cases for strokes. - if transforms.size.x.is_zero() { - transforms.size.x = Abs::pt(1.0); - } - - if transforms.size.y.is_zero() { - transforms.size.y = Abs::pt(1.0); - } - let size = match gradient.unwrap_relative(on_text) { - RelativeTo::Self_ => transforms.size, - RelativeTo::Parent => transforms.container_size, - }; - - let (offset_x, offset_y) = match gradient { - Gradient::Conic(conic) => ( - -size.x * (1.0 - conic.center.x.get() / 2.0) / 2.0, - -size.y * (1.0 - conic.center.y.get() / 2.0) / 2.0, - ), - _ => (Abs::zero(), Abs::zero()), - }; - - let rotation = gradient.angle().unwrap_or_else(Angle::zero); - - let transform = match gradient.unwrap_relative(on_text) { - RelativeTo::Self_ => transforms.transform, - RelativeTo::Parent => transforms.container_transform, - }; - - let scale_offset = match gradient { - Gradient::Conic(_) => 4.0_f64, - _ => 1.0, - }; - - let pdf_gradient = PdfGradient { - aspect_ratio: size.aspect_ratio(), - transform: transform - .pre_concat(Transform::translate( - offset_x * scale_offset, - offset_y * scale_offset, - )) - .pre_concat(Transform::scale( - Ratio::new(size.x.to_pt() * scale_offset), - Ratio::new(size.y.to_pt() * scale_offset), - )), - gradient: gradient.clone(), - angle: Gradient::correct_aspect_ratio(rotation, size.aspect_ratio()), - }; - - ctx.resources.colors.mark_as_used(color_space_of(gradient)); - - ctx.resources.gradients.insert(pdf_gradient) -} - -/// Writes a single Coons Patch as defined in the PDF specification -/// to a binary vec. -/// -/// Structure: -/// - flag: `u8` -/// - points: `[u16; 24]` -/// - colors: `[u16; 4*N]` (N = number of components) -fn write_patch( - target: &mut Vec, - t: f32, - t1: f32, - c0: &[u16], - c1: &[u16], - angle: Angle, -) { - let theta = -TAU * t + angle.to_rad() as f32 + PI; - let theta1 = -TAU * t1 + angle.to_rad() as f32 + PI; - - let (cp1, cp2) = - control_point(Point::new(Abs::pt(0.5), Abs::pt(0.5)), 0.5, theta, theta1); - - // Push the flag - target.push(0); - - let p1 = - [u16::quantize(0.5, [0.0, 1.0]).to_be(), u16::quantize(0.5, [0.0, 1.0]).to_be()]; - - let p2 = [ - u16::quantize(theta.cos(), [-1.0, 1.0]).to_be(), - u16::quantize(theta.sin(), [-1.0, 1.0]).to_be(), - ]; - - let p3 = [ - u16::quantize(theta1.cos(), [-1.0, 1.0]).to_be(), - u16::quantize(theta1.sin(), [-1.0, 1.0]).to_be(), - ]; - - let cp1 = [ - u16::quantize(cp1.x.to_f32(), [0.0, 1.0]).to_be(), - u16::quantize(cp1.y.to_f32(), [0.0, 1.0]).to_be(), - ]; - - let cp2 = [ - u16::quantize(cp2.x.to_f32(), [0.0, 1.0]).to_be(), - u16::quantize(cp2.y.to_f32(), [0.0, 1.0]).to_be(), - ]; - - // Push the points - target.extend_from_slice(bytemuck::cast_slice(&[ - p1, p1, p2, p2, cp1, cp2, p3, p3, p1, p1, p1, p1, - ])); - - // Push the colors. - let colors = [c0, c0, c1, c1] - .into_iter() - .flat_map(|c| c.iter().copied().map(u16::to_be_bytes)) - .flatten(); - - target.extend(colors); -} - -fn control_point(c: Point, r: f32, angle_start: f32, angle_end: f32) -> (Point, Point) { - let n = (TAU / (angle_end - angle_start)).abs(); - let f = ((angle_end - angle_start) / n).tan() * 4.0 / 3.0; - - let p1 = c + Point::new( - Abs::pt((r * angle_start.cos() - f * r * angle_start.sin()) as f64), - Abs::pt((r * angle_start.sin() + f * r * angle_start.cos()) as f64), - ); - - let p2 = c + Point::new( - Abs::pt((r * angle_end.cos() + f * r * angle_end.sin()) as f64), - Abs::pt((r * angle_end.sin() - f * r * angle_end.cos()) as f64), - ); - - (p1, p2) -} - -#[comemo::memoize] -fn compute_vertex_stream(gradient: &Gradient, aspect_ratio: Ratio) -> Arc> { - let Gradient::Conic(conic) = gradient else { unreachable!() }; - - // Generated vertices for the Coons patches - let mut vertices = Vec::new(); - - // Correct the gradient's angle - let angle = Gradient::correct_aspect_ratio(conic.angle, aspect_ratio); - - for window in conic.stops.windows(2) { - let ((c0, t0), (c1, t1)) = (window[0], window[1]); - - // Precision: - // - On an even color, insert a stop every 90deg - // - For a hue-based color space, insert 200 stops minimum - // - On any other, insert 20 stops minimum - let max_dt = if c0 == c1 { - 0.25 - } else if conic.space.hue_index().is_some() { - 0.005 - } else { - 0.05 - }; - let encode_space = conic - .space - .hue_index() - .map(|_| ColorSpace::Oklab) - .unwrap_or(conic.space); - let mut t_x = t0.get(); - let dt = (t1.get() - t0.get()).min(max_dt); - - // Special casing for sharp gradients. - if t0 == t1 { - write_patch( - &mut vertices, - t0.get() as f32, - t1.get() as f32, - &encode_space.convert(c0), - &encode_space.convert(c1), - angle, - ); - continue; - } - - while t_x < t1.get() { - let t_next = (t_x + dt).min(t1.get()); - - // The current progress in the current window. - let t = |t| (t - t0.get()) / (t1.get() - t0.get()); - let c = Color::mix_iter( - [WeightedColor::new(c0, 1.0 - t(t_x)), WeightedColor::new(c1, t(t_x))], - conic.space, - ) - .unwrap(); - - let c_next = Color::mix_iter( - [ - WeightedColor::new(c0, 1.0 - t(t_next)), - WeightedColor::new(c1, t(t_next)), - ], - conic.space, - ) - .unwrap(); - - write_patch( - &mut vertices, - t_x as f32, - t_next as f32, - &encode_space.convert(c), - &encode_space.convert(c_next), - angle, - ); - - t_x = t_next; - } - } - - Arc::new(deflate(&vertices)) -} - -fn color_space_of(gradient: &Gradient) -> ColorSpace { - if gradient.space().hue_index().is_some() { - ColorSpace::Oklab - } else { - gradient.space() - } -} diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs index fa326e3e0..93bdb1950 100644 --- a/crates/typst-pdf/src/image.rs +++ b/crates/typst-pdf/src/image.rs @@ -1,249 +1,244 @@ -use std::collections::HashMap; -use std::io::Cursor; +use std::hash::{Hash, Hasher}; +use std::sync::{Arc, OnceLock}; -use ecow::eco_format; -use image::{DynamicImage, GenericImageView, Rgba}; -use pdf_writer::{Chunk, Filter, Finish, Ref}; -use typst_library::diag::{At, SourceResult, StrResult}; +use image::{DynamicImage, EncodableLayout, GenericImageView, Rgba}; +use krilla::image::{BitsPerComponent, CustomImage, ImageColorspace}; +use krilla::surface::Surface; +use krilla_svg::{SurfaceExt, SvgSettings}; +use typst_library::diag::{bail, SourceResult}; use typst_library::foundations::Smart; +use typst_library::layout::{Abs, Angle, Ratio, Size, Transform}; use typst_library::visualize::{ - ColorSpace, ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat, - RasterImage, SvgImage, + ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat, RasterImage, }; -use typst_utils::Deferred; +use typst_syntax::Span; -use crate::{color, deflate, PdfChunk, WithGlobalRefs}; +use crate::convert::{FrameContext, GlobalContext}; +use crate::util::{SizeExt, TransformExt}; -/// Embed all used images into the PDF. -#[typst_macros::time(name = "write images")] -pub fn write_images( - context: &WithGlobalRefs, -) -> SourceResult<(PdfChunk, HashMap)> { - let mut chunk = PdfChunk::new(); - let mut out = HashMap::new(); - context.resources.traverse(&mut |resources| { - for (i, image) in resources.images.items().enumerate() { - if out.contains_key(image) { - continue; +#[typst_macros::time(name = "handle image")] +pub(crate) fn handle_image( + gc: &mut GlobalContext, + fc: &mut FrameContext, + image: &Image, + size: Size, + surface: &mut Surface, + span: Span, +) -> SourceResult<()> { + surface.push_transform(&fc.state().transform().to_krilla()); + surface.set_location(span.into_raw().get()); + + let interpolate = image.scaling() == Smart::Custom(ImageScaling::Smooth); + + if let Some(alt) = image.alt() { + surface.start_alt_text(alt); + } + + gc.image_spans.insert(span); + + match image.kind() { + ImageKind::Raster(raster) => { + let (exif_transform, new_size) = exif_transform(raster, size); + surface.push_transform(&exif_transform.to_krilla()); + + let image = match convert_raster(raster.clone(), interpolate) { + None => bail!(span, "failed to process image"), + Some(i) => i, + }; + + if !gc.image_to_spans.contains_key(&image) { + gc.image_to_spans.insert(image.clone(), span); } - let (handle, span) = resources.deferred_images.get(&i).unwrap(); - let encoded = handle.wait().as_ref().map_err(Clone::clone).at(*span)?; - - match encoded { - EncodedImage::Raster { - data, - filter, - color_space, - bits_per_component, - width, - height, - compressed_icc, - alpha, - interpolate, - } => { - let image_ref = chunk.alloc(); - out.insert(image.clone(), image_ref); - - let mut image = chunk.chunk.image_xobject(image_ref, data); - image.filter(*filter); - image.width(*width as i32); - image.height(*height as i32); - image.bits_per_component(i32::from(*bits_per_component)); - image.interpolate(*interpolate); - - let mut icc_ref = None; - let space = image.color_space(); - if compressed_icc.is_some() { - let id = chunk.alloc.bump(); - space.icc_based(id); - icc_ref = Some(id); - } else { - color::write( - *color_space, - space, - &context.globals.color_functions, - ); - } - - // Add a second gray-scale image containing the alpha values if - // this image has an alpha channel. - if let Some((alpha_data, alpha_filter)) = alpha { - let mask_ref = chunk.alloc.bump(); - image.s_mask(mask_ref); - image.finish(); - - let mut mask = chunk.image_xobject(mask_ref, alpha_data); - mask.filter(*alpha_filter); - mask.width(*width as i32); - mask.height(*height as i32); - mask.color_space().device_gray(); - mask.bits_per_component(i32::from(*bits_per_component)); - mask.interpolate(*interpolate); - } else { - image.finish(); - } - - if let (Some(compressed_icc), Some(icc_ref)) = - (compressed_icc, icc_ref) - { - let mut stream = chunk.icc_profile(icc_ref, compressed_icc); - stream.filter(Filter::FlateDecode); - match color_space { - ColorSpace::Srgb => { - stream.n(3); - stream.alternate().srgb(); - } - ColorSpace::D65Gray => { - stream.n(1); - stream.alternate().d65_gray(); - } - _ => unimplemented!(), - } - } - } - EncodedImage::Svg(svg_chunk, id) => { - let mut map = HashMap::new(); - svg_chunk.renumber_into(&mut chunk.chunk, |old| { - *map.entry(old).or_insert_with(|| chunk.alloc.bump()) - }); - out.insert(image.clone(), map[id]); - } - } + surface.draw_image(image, new_size.to_krilla()); + surface.pop(); } + ImageKind::Svg(svg) => { + surface.draw_svg( + svg.tree(), + size.to_krilla(), + SvgSettings { embed_text: true, ..Default::default() }, + ); + } + } - Ok(()) - })?; + if image.alt().is_some() { + surface.end_alt_text(); + } - Ok((chunk, out)) + surface.pop(); + surface.reset_location(); + + Ok(()) } -/// Creates a new PDF image from the given image. -/// -/// Also starts the deferred encoding of the image. -#[comemo::memoize] -pub fn deferred_image( - image: Image, - pdfa: bool, -) -> (Deferred>, Option) { - let color_space = match image.kind() { - ImageKind::Raster(raster) if raster.icc().is_none() => { - Some(to_color_space(raster.dynamic().color())) +struct Repr { + /// The original, underlying raster image. + raster: RasterImage, + /// The alpha channel of the raster image, if existing. + alpha_channel: OnceLock>>, + /// A (potentially) converted version of the dynamic image stored `raster` that is + /// guaranteed to either be in luma8 or rgb8, and thus can be used for the + /// `color_channel` method of `CustomImage`. + actual_dynamic: OnceLock>, +} + +/// A wrapper around `RasterImage` so that we can implement `CustomImage`. +#[derive(Clone)] +struct PdfImage(Arc); + +impl PdfImage { + pub fn new(raster: RasterImage) -> Self { + Self(Arc::new(Repr { + raster, + alpha_channel: OnceLock::new(), + actual_dynamic: OnceLock::new(), + })) + } +} + +impl Hash for PdfImage { + fn hash(&self, state: &mut H) { + // `alpha_channel` and `actual_dynamic` are generated from the underlying `RasterImage`, + // so this is enough. Since `raster` is prehashed, this is also very cheap. + self.0.raster.hash(state); + } +} + +impl CustomImage for PdfImage { + fn color_channel(&self) -> &[u8] { + self.0 + .actual_dynamic + .get_or_init(|| { + let dynamic = self.0.raster.dynamic(); + let channel_count = dynamic.color().channel_count(); + + match (dynamic.as_ref(), channel_count) { + // Pure luma8 or rgb8 image, can use it directly. + (DynamicImage::ImageLuma8(_), _) => dynamic.clone(), + (DynamicImage::ImageRgb8(_), _) => dynamic.clone(), + // Grey-scale image, convert to luma8. + (_, 1 | 2) => Arc::new(DynamicImage::ImageLuma8(dynamic.to_luma8())), + // Anything else, convert to rgb8. + _ => Arc::new(DynamicImage::ImageRgb8(dynamic.to_rgb8())), + } + }) + .as_bytes() + } + + fn alpha_channel(&self) -> Option<&[u8]> { + self.0 + .alpha_channel + .get_or_init(|| { + self.0.raster.dynamic().color().has_alpha().then(|| { + self.0 + .raster + .dynamic() + .pixels() + .map(|(_, _, Rgba([_, _, _, a]))| a) + .collect() + }) + }) + .as_ref() + .map(|v| &**v) + } + + fn bits_per_component(&self) -> BitsPerComponent { + BitsPerComponent::Eight + } + + fn size(&self) -> (u32, u32) { + (self.0.raster.width(), self.0.raster.height()) + } + + fn icc_profile(&self) -> Option<&[u8]> { + if matches!( + self.0.raster.dynamic().as_ref(), + DynamicImage::ImageLuma8(_) + | DynamicImage::ImageLumaA8(_) + | DynamicImage::ImageRgb8(_) + | DynamicImage::ImageRgba8(_) + ) { + self.0.raster.icc().map(|b| b.as_bytes()) + } else { + // In all other cases, the dynamic will be converted into RGB8 or LUMA8, so the ICC + // profile may become invalid, and thus we don't include it. + None } - _ => None, + } + + fn color_space(&self) -> ImageColorspace { + // Remember that we convert all images to either RGB or luma. + if self.0.raster.dynamic().color().has_color() { + ImageColorspace::Rgb + } else { + ImageColorspace::Luma + } + } +} + +#[comemo::memoize] +fn convert_raster( + raster: RasterImage, + interpolate: bool, +) -> Option { + if let RasterFormat::Exchange(ExchangeFormat::Jpg) = raster.format() { + let image_data: Arc + Send + Sync> = + Arc::new(raster.data().clone()); + let icc_profile = raster.icc().map(|i| { + let i: Arc + Send + Sync> = Arc::new(i.clone()); + i + }); + + krilla::image::Image::from_jpeg_with_icc( + image_data.into(), + icc_profile.map(|i| i.into()), + interpolate, + ) + } else { + krilla::image::Image::from_custom(PdfImage::new(raster), interpolate) + } +} + +fn exif_transform(image: &RasterImage, size: Size) -> (Transform, Size) { + let base = |hp: bool, vp: bool, mut base_ts: Transform, size: Size| { + if hp { + // Flip horizontally in-place. + base_ts = base_ts.pre_concat( + Transform::scale(-Ratio::one(), Ratio::one()) + .pre_concat(Transform::translate(-size.x, Abs::zero())), + ) + } + + if vp { + // Flip vertically in-place. + base_ts = base_ts.pre_concat( + Transform::scale(Ratio::one(), -Ratio::one()) + .pre_concat(Transform::translate(Abs::zero(), -size.y)), + ) + } + + base_ts }; - // PDF/A does not appear to allow interpolation. - // See https://github.com/typst/typst/issues/2942. - let interpolate = !pdfa && image.scaling() == Smart::Custom(ImageScaling::Smooth); + let no_flipping = + |hp: bool, vp: bool| (base(hp, vp, Transform::identity(), size), size); - let deferred = Deferred::new(move || match image.kind() { - ImageKind::Raster(raster) => Ok(encode_raster_image(raster, interpolate)), - ImageKind::Svg(svg) => { - let (chunk, id) = encode_svg(svg, pdfa) - .map_err(|err| eco_format!("failed to convert SVG to PDF: {err}"))?; - Ok(EncodedImage::Svg(chunk, id)) - } - }); + let with_flipping = |hp: bool, vp: bool| { + let base_ts = Transform::rotate_at(Angle::deg(90.0), Abs::zero(), Abs::zero()) + .pre_concat(Transform::scale(Ratio::one(), -Ratio::one())); + let inv_size = Size::new(size.y, size.x); + (base(hp, vp, base_ts, inv_size), inv_size) + }; - (deferred, color_space) -} - -/// Encode an image with a suitable filter. -#[typst_macros::time(name = "encode raster image")] -fn encode_raster_image(image: &RasterImage, interpolate: bool) -> EncodedImage { - let dynamic = image.dynamic(); - let color_space = to_color_space(dynamic.color()); - - let (filter, data, bits_per_component) = - if image.format() == RasterFormat::Exchange(ExchangeFormat::Jpg) { - let mut data = Cursor::new(vec![]); - dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap(); - (Filter::DctDecode, data.into_inner(), 8) - } else { - // TODO: Encode flate streams with PNG-predictor? - let (data, bits_per_component) = match (dynamic, color_space) { - // RGB image. - (DynamicImage::ImageRgb8(rgb), _) => (deflate(rgb.as_raw()), 8), - // Grayscale image - (DynamicImage::ImageLuma8(luma), _) => (deflate(luma.as_raw()), 8), - (_, ColorSpace::D65Gray) => (deflate(dynamic.to_luma8().as_raw()), 8), - // Anything else - _ => (deflate(dynamic.to_rgb8().as_raw()), 8), - }; - (Filter::FlateDecode, data, bits_per_component) - }; - - let compressed_icc = image.icc().map(|data| deflate(data)); - let alpha = dynamic.color().has_alpha().then(|| encode_alpha(dynamic)); - - EncodedImage::Raster { - data, - filter, - color_space, - bits_per_component, - width: image.width(), - height: image.height(), - compressed_icc, - alpha, - interpolate, - } -} - -/// Encode an image's alpha channel if present. -#[typst_macros::time(name = "encode alpha")] -fn encode_alpha(image: &DynamicImage) -> (Vec, Filter) { - let pixels: Vec<_> = image.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect(); - (deflate(&pixels), Filter::FlateDecode) -} - -/// Encode an SVG into a chunk of PDF objects. -#[typst_macros::time(name = "encode svg")] -fn encode_svg( - svg: &SvgImage, - pdfa: bool, -) -> Result<(Chunk, Ref), svg2pdf::ConversionError> { - svg2pdf::to_chunk( - svg.tree(), - svg2pdf::ConversionOptions { pdfa, ..Default::default() }, - ) -} - -/// A pre-encoded image. -pub enum EncodedImage { - /// A pre-encoded rasterized image. - Raster { - /// The raw, pre-deflated image data. - data: Vec, - /// The filter to use for the image. - filter: Filter, - /// Which color space this image is encoded in. - color_space: ColorSpace, - /// How many bits of each color component are stored. - bits_per_component: u8, - /// The image's width. - width: u32, - /// The image's height. - height: u32, - /// The image's ICC profile, deflated, if any. - compressed_icc: Option>, - /// The alpha channel of the image, pre-deflated, if any. - alpha: Option<(Vec, Filter)>, - /// Whether image interpolation should be enabled. - interpolate: bool, - }, - /// A vector graphic. - /// - /// The chunk is the SVG converted to PDF objects. - Svg(Chunk, Ref), -} - -/// Matches an [`image::ColorType`] to [`ColorSpace`]. -fn to_color_space(color: image::ColorType) -> ColorSpace { - use image::ColorType::*; - match color { - L8 | La8 | L16 | La16 => ColorSpace::D65Gray, - Rgb8 | Rgba8 | Rgb16 | Rgba16 | Rgb32F | Rgba32F => ColorSpace::Srgb, - _ => unimplemented!(), + match image.exif_rotation() { + Some(2) => no_flipping(true, false), + Some(3) => no_flipping(true, true), + Some(4) => no_flipping(false, true), + Some(5) => with_flipping(false, false), + Some(6) => with_flipping(true, false), + Some(7) => with_flipping(true, true), + Some(8) => with_flipping(false, true), + _ => no_flipping(false, false), } } diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs index 88e62389c..4e0b74308 100644 --- a/crates/typst-pdf/src/lib.rs +++ b/crates/typst-pdf/src/lib.rs @@ -1,81 +1,33 @@ -//! Exporting of Typst documents into PDFs. +//! Exporting Typst documents to PDF. -mod catalog; -mod color; -mod color_font; -mod content; +mod convert; mod embed; -mod extg; -mod font; -mod gradient; mod image; -mod named_destination; +mod link; +mod metadata; mod outline; mod page; -mod resources; -mod tiling; +mod paint; +mod shape; +mod text; +mod util; + +pub use self::metadata::{Timestamp, Timezone}; -use std::collections::{BTreeMap, HashMap}; use std::fmt::{self, Debug, Formatter}; -use std::hash::Hash; -use std::ops::{Deref, DerefMut}; -use base64::Engine; -use ecow::EcoString; -use pdf_writer::{Chunk, Name, Pdf, Ref, Str, TextStr}; +use ecow::eco_format; use serde::{Deserialize, Serialize}; use typst_library::diag::{bail, SourceResult, StrResult}; -use typst_library::foundations::{Datetime, Smart}; -use typst_library::layout::{Abs, Em, PageRanges, PagedDocument, Transform}; -use typst_library::text::Font; -use typst_library::visualize::Image; -use typst_syntax::Span; -use typst_utils::Deferred; - -use crate::catalog::write_catalog; -use crate::color::{alloc_color_functions_refs, ColorFunctionRefs}; -use crate::color_font::{write_color_fonts, ColorFontSlice}; -use crate::embed::write_embedded_files; -use crate::extg::{write_graphic_states, ExtGState}; -use crate::font::write_fonts; -use crate::gradient::{write_gradients, PdfGradient}; -use crate::image::write_images; -use crate::named_destination::{write_named_destinations, NamedDestinations}; -use crate::page::{alloc_page_refs, traverse_pages, write_page_tree, EncodedPage}; -use crate::resources::{ - alloc_resources_refs, write_resource_dictionaries, Resources, ResourcesRefs, -}; -use crate::tiling::{write_tilings, PdfTiling}; +use typst_library::foundations::Smart; +use typst_library::layout::{PageRanges, PagedDocument}; /// Export a document into a PDF file. /// /// Returns the raw bytes making up the PDF file. #[typst_macros::time(name = "pdf")] pub fn pdf(document: &PagedDocument, options: &PdfOptions) -> SourceResult> { - PdfBuilder::new(document, options) - .phase(|builder| builder.run(traverse_pages))? - .phase(|builder| { - Ok(GlobalRefs { - color_functions: builder.run(alloc_color_functions_refs)?, - pages: builder.run(alloc_page_refs)?, - resources: builder.run(alloc_resources_refs)?, - }) - })? - .phase(|builder| { - Ok(References { - named_destinations: builder.run(write_named_destinations)?, - fonts: builder.run(write_fonts)?, - color_fonts: builder.run(write_color_fonts)?, - images: builder.run(write_images)?, - gradients: builder.run(write_gradients)?, - tilings: builder.run(write_tilings)?, - ext_gs: builder.run(write_graphic_states)?, - embedded_files: builder.run(write_embedded_files)?, - }) - })? - .phase(|builder| builder.run(write_page_tree))? - .phase(|builder| builder.run(write_resource_dictionaries))? - .export_with(write_catalog) + convert::convert(document, options) } /// Settings for PDF export. @@ -103,82 +55,74 @@ pub struct PdfOptions<'a> { pub standards: PdfStandards, } -/// A timestamp with timezone information. -#[derive(Debug, Clone, Copy)] -pub struct Timestamp { - /// The datetime of the timestamp. - pub(crate) datetime: Datetime, - /// The timezone of the timestamp. - pub(crate) timezone: Timezone, -} - -impl Timestamp { - /// Create a new timestamp with a given datetime and UTC suffix. - pub fn new_utc(datetime: Datetime) -> Self { - Self { datetime, timezone: Timezone::UTC } - } - - /// Create a new timestamp with a given datetime, and a local timezone offset. - pub fn new_local(datetime: Datetime, whole_minute_offset: i32) -> Option { - let hour_offset = (whole_minute_offset / 60).try_into().ok()?; - // Note: the `%` operator in Rust is the remainder operator, not the - // modulo operator. The remainder operator can return negative results. - // We can simply apply `abs` here because we assume the `minute_offset` - // will have the same sign as `hour_offset`. - let minute_offset = (whole_minute_offset % 60).abs().try_into().ok()?; - match (hour_offset, minute_offset) { - // Only accept valid timezone offsets with `-23 <= hours <= 23`, - // and `0 <= minutes <= 59`. - (-23..=23, 0..=59) => Some(Self { - datetime, - timezone: Timezone::Local { hour_offset, minute_offset }, - }), - _ => None, - } - } -} - -/// A timezone. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Timezone { - /// The UTC timezone. - UTC, - /// The local timezone offset from UTC. And the `minute_offset` will have - /// same sign as `hour_offset`. - Local { hour_offset: i8, minute_offset: u8 }, -} - /// Encapsulates a list of compatible PDF standards. #[derive(Clone)] pub struct PdfStandards { - /// For now, we simplify to just PDF/A. But it can be more fine-grained in - /// the future. - pub(crate) pdfa: bool, - /// Whether the standard allows for embedding any kind of file into the PDF. - /// We disallow this for PDF/A-2, since it only allows embedding - /// PDF/A-1 and PDF/A-2 documents. - pub(crate) embedded_files: bool, - /// Part of the PDF/A standard. - pub(crate) pdfa_part: Option<(i32, &'static str)>, + pub(crate) config: krilla::configure::Configuration, } impl PdfStandards { /// Validates a list of PDF standards for compatibility and returns their /// encapsulated representation. pub fn new(list: &[PdfStandard]) -> StrResult { - let a2b = list.contains(&PdfStandard::A_2b); - let a3b = list.contains(&PdfStandard::A_3b); + use krilla::configure::{Configuration, PdfVersion, Validator}; - if a2b && a3b { - bail!("PDF cannot conform to A-2B and A-3B at the same time") + let mut version: Option = None; + let mut set_version = |v: PdfVersion| -> StrResult<()> { + if let Some(prev) = version { + bail!( + "PDF cannot conform to {} and {} at the same time", + prev.as_str(), + v.as_str() + ); + } + version = Some(v); + Ok(()) + }; + + let mut validator = None; + let mut set_validator = |v: Validator| -> StrResult<()> { + if validator.is_some() { + bail!("Typst currently only supports one PDF substandard at a time"); + } + validator = Some(v); + Ok(()) + }; + + for standard in list { + match standard { + PdfStandard::V_1_4 => set_version(PdfVersion::Pdf14)?, + PdfStandard::V_1_5 => set_version(PdfVersion::Pdf15)?, + PdfStandard::V_1_6 => set_version(PdfVersion::Pdf16)?, + PdfStandard::V_1_7 => set_version(PdfVersion::Pdf17)?, + PdfStandard::V_2_0 => set_version(PdfVersion::Pdf20)?, + PdfStandard::A_1b => set_validator(Validator::A1_B)?, + PdfStandard::A_2b => set_validator(Validator::A2_B)?, + PdfStandard::A_2u => set_validator(Validator::A2_U)?, + PdfStandard::A_3b => set_validator(Validator::A3_B)?, + PdfStandard::A_3u => set_validator(Validator::A3_U)?, + PdfStandard::A_4 => set_validator(Validator::A4)?, + PdfStandard::A_4f => set_validator(Validator::A4F)?, + PdfStandard::A_4e => set_validator(Validator::A4E)?, + } } - let pdfa = a2b || a3b; - Ok(Self { - pdfa, - embedded_files: !a2b, - pdfa_part: pdfa.then_some((if a2b { 2 } else { 3 }, "B")), - }) + let config = match (version, validator) { + (Some(version), Some(validator)) => { + Configuration::new_with(validator, version).ok_or_else(|| { + eco_format!( + "{} is not compatible with {}", + version.as_str(), + validator.as_str() + ) + })? + } + (Some(version), None) => Configuration::new_with_version(version), + (None, Some(validator)) => Configuration::new_with_validator(validator), + (None, None) => Configuration::new_with_version(PdfVersion::Pdf17), + }; + + Ok(Self { config }) } } @@ -190,7 +134,10 @@ impl Debug for PdfStandards { impl Default for PdfStandards { fn default() -> Self { - Self { pdfa: false, embedded_files: true, pdfa_part: None } + use krilla::configure::{Configuration, PdfVersion}; + Self { + config: Configuration::new_with_version(PdfVersion::Pdf17), + } } } @@ -201,531 +148,43 @@ impl Default for PdfStandards { #[allow(non_camel_case_types)] #[non_exhaustive] pub enum PdfStandard { + /// PDF 1.4. + #[serde(rename = "1.4")] + V_1_4, + /// PDF 1.5. + #[serde(rename = "1.5")] + V_1_5, + /// PDF 1.5. + #[serde(rename = "1.6")] + V_1_6, /// PDF 1.7. #[serde(rename = "1.7")] V_1_7, + /// PDF 2.0. + #[serde(rename = "2.0")] + V_2_0, + /// PDF/A-1b. + #[serde(rename = "a-1b")] + A_1b, /// PDF/A-2b. #[serde(rename = "a-2b")] A_2b, - /// PDF/A-3b. + /// PDF/A-2u. + #[serde(rename = "a-2u")] + A_2u, + /// PDF/A-3u. #[serde(rename = "a-3b")] A_3b, -} - -/// A struct to build a PDF following a fixed succession of phases. -/// -/// This type uses generics to represent its current state. `S` (for "state") is -/// all data that was produced by the previous phases, that is now read-only. -/// -/// Phase after phase, this state will be transformed. Each phase corresponds to -/// a call to the [eponymous function](`PdfBuilder::phase`) and produces a new -/// part of the state, that will be aggregated with all other information, for -/// consumption during the next phase. -/// -/// In other words: this struct follows the **typestate pattern**. This prevents -/// you from using data that is not yet available, at the type level. -/// -/// Each phase consists of processes, that can read the state of the previous -/// phases, and construct a part of the new state. -/// -/// A final step, that has direct access to the global reference allocator and -/// PDF document, can be run with [`PdfBuilder::export_with`]. -struct PdfBuilder { - /// The context that has been accumulated so far. - state: S, - /// A global bump allocator. - alloc: Ref, - /// The PDF document that is being written. - pdf: Pdf, -} - -/// The initial state: we are exploring the document, collecting all resources -/// that will be necessary later. The content of the pages is also built during -/// this phase. -struct WithDocument<'a> { - /// The Typst document that is exported. - document: &'a PagedDocument, - /// Settings for PDF export. - options: &'a PdfOptions<'a>, -} - -/// At this point, resources were listed, but they don't have any reference -/// associated with them. -/// -/// This phase allocates some global references. -struct WithResources<'a> { - document: &'a PagedDocument, - options: &'a PdfOptions<'a>, - /// The content of the pages encoded as PDF content streams. - /// - /// The pages are at the index corresponding to their page number, but they - /// may be `None` if they are not in the range specified by - /// `exported_pages`. - pages: Vec>, - /// The PDF resources that are used in the content of the pages. - resources: Resources<()>, -} - -/// Global references. -struct GlobalRefs { - /// References for color conversion functions. - color_functions: ColorFunctionRefs, - /// Reference for pages. - /// - /// Items of this vector are `None` if the corresponding page is not - /// exported. - pages: Vec>, - /// References for the resource dictionaries. - resources: ResourcesRefs, -} - -impl<'a> From<(WithDocument<'a>, (Vec>, Resources<()>))> - for WithResources<'a> -{ - fn from( - (previous, (pages, resources)): ( - WithDocument<'a>, - (Vec>, Resources<()>), - ), - ) -> Self { - Self { - document: previous.document, - options: previous.options, - pages, - resources, - } - } -} - -/// At this point, the resources have been collected, and global references have -/// been allocated. -/// -/// We are now writing objects corresponding to resources, and giving them references, -/// that will be collected in [`References`]. -struct WithGlobalRefs<'a> { - document: &'a PagedDocument, - options: &'a PdfOptions<'a>, - pages: Vec>, - /// Resources are the same as in previous phases, but each dictionary now has a reference. - resources: Resources, - /// Global references that were just allocated. - globals: GlobalRefs, -} - -impl<'a> From<(WithResources<'a>, GlobalRefs)> for WithGlobalRefs<'a> { - fn from((previous, globals): (WithResources<'a>, GlobalRefs)) -> Self { - Self { - document: previous.document, - options: previous.options, - pages: previous.pages, - resources: previous.resources.with_refs(&globals.resources), - globals, - } - } -} - -/// The references that have been assigned to each object. -struct References { - /// List of named destinations, each with an ID. - named_destinations: NamedDestinations, - /// The IDs of written fonts. - fonts: HashMap, - /// The IDs of written color fonts. - color_fonts: HashMap, - /// The IDs of written images. - images: HashMap, - /// The IDs of written gradients. - gradients: HashMap, - /// The IDs of written tilings. - tilings: HashMap, - /// The IDs of written external graphics states. - ext_gs: HashMap, - /// The names and references for embedded files. - embedded_files: BTreeMap, -} - -/// At this point, the references have been assigned to all resources. The page -/// tree is going to be written, and given a reference. It is also at this point that -/// the page contents is actually written. -struct WithRefs<'a> { - document: &'a PagedDocument, - options: &'a PdfOptions<'a>, - globals: GlobalRefs, - pages: Vec>, - resources: Resources, - /// References that were allocated for resources. - references: References, -} - -impl<'a> From<(WithGlobalRefs<'a>, References)> for WithRefs<'a> { - fn from((previous, references): (WithGlobalRefs<'a>, References)) -> Self { - Self { - document: previous.document, - options: previous.options, - globals: previous.globals, - pages: previous.pages, - resources: previous.resources, - references, - } - } -} - -/// In this phase, we write resource dictionaries. -/// -/// Each sub-resource gets its own isolated resource dictionary. -struct WithEverything<'a> { - document: &'a PagedDocument, - options: &'a PdfOptions<'a>, - globals: GlobalRefs, - pages: Vec>, - resources: Resources, - references: References, - /// Reference that was allocated for the page tree. - page_tree_ref: Ref, -} - -impl<'a> From<(WithEverything<'a>, ())> for WithEverything<'a> { - fn from((this, _): (WithEverything<'a>, ())) -> Self { - this - } -} - -impl<'a> From<(WithRefs<'a>, Ref)> for WithEverything<'a> { - fn from((previous, page_tree_ref): (WithRefs<'a>, Ref)) -> Self { - Self { - document: previous.document, - options: previous.options, - globals: previous.globals, - resources: previous.resources, - references: previous.references, - pages: previous.pages, - page_tree_ref, - } - } -} - -impl<'a> PdfBuilder> { - /// Start building a PDF for a Typst document. - fn new(document: &'a PagedDocument, options: &'a PdfOptions<'a>) -> Self { - Self { - alloc: Ref::new(1), - pdf: Pdf::new(), - state: WithDocument { document, options }, - } - } -} - -impl PdfBuilder { - /// Start a new phase, and save its output in the global state. - fn phase(mut self, builder: B) -> SourceResult> - where - // New state - NS: From<(S, O)>, - // Builder - B: Fn(&mut Self) -> SourceResult, - { - let output = builder(&mut self)?; - Ok(PdfBuilder { - state: NS::from((self.state, output)), - alloc: self.alloc, - pdf: self.pdf, - }) - } - - /// Run a step with the current state, merges its output into the PDF file, - /// and renumbers any references it returned. - fn run(&mut self, process: P) -> SourceResult - where - // Process - P: Fn(&S) -> SourceResult<(PdfChunk, O)>, - // Output - O: Renumber, - { - let (chunk, mut output) = process(&self.state)?; - // Allocate a final reference for each temporary one - let allocated = chunk.alloc.get() - TEMPORARY_REFS_START; - let offset = TEMPORARY_REFS_START - self.alloc.get(); - - // Merge the chunk into the PDF, using the new references - chunk.renumber_into(&mut self.pdf, |mut r| { - r.renumber(offset); - - r - }); - - // Also update the references in the output - output.renumber(offset); - - self.alloc = Ref::new(self.alloc.get() + allocated); - - Ok(output) - } - - /// Finalize the PDF export and returns the buffer representing the - /// document. - fn export_with

(mut self, process: P) -> SourceResult> - where - P: Fn(S, &mut Pdf, &mut Ref) -> SourceResult<()>, - { - process(self.state, &mut self.pdf, &mut self.alloc)?; - Ok(self.pdf.finish()) - } -} - -/// A reference or collection of references that can be re-numbered, -/// to become valid in a global scope. -trait Renumber { - /// Renumber this value by shifting any references it contains by `offset`. - fn renumber(&mut self, offset: i32); -} - -impl Renumber for () { - fn renumber(&mut self, _offset: i32) {} -} - -impl Renumber for Ref { - fn renumber(&mut self, offset: i32) { - if self.get() >= TEMPORARY_REFS_START { - *self = Ref::new(self.get() - offset); - } - } -} - -impl Renumber for Vec { - fn renumber(&mut self, offset: i32) { - for item in self { - item.renumber(offset); - } - } -} - -impl Renumber for HashMap { - fn renumber(&mut self, offset: i32) { - for v in self.values_mut() { - v.renumber(offset); - } - } -} - -impl Renumber for BTreeMap { - fn renumber(&mut self, offset: i32) { - for v in self.values_mut() { - v.renumber(offset); - } - } -} - -impl Renumber for Option { - fn renumber(&mut self, offset: i32) { - if let Some(r) = self { - r.renumber(offset) - } - } -} - -impl Renumber for (T, R) { - fn renumber(&mut self, offset: i32) { - self.1.renumber(offset) - } -} - -/// A portion of a PDF file. -struct PdfChunk { - /// The actual chunk. - chunk: Chunk, - /// A local allocator. - alloc: Ref, -} - -/// Any reference below that value was already allocated before and -/// should not be rewritten. Anything above was allocated in the current -/// chunk, and should be remapped. -/// -/// This is a constant (large enough to avoid collisions) and not -/// dependent on self.alloc to allow for better memoization of steps, if -/// needed in the future. -const TEMPORARY_REFS_START: i32 = 1_000_000_000; - -/// A part of a PDF document. -impl PdfChunk { - /// Start writing a new part of the document. - fn new() -> Self { - PdfChunk { - chunk: Chunk::new(), - alloc: Ref::new(TEMPORARY_REFS_START), - } - } - - /// Allocate a reference that is valid in the context of this chunk. - /// - /// References allocated with this function should be [renumbered](`Renumber::renumber`) - /// before being used in other chunks. This is done automatically if these - /// references are stored in the global `PdfBuilder` state. - fn alloc(&mut self) -> Ref { - self.alloc.bump() - } -} - -impl Deref for PdfChunk { - type Target = Chunk; - - fn deref(&self) -> &Self::Target { - &self.chunk - } -} - -impl DerefMut for PdfChunk { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.chunk - } -} - -/// Compress data with the DEFLATE algorithm. -fn deflate(data: &[u8]) -> Vec { - const COMPRESSION_LEVEL: u8 = 6; - miniz_oxide::deflate::compress_to_vec_zlib(data, COMPRESSION_LEVEL) -} - -/// Memoized and deferred version of [`deflate`] specialized for a page's content -/// stream. -#[comemo::memoize] -fn deflate_deferred(content: Vec) -> Deferred> { - Deferred::new(move || deflate(&content)) -} - -/// Create a base64-encoded hash of the value. -fn hash_base64(value: &T) -> String { - base64::engine::general_purpose::STANDARD - .encode(typst_utils::hash128(value).to_be_bytes()) -} - -/// Additional methods for [`Abs`]. -trait AbsExt { - /// Convert an to a number of points. - fn to_f32(self) -> f32; -} - -impl AbsExt for Abs { - fn to_f32(self) -> f32 { - self.to_pt() as f32 - } -} - -/// Additional methods for [`Em`]. -trait EmExt { - /// Convert an em length to a number of PDF font units. - fn to_font_units(self) -> f32; -} - -impl EmExt for Em { - fn to_font_units(self) -> f32 { - 1000.0 * self.get() as f32 - } -} - -trait NameExt<'a> { - /// The maximum length of a name in PDF/A. - const PDFA_LIMIT: usize = 127; -} - -impl<'a> NameExt<'a> for Name<'a> {} - -/// Additional methods for [`Str`]. -trait StrExt<'a>: Sized { - /// The maximum length of a string in PDF/A. - const PDFA_LIMIT: usize = 32767; - - /// Create a string that satisfies the constraints of PDF/A. - #[allow(unused)] - fn trimmed(string: &'a [u8]) -> Self; -} - -impl<'a> StrExt<'a> for Str<'a> { - fn trimmed(string: &'a [u8]) -> Self { - Self(&string[..string.len().min(Self::PDFA_LIMIT)]) - } -} - -/// Additional methods for [`TextStr`]. -trait TextStrExt<'a>: Sized { - /// The maximum length of a string in PDF/A. - const PDFA_LIMIT: usize = Str::PDFA_LIMIT; - - /// Create a text string that satisfies the constraints of PDF/A. - fn trimmed(string: &'a str) -> Self; -} - -impl<'a> TextStrExt<'a> for TextStr<'a> { - fn trimmed(string: &'a str) -> Self { - Self(&string[..string.len().min(Self::PDFA_LIMIT)]) - } -} - -/// Extension trait for [`Content`](pdf_writer::Content). -trait ContentExt { - fn save_state_checked(&mut self) -> SourceResult<()>; -} - -impl ContentExt for pdf_writer::Content { - fn save_state_checked(&mut self) -> SourceResult<()> { - self.save_state(); - if self.state_nesting_depth() > 28 { - bail!( - Span::detached(), - "maximum PDF grouping depth exceeding"; - hint: "try to avoid excessive nesting of layout containers", - ); - } - Ok(()) - } -} - -/// Convert to an array of floats. -fn transform_to_array(ts: Transform) -> [f32; 6] { - [ - ts.sx.get() as f32, - ts.ky.get() as f32, - ts.kx.get() as f32, - ts.sy.get() as f32, - ts.tx.to_f32(), - ts.ty.to_f32(), - ] -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_timestamp_new_local() { - let dummy_datetime = Datetime::from_ymd_hms(2024, 12, 17, 10, 10, 10).unwrap(); - let test = |whole_minute_offset, expect_timezone| { - assert_eq!( - Timestamp::new_local(dummy_datetime, whole_minute_offset) - .unwrap() - .timezone, - expect_timezone - ); - }; - - // Valid timezone offsets - test(0, Timezone::Local { hour_offset: 0, minute_offset: 0 }); - test(480, Timezone::Local { hour_offset: 8, minute_offset: 0 }); - test(-480, Timezone::Local { hour_offset: -8, minute_offset: 0 }); - test(330, Timezone::Local { hour_offset: 5, minute_offset: 30 }); - test(-210, Timezone::Local { hour_offset: -3, minute_offset: 30 }); - test(-720, Timezone::Local { hour_offset: -12, minute_offset: 0 }); // AoE - - // Corner cases - test(315, Timezone::Local { hour_offset: 5, minute_offset: 15 }); - test(-225, Timezone::Local { hour_offset: -3, minute_offset: 45 }); - test(1439, Timezone::Local { hour_offset: 23, minute_offset: 59 }); - test(-1439, Timezone::Local { hour_offset: -23, minute_offset: 59 }); - - // Invalid timezone offsets - assert!(Timestamp::new_local(dummy_datetime, 1440).is_none()); - assert!(Timestamp::new_local(dummy_datetime, -1440).is_none()); - assert!(Timestamp::new_local(dummy_datetime, i32::MAX).is_none()); - assert!(Timestamp::new_local(dummy_datetime, i32::MIN).is_none()); - } + /// PDF/A-3u. + #[serde(rename = "a-3u")] + A_3u, + /// PDF/A-4. + #[serde(rename = "a-4")] + A_4, + /// PDF/A-4f. + #[serde(rename = "a-4f")] + A_4f, + /// PDF/A-4e. + #[serde(rename = "a-4e")] + A_4e, } diff --git a/crates/typst-pdf/src/link.rs b/crates/typst-pdf/src/link.rs new file mode 100644 index 000000000..64cb8f0a2 --- /dev/null +++ b/crates/typst-pdf/src/link.rs @@ -0,0 +1,94 @@ +use krilla::action::{Action, LinkAction}; +use krilla::annotation::{LinkAnnotation, Target}; +use krilla::destination::XyzDestination; +use krilla::geom::Rect; +use typst_library::layout::{Abs, Point, Size}; +use typst_library::model::Destination; + +use crate::convert::{FrameContext, GlobalContext}; +use crate::util::{AbsExt, PointExt}; + +pub(crate) fn handle_link( + fc: &mut FrameContext, + gc: &mut GlobalContext, + dest: &Destination, + size: Size, +) { + let mut min_x = Abs::inf(); + let mut min_y = Abs::inf(); + let mut max_x = -Abs::inf(); + let mut max_y = -Abs::inf(); + + let pos = Point::zero(); + + // Compute the bounding box of the transformed link. + for point in [ + pos, + pos + Point::with_x(size.x), + pos + Point::with_y(size.y), + pos + size.to_point(), + ] { + let t = point.transform(fc.state().transform()); + min_x.set_min(t.x); + min_y.set_min(t.y); + max_x.set_max(t.x); + max_y.set_max(t.y); + } + + let x1 = min_x.to_f32(); + let x2 = max_x.to_f32(); + let y1 = min_y.to_f32(); + let y2 = max_y.to_f32(); + + let rect = Rect::from_ltrb(x1, y1, x2, y2).unwrap(); + + // TODO: Support quad points. + + let pos = match dest { + Destination::Url(u) => { + fc.push_annotation( + LinkAnnotation::new( + rect, + None, + Target::Action(Action::Link(LinkAction::new(u.to_string()))), + ) + .into(), + ); + return; + } + Destination::Position(p) => *p, + Destination::Location(loc) => { + if let Some(nd) = gc.loc_to_names.get(loc) { + // If a named destination has been registered, it's already guaranteed to + // not point to an excluded page. + fc.push_annotation( + LinkAnnotation::new( + rect, + None, + Target::Destination(krilla::destination::Destination::Named( + nd.clone(), + )), + ) + .into(), + ); + return; + } else { + gc.document.introspector.position(*loc) + } + } + }; + + let page_index = pos.page.get() - 1; + if let Some(index) = gc.page_index_converter.pdf_page_index(page_index) { + fc.push_annotation( + LinkAnnotation::new( + rect, + None, + Target::Destination(krilla::destination::Destination::Xyz( + XyzDestination::new(index, pos.point.to_krilla()), + )), + ) + .into(), + ); + } +} diff --git a/crates/typst-pdf/src/metadata.rs b/crates/typst-pdf/src/metadata.rs new file mode 100644 index 000000000..589c5e2fb --- /dev/null +++ b/crates/typst-pdf/src/metadata.rs @@ -0,0 +1,184 @@ +use ecow::EcoString; +use krilla::metadata::{Metadata, TextDirection}; +use typst_library::foundations::{Datetime, Smart}; +use typst_library::layout::Dir; +use typst_library::text::Lang; + +use crate::convert::GlobalContext; + +pub(crate) fn build_metadata(gc: &GlobalContext) -> Metadata { + let creator = format!("Typst {}", env!("CARGO_PKG_VERSION")); + + let lang = gc.languages.iter().max_by_key(|(_, &count)| count).map(|(&l, _)| l); + + let dir = if lang.map(Lang::dir) == Some(Dir::RTL) { + TextDirection::RightToLeft + } else { + TextDirection::LeftToRight + }; + + let mut metadata = Metadata::new() + .creator(creator) + .keywords(gc.document.info.keywords.iter().map(EcoString::to_string).collect()) + .authors(gc.document.info.author.iter().map(EcoString::to_string).collect()); + + let lang = gc.languages.iter().max_by_key(|(_, &count)| count).map(|(&l, _)| l); + + if let Some(lang) = lang { + metadata = metadata.language(lang.as_str().to_string()); + } + + if let Some(title) = &gc.document.info.title { + metadata = metadata.title(title.to_string()); + } + + if let Some(subject) = &gc.document.info.description { + metadata = metadata.subject(subject.to_string()); + } + + if let Some(ident) = gc.options.ident.custom() { + metadata = metadata.document_id(ident.to_string()); + } + + // (1) If the `document.date` is set to specific `datetime` or `none`, use it. + // (2) If the `document.date` is set to `auto` or not set, try to use the + // date from the options. + // (3) Otherwise, we don't write date metadata. + let (date, tz) = match (gc.document.info.date, gc.options.timestamp) { + (Smart::Custom(date), _) => (date, None), + (Smart::Auto, Some(timestamp)) => { + (Some(timestamp.datetime), Some(timestamp.timezone)) + } + _ => (None, None), + }; + + if let Some(date) = date.and_then(|d| convert_date(d, tz)) { + metadata = metadata.creation_date(date); + } + + metadata = metadata.text_direction(dir); + + metadata +} + +fn convert_date( + datetime: Datetime, + tz: Option, +) -> Option { + let year = datetime.year().filter(|&y| y >= 0)? as u16; + + let mut kd = krilla::metadata::DateTime::new(year); + + if let Some(month) = datetime.month() { + kd = kd.month(month); + } + + if let Some(day) = datetime.day() { + kd = kd.day(day); + } + + if let Some(h) = datetime.hour() { + kd = kd.hour(h); + } + + if let Some(m) = datetime.minute() { + kd = kd.minute(m); + } + + if let Some(s) = datetime.second() { + kd = kd.second(s); + } + + match tz { + Some(Timezone::UTC) => kd = kd.utc_offset_hour(0).utc_offset_minute(0), + Some(Timezone::Local { hour_offset, minute_offset }) => { + kd = kd.utc_offset_hour(hour_offset).utc_offset_minute(minute_offset) + } + None => {} + } + + Some(kd) +} + +/// A timestamp with timezone information. +#[derive(Debug, Clone, Copy)] +pub struct Timestamp { + /// The datetime of the timestamp. + pub(crate) datetime: Datetime, + /// The timezone of the timestamp. + pub(crate) timezone: Timezone, +} + +impl Timestamp { + /// Create a new timestamp with a given datetime and UTC suffix. + pub fn new_utc(datetime: Datetime) -> Self { + Self { datetime, timezone: Timezone::UTC } + } + + /// Create a new timestamp with a given datetime, and a local timezone offset. + pub fn new_local(datetime: Datetime, whole_minute_offset: i32) -> Option { + let hour_offset = (whole_minute_offset / 60).try_into().ok()?; + // Note: the `%` operator in Rust is the remainder operator, not the + // modulo operator. The remainder operator can return negative results. + // We can simply apply `abs` here because we assume the `minute_offset` + // will have the same sign as `hour_offset`. + let minute_offset = (whole_minute_offset % 60).abs().try_into().ok()?; + match (hour_offset, minute_offset) { + // Only accept valid timezone offsets with `-23 <= hours <= 23`, + // and `0 <= minutes <= 59`. + (-23..=23, 0..=59) => Some(Self { + datetime, + timezone: Timezone::Local { hour_offset, minute_offset }, + }), + _ => None, + } + } +} + +/// A timezone. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Timezone { + /// The UTC timezone. + UTC, + /// The local timezone offset from UTC. And the `minute_offset` will have + /// same sign as `hour_offset`. + Local { hour_offset: i8, minute_offset: u8 }, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_timestamp_new_local() { + let dummy_datetime = Datetime::from_ymd_hms(2024, 12, 17, 10, 10, 10).unwrap(); + let test = |whole_minute_offset, expect_timezone| { + assert_eq!( + Timestamp::new_local(dummy_datetime, whole_minute_offset) + .unwrap() + .timezone, + expect_timezone + ); + }; + + // Valid timezone offsets + test(0, Timezone::Local { hour_offset: 0, minute_offset: 0 }); + test(480, Timezone::Local { hour_offset: 8, minute_offset: 0 }); + test(-480, Timezone::Local { hour_offset: -8, minute_offset: 0 }); + test(330, Timezone::Local { hour_offset: 5, minute_offset: 30 }); + test(-210, Timezone::Local { hour_offset: -3, minute_offset: 30 }); + test(-720, Timezone::Local { hour_offset: -12, minute_offset: 0 }); // AoE + + // Corner cases + test(315, Timezone::Local { hour_offset: 5, minute_offset: 15 }); + test(-225, Timezone::Local { hour_offset: -3, minute_offset: 45 }); + test(1439, Timezone::Local { hour_offset: 23, minute_offset: 59 }); + test(-1439, Timezone::Local { hour_offset: -23, minute_offset: 59 }); + + // Invalid timezone offsets + assert!(Timestamp::new_local(dummy_datetime, 1440).is_none()); + assert!(Timestamp::new_local(dummy_datetime, -1440).is_none()); + assert!(Timestamp::new_local(dummy_datetime, i32::MAX).is_none()); + assert!(Timestamp::new_local(dummy_datetime, i32::MIN).is_none()); + } +} diff --git a/crates/typst-pdf/src/named_destination.rs b/crates/typst-pdf/src/named_destination.rs deleted file mode 100644 index 7ae2c5e6f..000000000 --- a/crates/typst-pdf/src/named_destination.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::collections::{HashMap, HashSet}; - -use pdf_writer::writers::Destination; -use pdf_writer::{Ref, Str}; -use typst_library::diag::SourceResult; -use typst_library::foundations::{Label, NativeElement}; -use typst_library::introspection::Location; -use typst_library::layout::Abs; -use typst_library::model::HeadingElem; - -use crate::{AbsExt, PdfChunk, Renumber, StrExt, WithGlobalRefs}; - -/// A list of destinations in the PDF document (a specific point on a specific -/// page), that have a name associated with them. -/// -/// Typst creates a named destination for each heading in the document, that -/// will then be written in the document catalog. PDF readers can then display -/// them to show a clickable outline of the document. -#[derive(Default)] -pub struct NamedDestinations { - /// A map between elements and their associated labels - pub loc_to_dest: HashMap, - /// A sorted list of all named destinations. - pub dests: Vec<(Label, Ref)>, -} - -impl Renumber for NamedDestinations { - fn renumber(&mut self, offset: i32) { - for (_, reference) in &mut self.dests { - reference.renumber(offset); - } - } -} - -/// Fills in the map and vector for named destinations and writes the indirect -/// destination objects. -pub fn write_named_destinations( - context: &WithGlobalRefs, -) -> SourceResult<(PdfChunk, NamedDestinations)> { - let mut chunk = PdfChunk::new(); - let mut out = NamedDestinations::default(); - let mut seen = HashSet::new(); - - // Find all headings that have a label and are the first among other - // headings with the same label. - let mut matches: Vec<_> = context - .document - .introspector - .query(&HeadingElem::elem().select()) - .iter() - .filter_map(|elem| elem.location().zip(elem.label())) - .filter(|&(_, label)| seen.insert(label)) - .collect(); - - // Named destinations must be sorted by key. - matches.sort_by_key(|&(_, label)| label.resolve()); - - for (loc, label) in matches { - // Don't encode named destinations that would exceed the limit. Those - // will instead be encoded as normal links. - if label.resolve().len() > Str::PDFA_LIMIT { - continue; - } - - let pos = context.document.introspector.position(loc); - let index = pos.page.get() - 1; - let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); - - if let Some((Some(page), Some(page_ref))) = - context.pages.get(index).zip(context.globals.pages.get(index)) - { - let dest_ref = chunk.alloc(); - let x = pos.point.x.to_f32(); - let y = (page.content.size.y - y).to_f32(); - out.dests.push((label, dest_ref)); - out.loc_to_dest.insert(loc, label); - chunk - .indirect(dest_ref) - .start::() - .page(*page_ref) - .xyz(x, y, None); - } - } - - Ok((chunk, out)) -} diff --git a/crates/typst-pdf/src/outline.rs b/crates/typst-pdf/src/outline.rs index eff1182c1..e6324309f 100644 --- a/crates/typst-pdf/src/outline.rs +++ b/crates/typst-pdf/src/outline.rs @@ -1,18 +1,15 @@ use std::num::NonZeroUsize; -use pdf_writer::{Finish, Pdf, Ref, TextStr}; +use krilla::destination::XyzDestination; +use krilla::outline::{Outline, OutlineNode}; use typst_library::foundations::{NativeElement, Packed, StyleChain}; use typst_library::layout::Abs; use typst_library::model::HeadingElem; -use crate::{AbsExt, TextStrExt, WithEverything}; +use crate::convert::GlobalContext; +use crate::util::AbsExt; -/// Construct the outline for the document. -pub(crate) fn write_outline( - chunk: &mut Pdf, - alloc: &mut Ref, - ctx: &WithEverything, -) -> Option { +pub(crate) fn build_outline(gc: &GlobalContext) -> Outline { let mut tree: Vec = vec![]; // Stores the level of the topmost skipped ancestor of the next bookmarked @@ -21,14 +18,14 @@ pub(crate) fn write_outline( // Therefore, its next descendant must be added at its level, which is // enforced in the manner shown below. let mut last_skipped_level = None; - let elements = ctx.document.introspector.query(&HeadingElem::elem().select()); + let elements = &gc.document.introspector.query(&HeadingElem::elem().select()); for elem in elements.iter() { - if let Some(page_ranges) = &ctx.options.page_ranges { + if let Some(page_ranges) = &gc.options.page_ranges { if !page_ranges - .includes_page(ctx.document.introspector.page(elem.location().unwrap())) + .includes_page(gc.document.introspector.page(elem.location().unwrap())) { - // Don't bookmark headings in non-exported pages + // Don't bookmark headings in non-exported pages. continue; } } @@ -95,39 +92,15 @@ pub(crate) fn write_outline( } } - if tree.is_empty() { - return None; + let mut outline = Outline::new(); + + for child in convert_nodes(&tree, gc) { + outline.push_child(child); } - let root_id = alloc.bump(); - let start_ref = *alloc; - let len = tree.len(); - - let mut prev_ref = None; - for (i, node) in tree.iter().enumerate() { - prev_ref = Some(write_outline_item( - ctx, - chunk, - alloc, - node, - root_id, - prev_ref, - i + 1 == len, - )); - } - - chunk - .outline(root_id) - .first(start_ref) - .last(Ref::new( - alloc.get() - tree.last().map(|child| child.len() as i32).unwrap_or(1), - )) - .count(tree.len() as i32); - - Some(root_id) + outline } -/// A heading in the outline panel. #[derive(Debug)] struct HeadingNode<'a> { element: &'a Packed, @@ -149,73 +122,31 @@ impl<'a> HeadingNode<'a> { } } - fn len(&self) -> usize { - 1 + self.children.iter().map(Self::len).sum::() + fn to_krilla(&self, gc: &GlobalContext) -> Option { + let loc = self.element.location().unwrap(); + let title = self.element.body.plain_text().to_string(); + let pos = gc.document.introspector.position(loc); + let page_index = pos.page.get() - 1; + + if let Some(index) = gc.page_index_converter.pdf_page_index(page_index) { + let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); + let dest = XyzDestination::new( + index, + krilla::geom::Point::from_xy(pos.point.x.to_f32(), y.to_f32()), + ); + + let mut outline_node = OutlineNode::new(title, dest); + for child in convert_nodes(&self.children, gc) { + outline_node.push_child(child); + } + + return Some(outline_node); + } + + None } } -/// Write an outline item and all its children. -fn write_outline_item( - ctx: &WithEverything, - chunk: &mut Pdf, - alloc: &mut Ref, - node: &HeadingNode, - parent_ref: Ref, - prev_ref: Option, - is_last: bool, -) -> Ref { - let id = alloc.bump(); - let next_ref = Ref::new(id.get() + node.len() as i32); - - let mut outline = chunk.outline_item(id); - outline.parent(parent_ref); - - if !is_last { - outline.next(next_ref); - } - - if let Some(prev_rev) = prev_ref { - outline.prev(prev_rev); - } - - if let Some(last_immediate_child) = node.children.last() { - outline.first(Ref::new(id.get() + 1)); - outline.last(Ref::new(next_ref.get() - last_immediate_child.len() as i32)); - outline.count(-(node.children.len() as i32)); - } - - outline.title(TextStr::trimmed(node.element.body.plain_text().trim())); - - let loc = node.element.location().unwrap(); - let pos = ctx.document.introspector.position(loc); - let index = pos.page.get() - 1; - - // Don't link to non-exported pages. - if let Some((Some(page), Some(page_ref))) = - ctx.pages.get(index).zip(ctx.globals.pages.get(index)) - { - let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); - outline.dest().page(*page_ref).xyz( - pos.point.x.to_f32(), - (page.content.size.y - y).to_f32(), - None, - ); - } - - outline.finish(); - - let mut prev_ref = None; - for (i, child) in node.children.iter().enumerate() { - prev_ref = Some(write_outline_item( - ctx, - chunk, - alloc, - child, - id, - prev_ref, - i + 1 == node.children.len(), - )); - } - - id +fn convert_nodes(nodes: &[HeadingNode], gc: &GlobalContext) -> Vec { + nodes.iter().flat_map(|node| node.to_krilla(gc)).collect() } diff --git a/crates/typst-pdf/src/page.rs b/crates/typst-pdf/src/page.rs index 68125d29a..aa34400ef 100644 --- a/crates/typst-pdf/src/page.rs +++ b/crates/typst-pdf/src/page.rs @@ -1,310 +1,64 @@ -use std::collections::HashMap; -use std::num::NonZeroU64; +use std::num::NonZeroUsize; -use ecow::EcoString; -use pdf_writer::types::{ActionType, AnnotationFlags, AnnotationType, NumberingStyle}; -use pdf_writer::{Filter, Finish, Name, Rect, Ref, Str}; -use typst_library::diag::SourceResult; -use typst_library::foundations::Label; -use typst_library::introspection::Location; -use typst_library::layout::{Abs, Page}; -use typst_library::model::{Destination, Numbering}; +use krilla::page::{NumberingStyle, PageLabel}; +use typst_library::model::Numbering; -use crate::{ - content, AbsExt, PdfChunk, PdfOptions, Resources, WithDocument, WithRefs, - WithResources, -}; - -/// Construct page objects. -#[typst_macros::time(name = "construct pages")] -#[allow(clippy::type_complexity)] -pub fn traverse_pages( - state: &WithDocument, -) -> SourceResult<(PdfChunk, (Vec>, Resources<()>))> { - let mut resources = Resources::default(); - let mut pages = Vec::with_capacity(state.document.pages.len()); - let mut skipped_pages = 0; - for (i, page) in state.document.pages.iter().enumerate() { - if state - .options - .page_ranges - .as_ref() - .is_some_and(|ranges| !ranges.includes_page_index(i)) - { - // Don't export this page. - pages.push(None); - skipped_pages += 1; - } else { - let mut encoded = construct_page(state.options, &mut resources, page)?; - encoded.label = page - .numbering - .as_ref() - .and_then(|num| PdfPageLabel::generate(num, page.number)) - .or_else(|| { - // When some pages were ignored from export, we show a page label with - // the correct real (not logical) page number. - // This is for consistency with normal output when pages have no numbering - // and all are exported: the final PDF page numbers always correspond to - // the real (not logical) page numbers. Here, the final PDF page number - // will differ, but we can at least use labels to indicate what was - // the corresponding real page number in the Typst document. - (skipped_pages > 0).then(|| PdfPageLabel::arabic((i + 1) as u64)) - }); - pages.push(Some(encoded)); - } - } - - Ok((PdfChunk::new(), (pages, resources))) -} - -/// Construct a page object. -#[typst_macros::time(name = "construct page")] -fn construct_page( - options: &PdfOptions, - out: &mut Resources<()>, - page: &Page, -) -> SourceResult { - Ok(EncodedPage { - content: content::build( - options, - out, - &page.frame, - page.fill_or_transparent(), - None, - )?, - label: None, - }) -} - -/// Allocate a reference for each exported page. -pub fn alloc_page_refs( - context: &WithResources, -) -> SourceResult<(PdfChunk, Vec>)> { - let mut chunk = PdfChunk::new(); - let page_refs = context - .pages - .iter() - .map(|p| p.as_ref().map(|_| chunk.alloc())) - .collect(); - Ok((chunk, page_refs)) -} - -/// Write the page tree. -pub fn write_page_tree(ctx: &WithRefs) -> SourceResult<(PdfChunk, Ref)> { - let mut chunk = PdfChunk::new(); - let page_tree_ref = chunk.alloc.bump(); - - for i in 0..ctx.pages.len() { - let content_id = chunk.alloc.bump(); - write_page( - &mut chunk, - ctx, - content_id, - page_tree_ref, - &ctx.references.named_destinations.loc_to_dest, - i, - ); - } - - let page_kids = ctx.globals.pages.iter().filter_map(Option::as_ref).copied(); - - chunk - .pages(page_tree_ref) - .count(page_kids.clone().count() as i32) - .kids(page_kids); - - Ok((chunk, page_tree_ref)) -} - -/// Write a page tree node. -fn write_page( - chunk: &mut PdfChunk, - ctx: &WithRefs, - content_id: Ref, - page_tree_ref: Ref, - loc_to_dest: &HashMap, - i: usize, -) { - let Some((page, page_ref)) = ctx.pages[i].as_ref().zip(ctx.globals.pages[i]) else { - // Page excluded from export. - return; - }; - - let mut annotations = Vec::with_capacity(page.content.links.len()); - for (dest, rect) in &page.content.links { - let id = chunk.alloc(); - annotations.push(id); - - let mut annotation = chunk.annotation(id); - annotation.subtype(AnnotationType::Link).rect(*rect); - annotation.border(0.0, 0.0, 0.0, None).flags(AnnotationFlags::PRINT); - - let pos = match dest { - Destination::Url(uri) => { - annotation - .action() - .action_type(ActionType::Uri) - .uri(Str(uri.as_bytes())); - continue; - } - Destination::Position(pos) => *pos, - Destination::Location(loc) => { - if let Some(key) = loc_to_dest.get(loc) { - annotation - .action() - .action_type(ActionType::GoTo) - // `key` must be a `Str`, not a `Name`. - .pair(Name(b"D"), Str(key.resolve().as_bytes())); - continue; - } else { - ctx.document.introspector.position(*loc) - } - } - }; - - let index = pos.page.get() - 1; - let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); - - // Don't add links to non-exported pages. - if let Some((Some(page), Some(page_ref))) = - ctx.pages.get(index).zip(ctx.globals.pages.get(index)) - { - annotation - .action() - .action_type(ActionType::GoTo) - .destination() - .page(*page_ref) - .xyz(pos.point.x.to_f32(), (page.content.size.y - y).to_f32(), None); - } - } - - let mut page_writer = chunk.page(page_ref); - page_writer.parent(page_tree_ref); - - let w = page.content.size.x.to_f32(); - let h = page.content.size.y.to_f32(); - page_writer.media_box(Rect::new(0.0, 0.0, w, h)); - page_writer.contents(content_id); - page_writer.pair(Name(b"Resources"), ctx.resources.reference); - - if page.content.uses_opacities { - page_writer - .group() - .transparency() - .isolated(false) - .knockout(false) - .color_space() - .srgb(); - } - - page_writer.annotations(annotations); - - page_writer.finish(); - - chunk - .stream(content_id, page.content.content.wait()) - .filter(Filter::FlateDecode); -} - -/// Specification for a PDF page label. -#[derive(Debug, Clone, PartialEq, Hash, Default)] -pub(crate) struct PdfPageLabel { - /// Can be any string or none. Will always be prepended to the numbering style. - pub prefix: Option, - /// Based on the numbering pattern. - /// - /// If `None` or numbering is a function, the field will be empty. - pub style: Option, - /// Offset for the page label start. - /// - /// Describes where to start counting from when setting a style. - /// (Has to be greater or equal than 1) - pub offset: Option, -} - -/// A PDF page label number style. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum PdfPageLabelStyle { - /// Decimal arabic numerals (1, 2, 3). - Arabic, - /// Lowercase roman numerals (i, ii, iii). - LowerRoman, - /// Uppercase roman numerals (I, II, III). - UpperRoman, - /// Lowercase letters (`a` to `z` for the first 26 pages, - /// `aa` to `zz` and so on for the next). - LowerAlpha, - /// Uppercase letters (`A` to `Z` for the first 26 pages, - /// `AA` to `ZZ` and so on for the next). - UpperAlpha, -} - -impl PdfPageLabel { - /// Create a new `PdfNumbering` from a `Numbering` applied to a page +pub(crate) trait PageLabelExt { + /// Create a new `PageLabel` from a `Numbering` applied to a page /// number. - fn generate(numbering: &Numbering, number: u64) -> Option { - let Numbering::Pattern(pat) = numbering else { - return None; - }; - - let (prefix, kind) = pat.pieces.first()?; - - // If there is a suffix, we cannot use the common style optimisation, - // since PDF does not provide a suffix field. - let style = if pat.suffix.is_empty() { - use typst_library::model::NumberingKind as Kind; - use PdfPageLabelStyle as Style; - match kind { - Kind::Arabic => Some(Style::Arabic), - Kind::LowerRoman => Some(Style::LowerRoman), - Kind::UpperRoman => Some(Style::UpperRoman), - Kind::LowerLatin if number <= 26 => Some(Style::LowerAlpha), - Kind::LowerLatin if number <= 26 => Some(Style::UpperAlpha), - _ => None, - } - } else { - None - }; - - // Prefix and offset depend on the style: If it is supported by the PDF - // spec, we use the given prefix and an offset. Otherwise, everything - // goes into prefix. - let prefix = if style.is_none() { - Some(pat.apply(&[number])) - } else { - (!prefix.is_empty()).then(|| prefix.clone()) - }; - - let offset = style.and(NonZeroU64::new(number)); - Some(PdfPageLabel { prefix, style, offset }) - } + fn generate(numbering: &Numbering, number: u64) -> Option; /// Creates an arabic page label with the specified page number. /// For example, this will display page label `11` when given the page /// number 11. - fn arabic(number: u64) -> PdfPageLabel { - PdfPageLabel { - prefix: None, - style: Some(PdfPageLabelStyle::Arabic), - offset: NonZeroU64::new(number), - } - } + fn arabic(number: u64) -> PageLabel; } -impl PdfPageLabelStyle { - pub fn to_pdf_numbering_style(self) -> NumberingStyle { - match self { - PdfPageLabelStyle::Arabic => NumberingStyle::Arabic, - PdfPageLabelStyle::LowerRoman => NumberingStyle::LowerRoman, - PdfPageLabelStyle::UpperRoman => NumberingStyle::UpperRoman, - PdfPageLabelStyle::LowerAlpha => NumberingStyle::LowerAlpha, - PdfPageLabelStyle::UpperAlpha => NumberingStyle::UpperAlpha, +impl PageLabelExt for PageLabel { + fn generate(numbering: &Numbering, number: u64) -> Option { + { + let Numbering::Pattern(pat) = numbering else { + return None; + }; + + let (prefix, kind) = pat.pieces.first()?; + + // If there is a suffix, we cannot use the common style optimisation, + // since PDF does not provide a suffix field. + let style = if pat.suffix.is_empty() { + use krilla::page::NumberingStyle as Style; + use typst_library::model::NumberingKind as Kind; + match kind { + Kind::Arabic => Some(Style::Arabic), + Kind::LowerRoman => Some(Style::LowerRoman), + Kind::UpperRoman => Some(Style::UpperRoman), + Kind::LowerLatin if number <= 26 => Some(Style::LowerAlpha), + Kind::LowerLatin if number <= 26 => Some(Style::UpperAlpha), + _ => None, + } + } else { + None + }; + + // Prefix and offset depend on the style: If it is supported by the PDF + // spec, we use the given prefix and an offset. Otherwise, everything + // goes into prefix. + let prefix = if style.is_none() { + Some(pat.apply(&[number])) + } else { + (!prefix.is_empty()).then(|| prefix.clone()) + }; + + let offset = style.and(number.try_into().ok().and_then(NonZeroUsize::new)); + Some(PageLabel::new(style, prefix.map(|s| s.to_string()), offset)) } } -} -/// Data for an exported page. -pub struct EncodedPage { - pub content: content::Encoded, - pub label: Option, + fn arabic(number: u64) -> PageLabel { + PageLabel::new( + Some(NumberingStyle::Arabic), + None, + number.try_into().ok().and_then(NonZeroUsize::new), + ) + } } diff --git a/crates/typst-pdf/src/paint.rs b/crates/typst-pdf/src/paint.rs new file mode 100644 index 000000000..5224464ab --- /dev/null +++ b/crates/typst-pdf/src/paint.rs @@ -0,0 +1,379 @@ +//! Convert paint types from typst to krilla. + +use krilla::color::{self, cmyk, luma, rgb}; +use krilla::num::NormalizedF32; +use krilla::paint::{ + Fill, LinearGradient, Pattern, RadialGradient, SpreadMethod, Stop, Stroke, + StrokeDash, SweepGradient, +}; +use krilla::surface::Surface; +use typst_library::diag::SourceResult; +use typst_library::layout::{Abs, Angle, Quadrant, Ratio, Size, Transform}; +use typst_library::visualize::{ + Color, ColorSpace, DashPattern, FillRule, FixedStroke, Gradient, Paint, RatioOrAngle, + RelativeTo, Tiling, WeightedColor, +}; +use typst_utils::Numeric; + +use crate::convert::{handle_frame, FrameContext, GlobalContext, State}; +use crate::util::{AbsExt, FillRuleExt, LineCapExt, LineJoinExt, TransformExt}; + +pub(crate) fn convert_fill( + gc: &mut GlobalContext, + paint_: &Paint, + fill_rule_: FillRule, + on_text: bool, + surface: &mut Surface, + state: &State, + size: Size, +) -> SourceResult { + let (paint, opacity) = convert_paint(gc, paint_, on_text, surface, state, size)?; + + Ok(Fill { + paint, + rule: fill_rule_.to_krilla(), + opacity: NormalizedF32::new(opacity as f32 / 255.0).unwrap(), + }) +} + +pub(crate) fn convert_stroke( + fc: &mut GlobalContext, + stroke: &FixedStroke, + on_text: bool, + surface: &mut Surface, + state: &State, + size: Size, +) -> SourceResult { + let (paint, opacity) = + convert_paint(fc, &stroke.paint, on_text, surface, state, size)?; + + Ok(Stroke { + paint, + width: stroke.thickness.to_f32(), + miter_limit: stroke.miter_limit.get() as f32, + line_join: stroke.join.to_krilla(), + line_cap: stroke.cap.to_krilla(), + opacity: NormalizedF32::new(opacity as f32 / 255.0).unwrap(), + dash: stroke.dash.as_ref().map(convert_dash), + }) +} + +fn convert_paint( + gc: &mut GlobalContext, + paint: &Paint, + on_text: bool, + surface: &mut Surface, + state: &State, + mut size: Size, +) -> SourceResult<(krilla::paint::Paint, u8)> { + // Edge cases for strokes. + if size.x.is_zero() { + size.x = Abs::pt(1.0); + } + + if size.y.is_zero() { + size.y = Abs::pt(1.0); + } + + match paint { + Paint::Solid(c) => { + let (c, a) = convert_solid(c); + Ok((c.into(), a)) + } + Paint::Gradient(g) => Ok(convert_gradient(g, on_text, state, size)), + Paint::Tiling(p) => convert_pattern(gc, p, on_text, surface, state), + } +} + +fn convert_solid(color: &Color) -> (color::Color, u8) { + match color.space() { + ColorSpace::D65Gray => { + let (c, a) = convert_luma(color); + (c.into(), a) + } + ColorSpace::Cmyk => (convert_cmyk(color).into(), 255), + // Convert all other colors in different colors spaces into RGB. + _ => { + let (c, a) = convert_rgb(color); + (c.into(), a) + } + } +} + +fn convert_cmyk(color: &Color) -> cmyk::Color { + let components = color.to_space(ColorSpace::Cmyk).to_vec4_u8(); + + cmyk::Color::new(components[0], components[1], components[2], components[3]) +} + +fn convert_rgb(color: &Color) -> (rgb::Color, u8) { + let components = color.to_space(ColorSpace::Srgb).to_vec4_u8(); + (rgb::Color::new(components[0], components[1], components[2]), components[3]) +} + +fn convert_luma(color: &Color) -> (luma::Color, u8) { + let components = color.to_space(ColorSpace::D65Gray).to_vec4_u8(); + (luma::Color::new(components[0]), components[3]) +} + +fn convert_pattern( + gc: &mut GlobalContext, + pattern: &Tiling, + on_text: bool, + surface: &mut Surface, + state: &State, +) -> SourceResult<(krilla::paint::Paint, u8)> { + let transform = correct_transform(state, pattern.unwrap_relative(on_text)); + + let mut stream_builder = surface.stream_builder(); + let mut surface = stream_builder.surface(); + let mut fc = FrameContext::new(pattern.frame().size()); + handle_frame(&mut fc, pattern.frame(), None, &mut surface, gc)?; + surface.finish(); + let stream = stream_builder.finish(); + let pattern = Pattern { + stream, + transform: transform.to_krilla(), + width: (pattern.size().x + pattern.spacing().x).to_pt() as _, + height: (pattern.size().y + pattern.spacing().y).to_pt() as _, + }; + + Ok((pattern.into(), 255)) +} + +fn convert_gradient( + gradient: &Gradient, + on_text: bool, + state: &State, + size: Size, +) -> (krilla::paint::Paint, u8) { + let size = match gradient.unwrap_relative(on_text) { + RelativeTo::Self_ => size, + RelativeTo::Parent => state.container_size(), + }; + + let angle = gradient.angle().unwrap_or_else(Angle::zero); + let base_transform = correct_transform(state, gradient.unwrap_relative(on_text)); + let stops = convert_gradient_stops(gradient); + match &gradient { + Gradient::Linear(_) => { + let (x1, y1, x2, y2) = { + let (mut sin, mut cos) = (angle.sin(), angle.cos()); + + // Scale to edges of unit square. + let factor = cos.abs() + sin.abs(); + sin *= factor; + cos *= factor; + + match angle.quadrant() { + Quadrant::First => (0.0, 0.0, cos as f32, sin as f32), + Quadrant::Second => (1.0, 0.0, cos as f32 + 1.0, sin as f32), + Quadrant::Third => (1.0, 1.0, cos as f32 + 1.0, sin as f32 + 1.0), + Quadrant::Fourth => (0.0, 1.0, cos as f32, sin as f32 + 1.0), + } + }; + + let linear = LinearGradient { + x1, + y1, + x2, + y2, + // x and y coordinates are normalized, so need to scale by the size. + transform: base_transform + .pre_concat(Transform::scale( + Ratio::new(size.x.to_f32() as f64), + Ratio::new(size.y.to_f32() as f64), + )) + .to_krilla(), + spread_method: SpreadMethod::Pad, + stops, + anti_alias: gradient.anti_alias(), + }; + + (linear.into(), 255) + } + Gradient::Radial(radial) => { + let radial = RadialGradient { + fx: radial.focal_center.x.get() as f32, + fy: radial.focal_center.y.get() as f32, + fr: radial.focal_radius.get() as f32, + cx: radial.center.x.get() as f32, + cy: radial.center.y.get() as f32, + cr: radial.radius.get() as f32, + transform: base_transform + .pre_concat(Transform::scale( + Ratio::new(size.x.to_f32() as f64), + Ratio::new(size.y.to_f32() as f64), + )) + .to_krilla(), + spread_method: SpreadMethod::Pad, + stops, + anti_alias: gradient.anti_alias(), + }; + + (radial.into(), 255) + } + Gradient::Conic(conic) => { + // Correct the gradient's angle. + let cx = size.x.to_f32() * conic.center.x.get() as f32; + let cy = size.y.to_f32() * conic.center.y.get() as f32; + let actual_transform = base_transform + // Adjust for the angle. + .pre_concat(Transform::rotate_at( + angle, + Abs::pt(cx as f64), + Abs::pt(cy as f64), + )) + // Default start point in krilla and typst are at the opposite side, so we need + // to flip it horizontally. + .pre_concat(Transform::scale_at( + -Ratio::one(), + Ratio::one(), + Abs::pt(cx as f64), + Abs::pt(cy as f64), + )); + + let sweep = SweepGradient { + cx, + cy, + start_angle: 0.0, + end_angle: 360.0, + transform: actual_transform.to_krilla(), + spread_method: SpreadMethod::Pad, + stops, + anti_alias: gradient.anti_alias(), + }; + + (sweep.into(), 255) + } + } +} + +fn convert_gradient_stops(gradient: &Gradient) -> Vec { + let mut stops = vec![]; + + let use_cmyk = gradient.stops().iter().all(|s| s.color.space() == ColorSpace::Cmyk); + + let mut add_single = |color: &Color, offset: Ratio| { + let (color, opacity) = if use_cmyk { + (convert_cmyk(color).into(), 255) + } else { + let (c, a) = convert_rgb(color); + (c.into(), a) + }; + + let opacity = NormalizedF32::new((opacity as f32) / 255.0).unwrap(); + let offset = NormalizedF32::new(offset.get() as f32).unwrap(); + let stop = Stop { offset, color, opacity }; + stops.push(stop); + }; + + // Convert stops. + match &gradient { + Gradient::Linear(_) | Gradient::Radial(_) => { + if let Some(s) = gradient.stops().first() { + add_single(&s.color, s.offset.unwrap()); + } + + // Create the individual gradient functions for each pair of stops. + for window in gradient.stops().windows(2) { + let (first, second) = (window[0], window[1]); + + // If we have a hue index or are using Oklab, we will create several + // stops in-between to make the gradient smoother without interpolation + // issues with native color spaces. + if gradient.space().hue_index().is_some() { + for i in 0..=32 { + let t = i as f64 / 32.0; + let real_t = Ratio::new( + first.offset.unwrap().get() * (1.0 - t) + + second.offset.unwrap().get() * t, + ); + + let c = gradient.sample(RatioOrAngle::Ratio(real_t)); + add_single(&c, real_t); + } + } + + add_single(&second.color, second.offset.unwrap()); + } + } + Gradient::Conic(conic) => { + if let Some((c, t)) = conic.stops.first() { + add_single(c, *t); + } + + for window in conic.stops.windows(2) { + let ((c0, t0), (c1, t1)) = (window[0], window[1]); + + // Precision: + // - On an even color, insert a stop every 90deg. + // - For a hue-based color space, insert 200 stops minimum. + // - On any other, insert 20 stops minimum. + let max_dt = if c0 == c1 { + 0.25 + } else if conic.space.hue_index().is_some() { + 0.005 + } else { + 0.05 + }; + + let mut t_x = t0.get(); + let dt = (t1.get() - t0.get()).min(max_dt); + + // Special casing for sharp gradients. + if t0 == t1 { + add_single(&c1, t1); + continue; + } + + while t_x < t1.get() { + let t_next = (t_x + dt).min(t1.get()); + + // The current progress in the current window. + let t = |t| (t - t0.get()) / (t1.get() - t0.get()); + + let c_next = Color::mix_iter( + [ + WeightedColor::new(c0, 1.0 - t(t_next)), + WeightedColor::new(c1, t(t_next)), + ], + conic.space, + ) + .unwrap(); + + add_single(&c_next, Ratio::new(t_next)); + t_x = t_next; + } + + add_single(&c1, t1); + } + } + } + + stops +} + +fn convert_dash(dash: &DashPattern) -> StrokeDash { + StrokeDash { + array: dash.array.iter().map(|e| e.to_f32()).collect(), + offset: dash.phase.to_f32(), + } +} + +fn correct_transform(state: &State, relative: RelativeTo) -> Transform { + // In krilla, if we have a shape with a transform and a complex paint, + // then the paint will inherit the transform of the shape. + match relative { + // Because of the above, we don't need to apply an additional transform here. + RelativeTo::Self_ => Transform::identity(), + // Because of the above, we need to first reverse the transform that will be + // applied from the shape, and then re-apply the transform that is used for + // the next parent container. + RelativeTo::Parent => state + .transform() + .invert() + .unwrap() + .pre_concat(state.container_transform()), + } +} diff --git a/crates/typst-pdf/src/resources.rs b/crates/typst-pdf/src/resources.rs deleted file mode 100644 index bdbf2f1e4..000000000 --- a/crates/typst-pdf/src/resources.rs +++ /dev/null @@ -1,349 +0,0 @@ -//! PDF resources. -//! -//! Resources are defined in dictionaries. They map identifiers such as `Im0` to -//! a PDF reference. Each [content stream] is associated with a resource dictionary. -//! The identifiers defined in the resources can then be used in content streams. -//! -//! [content stream]: `crate::content` - -use std::collections::{BTreeMap, HashMap}; -use std::hash::Hash; - -use ecow::{eco_format, EcoString}; -use pdf_writer::{Dict, Finish, Name, Ref}; -use subsetter::GlyphRemapper; -use typst_library::diag::{SourceResult, StrResult}; -use typst_library::text::{Font, Lang}; -use typst_library::visualize::Image; -use typst_syntax::Span; -use typst_utils::Deferred; - -use crate::color::ColorSpaces; -use crate::color_font::ColorFontMap; -use crate::extg::ExtGState; -use crate::gradient::PdfGradient; -use crate::image::EncodedImage; -use crate::tiling::TilingRemapper; -use crate::{PdfChunk, Renumber, WithEverything, WithResources}; - -/// All the resources that have been collected when traversing the document. -/// -/// This does not allocate references to resources, only track what was used -/// and deduplicate what can be deduplicated. -/// -/// You may notice that this structure is a tree: [`TilingRemapper`] and -/// [`ColorFontMap`] (that are present in the fields of [`Resources`]), -/// themselves contain [`Resources`] (that will be called "sub-resources" from -/// now on). Because color glyphs and tilings are defined using content -/// streams, just like pages, they can refer to resources too, which are tracked -/// by the respective sub-resources. -/// -/// Each instance of this structure will become a `/Resources` dictionary in -/// the final PDF. It is not possible to use a single shared dictionary for all -/// pages, tilings and color fonts, because if a resource is listed in its own -/// `/Resources` dictionary, some PDF readers will fail to open the document. -/// -/// Because we need to lazily initialize sub-resources (we don't know how deep -/// the tree will be before reading the document), and that this is done in a -/// context where no PDF reference allocator is available, `Resources` are -/// originally created with the type parameter `R = ()`. The reference for each -/// dictionary will only be allocated in the next phase, once we know the shape -/// of the tree, at which point `R` becomes `Ref`. No other value of `R` should -/// ever exist. -pub struct Resources { - /// The global reference to this resource dictionary, or `()` if it has not - /// been allocated yet. - pub reference: R, - - /// Handles color space writing. - pub colors: ColorSpaces, - - /// Deduplicates fonts used across the document. - pub fonts: Remapper, - /// Deduplicates images used across the document. - pub images: Remapper, - /// Handles to deferred image conversions. - pub deferred_images: HashMap>, Span)>, - /// Deduplicates gradients used across the document. - pub gradients: Remapper, - /// Deduplicates tilings used across the document. - pub tilings: Option>>, - /// Deduplicates external graphics states used across the document. - pub ext_gs: Remapper, - /// Deduplicates color glyphs. - pub color_fonts: Option>>, - - // The fields below do not correspond to actual resources that will be - // written in a dictionary, but are more meta-data about resources that - // can't really live somewhere else. - /// The number of glyphs for all referenced languages in the content stream. - /// We keep track of this to determine the main document language. - /// BTreeMap is used to write sorted list of languages to metadata. - pub languages: BTreeMap, - - /// For each font a mapping from used glyphs to their text representation. - /// This is used for the PDF's /ToUnicode map, and important for copy-paste - /// and searching. - /// - /// Note that the text representation may contain multiple chars in case of - /// ligatures or similar things, and it may have no entry in the font's cmap - /// (or only a private-use codepoint), like the “Th” in Linux Libertine. - /// - /// A glyph may have multiple entries in the font's cmap, and even the same - /// glyph can have a different text representation within one document. - /// But /ToUnicode does not support that, so we just save the first occurrence. - pub glyph_sets: HashMap>, - /// Same as `glyph_sets`, but for color fonts. - pub color_glyph_sets: HashMap>, - /// Stores the glyph remapper for each font for the subsetter. - pub glyph_remappers: HashMap, -} - -impl Renumber for Resources { - fn renumber(&mut self, offset: i32) { - self.reference.renumber(offset); - - if let Some(color_fonts) = &mut self.color_fonts { - color_fonts.resources.renumber(offset); - } - - if let Some(tilings) = &mut self.tilings { - tilings.resources.renumber(offset); - } - } -} - -impl Default for Resources<()> { - fn default() -> Self { - Resources { - reference: (), - colors: ColorSpaces::default(), - fonts: Remapper::new("F"), - images: Remapper::new("Im"), - deferred_images: HashMap::new(), - gradients: Remapper::new("Gr"), - tilings: None, - ext_gs: Remapper::new("Gs"), - color_fonts: None, - languages: BTreeMap::new(), - glyph_sets: HashMap::new(), - color_glyph_sets: HashMap::new(), - glyph_remappers: HashMap::new(), - } - } -} - -impl Resources<()> { - /// Associate a reference with this resource dictionary (and do so - /// recursively for sub-resources). - pub fn with_refs(self, refs: &ResourcesRefs) -> Resources { - Resources { - reference: refs.reference, - colors: self.colors, - fonts: self.fonts, - images: self.images, - deferred_images: self.deferred_images, - gradients: self.gradients, - tilings: self - .tilings - .zip(refs.tilings.as_ref()) - .map(|(p, r)| Box::new(p.with_refs(r))), - ext_gs: self.ext_gs, - color_fonts: self - .color_fonts - .zip(refs.color_fonts.as_ref()) - .map(|(c, r)| Box::new(c.with_refs(r))), - languages: self.languages, - glyph_sets: self.glyph_sets, - color_glyph_sets: self.color_glyph_sets, - glyph_remappers: self.glyph_remappers, - } - } -} - -impl Resources { - /// Run a function on this resource dictionary and all - /// of its sub-resources. - pub fn traverse

(&self, process: &mut P) -> SourceResult<()> - where - P: FnMut(&Self) -> SourceResult<()>, - { - process(self)?; - if let Some(color_fonts) = &self.color_fonts { - color_fonts.resources.traverse(process)?; - } - if let Some(tilings) = &self.tilings { - tilings.resources.traverse(process)?; - } - Ok(()) - } -} - -/// References for a resource tree. -/// -/// This structure is a tree too, that should have the same structure as the -/// corresponding `Resources`. -pub struct ResourcesRefs { - pub reference: Ref, - pub color_fonts: Option>, - pub tilings: Option>, -} - -impl Renumber for ResourcesRefs { - fn renumber(&mut self, offset: i32) { - self.reference.renumber(offset); - if let Some(color_fonts) = &mut self.color_fonts { - color_fonts.renumber(offset); - } - if let Some(tilings) = &mut self.tilings { - tilings.renumber(offset); - } - } -} - -/// Allocate references for all resource dictionaries. -pub fn alloc_resources_refs( - context: &WithResources, -) -> SourceResult<(PdfChunk, ResourcesRefs)> { - let mut chunk = PdfChunk::new(); - /// Recursively explore resource dictionaries and assign them references. - fn refs_for(resources: &Resources<()>, chunk: &mut PdfChunk) -> ResourcesRefs { - ResourcesRefs { - reference: chunk.alloc(), - color_fonts: resources - .color_fonts - .as_ref() - .map(|c| Box::new(refs_for(&c.resources, chunk))), - tilings: resources - .tilings - .as_ref() - .map(|p| Box::new(refs_for(&p.resources, chunk))), - } - } - - let refs = refs_for(&context.resources, &mut chunk); - Ok((chunk, refs)) -} - -/// Write the resource dictionaries that will be referenced by all pages. -/// -/// We add a reference to this dictionary to each page individually instead of -/// to the root node of the page tree because using the resource inheritance -/// feature breaks PDF merging with Apple Preview. -/// -/// Also write resource dictionaries for Type3 fonts and PDF patterns. -pub fn write_resource_dictionaries(ctx: &WithEverything) -> SourceResult<(PdfChunk, ())> { - let mut chunk = PdfChunk::new(); - let mut used_color_spaces = ColorSpaces::default(); - - ctx.resources.traverse(&mut |resources| { - used_color_spaces.merge(&resources.colors); - - let images_ref = chunk.alloc.bump(); - let patterns_ref = chunk.alloc.bump(); - let ext_gs_states_ref = chunk.alloc.bump(); - let color_spaces_ref = chunk.alloc.bump(); - - let mut color_font_slices = Vec::new(); - let mut color_font_numbers = HashMap::new(); - if let Some(color_fonts) = &resources.color_fonts { - for (_, font_slice) in color_fonts.iter() { - color_font_numbers.insert(font_slice.clone(), color_font_slices.len()); - color_font_slices.push(font_slice); - } - } - let color_font_remapper = Remapper { - prefix: "Cf", - to_pdf: color_font_numbers, - to_items: color_font_slices, - }; - - resources - .images - .write(&ctx.references.images, &mut chunk.indirect(images_ref).dict()); - - let mut patterns_dict = chunk.indirect(patterns_ref).dict(); - resources - .gradients - .write(&ctx.references.gradients, &mut patterns_dict); - if let Some(p) = &resources.tilings { - p.remapper.write(&ctx.references.tilings, &mut patterns_dict); - } - patterns_dict.finish(); - - resources - .ext_gs - .write(&ctx.references.ext_gs, &mut chunk.indirect(ext_gs_states_ref).dict()); - - let mut res_dict = chunk - .indirect(resources.reference) - .start::(); - res_dict.pair(Name(b"XObject"), images_ref); - res_dict.pair(Name(b"Pattern"), patterns_ref); - res_dict.pair(Name(b"ExtGState"), ext_gs_states_ref); - res_dict.pair(Name(b"ColorSpace"), color_spaces_ref); - - // TODO: can't this be an indirect reference too? - let mut fonts_dict = res_dict.fonts(); - resources.fonts.write(&ctx.references.fonts, &mut fonts_dict); - color_font_remapper.write(&ctx.references.color_fonts, &mut fonts_dict); - fonts_dict.finish(); - - res_dict.finish(); - - let color_spaces = chunk.indirect(color_spaces_ref).dict(); - resources - .colors - .write_color_spaces(color_spaces, &ctx.globals.color_functions); - - Ok(()) - })?; - - used_color_spaces.write_functions(&mut chunk, &ctx.globals.color_functions); - - Ok((chunk, ())) -} - -/// Assigns new, consecutive PDF-internal indices to items. -pub struct Remapper { - /// The prefix to use when naming these resources. - prefix: &'static str, - /// Forwards from the items to the pdf indices. - to_pdf: HashMap, - /// Backwards from the pdf indices to the items. - to_items: Vec, -} - -impl Remapper -where - T: Eq + Hash + Clone, -{ - /// Create an empty mapping. - pub fn new(prefix: &'static str) -> Self { - Self { prefix, to_pdf: HashMap::new(), to_items: vec![] } - } - - /// Insert an item in the mapping if it was not already present. - pub fn insert(&mut self, item: T) -> usize { - let to_layout = &mut self.to_items; - *self.to_pdf.entry(item.clone()).or_insert_with(|| { - let pdf_index = to_layout.len(); - to_layout.push(item); - pdf_index - }) - } - - /// All items in this - pub fn items(&self) -> impl Iterator + '_ { - self.to_items.iter() - } - - /// Write this list of items in a Resource dictionary. - fn write(&self, mapping: &HashMap, dict: &mut Dict) { - for (number, item) in self.items().enumerate() { - let name = eco_format!("{}{}", self.prefix, number); - let reference = mapping[item]; - dict.pair(Name(name.as_bytes()), reference); - } - } -} diff --git a/crates/typst-pdf/src/shape.rs b/crates/typst-pdf/src/shape.rs new file mode 100644 index 000000000..5b9232dbe --- /dev/null +++ b/crates/typst-pdf/src/shape.rs @@ -0,0 +1,106 @@ +use krilla::geom::{Path, PathBuilder, Rect}; +use krilla::surface::Surface; +use typst_library::diag::SourceResult; +use typst_library::visualize::{Geometry, Shape}; +use typst_syntax::Span; + +use crate::convert::{FrameContext, GlobalContext}; +use crate::paint; +use crate::util::{convert_path, AbsExt, TransformExt}; + +#[typst_macros::time(name = "handle shape")] +pub(crate) fn handle_shape( + fc: &mut FrameContext, + shape: &Shape, + surface: &mut Surface, + gc: &mut GlobalContext, + span: Span, +) -> SourceResult<()> { + surface.set_location(span.into_raw().get()); + surface.push_transform(&fc.state().transform().to_krilla()); + + if let Some(path) = convert_geometry(&shape.geometry) { + let fill = if let Some(paint) = &shape.fill { + Some(paint::convert_fill( + gc, + paint, + shape.fill_rule, + false, + surface, + fc.state(), + shape.geometry.bbox_size(), + )?) + } else { + None + }; + + let stroke = shape.stroke.as_ref().and_then(|stroke| { + if stroke.thickness.to_f32() > 0.0 { + Some(stroke) + } else { + None + } + }); + + let stroke = if let Some(stroke) = &stroke { + let stroke = paint::convert_stroke( + gc, + stroke, + false, + surface, + fc.state(), + shape.geometry.bbox_size(), + )?; + + Some(stroke) + } else { + None + }; + + // Otherwise, krilla will by default fill with a black paint. + if fill.is_some() || stroke.is_some() { + surface.set_fill(fill); + surface.set_stroke(stroke); + surface.draw_path(&path); + } + } + + surface.pop(); + surface.reset_location(); + + Ok(()) +} + +fn convert_geometry(geometry: &Geometry) -> Option { + let mut path_builder = PathBuilder::new(); + + match geometry { + Geometry::Line(l) => { + path_builder.move_to(0.0, 0.0); + path_builder.line_to(l.x.to_f32(), l.y.to_f32()); + } + Geometry::Rect(size) => { + let w = size.x.to_f32(); + let h = size.y.to_f32(); + let rect = if w < 0.0 || h < 0.0 { + // krilla doesn't normally allow for negative dimensions, but + // Typst supports them, so we apply a transform if needed. + let transform = + krilla::geom::Transform::from_scale(w.signum(), h.signum()); + Rect::from_xywh(0.0, 0.0, w.abs(), h.abs()) + .and_then(|rect| rect.transform(transform)) + } else { + Rect::from_xywh(0.0, 0.0, w, h) + }; + + if let Some(rect) = rect { + path_builder.push_rect(rect); + } + } + Geometry::Curve(c) => { + convert_path(c, &mut path_builder); + } + } + + path_builder.finish() +} diff --git a/crates/typst-pdf/src/text.rs b/crates/typst-pdf/src/text.rs new file mode 100644 index 000000000..8d532e08c --- /dev/null +++ b/crates/typst-pdf/src/text.rs @@ -0,0 +1,135 @@ +use std::ops::Range; +use std::sync::Arc; + +use bytemuck::TransparentWrapper; +use krilla::surface::{Location, Surface}; +use krilla::text::GlyphId; +use typst_library::diag::{bail, SourceResult}; +use typst_library::layout::Size; +use typst_library::text::{Font, Glyph, TextItem}; +use typst_library::visualize::FillRule; +use typst_syntax::Span; + +use crate::convert::{FrameContext, GlobalContext}; +use crate::paint; +use crate::util::{display_font, AbsExt, TransformExt}; + +#[typst_macros::time(name = "handle text")] +pub(crate) fn handle_text( + fc: &mut FrameContext, + t: &TextItem, + surface: &mut Surface, + gc: &mut GlobalContext, +) -> SourceResult<()> { + *gc.languages.entry(t.lang).or_insert(0) += t.glyphs.len(); + + let font = convert_font(gc, t.font.clone())?; + let fill = paint::convert_fill( + gc, + &t.fill, + FillRule::NonZero, + true, + surface, + fc.state(), + Size::zero(), + )?; + let stroke = + if let Some(stroke) = t.stroke.as_ref().map(|s| { + paint::convert_stroke(gc, s, true, surface, fc.state(), Size::zero()) + }) { + Some(stroke?) + } else { + None + }; + let text = t.text.as_str(); + let size = t.size; + let glyphs: &[PdfGlyph] = TransparentWrapper::wrap_slice(t.glyphs.as_slice()); + + surface.push_transform(&fc.state().transform().to_krilla()); + surface.set_fill(Some(fill)); + surface.set_stroke(stroke); + surface.draw_glyphs( + krilla::geom::Point::from_xy(0.0, 0.0), + glyphs, + font.clone(), + text, + size.to_f32(), + false, + ); + + surface.pop(); + + Ok(()) +} + +fn convert_font( + gc: &mut GlobalContext, + typst_font: Font, +) -> SourceResult { + if let Some(font) = gc.fonts_forward.get(&typst_font) { + Ok(font.clone()) + } else { + let font = build_font(typst_font.clone())?; + + gc.fonts_forward.insert(typst_font.clone(), font.clone()); + gc.fonts_backward.insert(font.clone(), typst_font.clone()); + + Ok(font) + } +} + +#[comemo::memoize] +fn build_font(typst_font: Font) -> SourceResult { + let font_data: Arc + Send + Sync> = + Arc::new(typst_font.data().clone()); + + match krilla::text::Font::new(font_data.into(), typst_font.index()) { + None => { + let font_str = display_font(&typst_font); + bail!(Span::detached(), "failed to process font {font_str}"); + } + Some(f) => Ok(f), + } +} + +#[derive(TransparentWrapper, Debug)] +#[repr(transparent)] +struct PdfGlyph(Glyph); + +impl krilla::text::Glyph for PdfGlyph { + #[inline(always)] + fn glyph_id(&self) -> GlyphId { + GlyphId::new(self.0.id as u32) + } + + #[inline(always)] + fn text_range(&self) -> Range { + self.0.range.start as usize..self.0.range.end as usize + } + + #[inline(always)] + fn x_advance(&self, size: f32) -> f32 { + // Don't use `Em::at`, because it contains an expensive check whether the result is finite. + self.0.x_advance.get() as f32 * size + } + + #[inline(always)] + fn x_offset(&self, size: f32) -> f32 { + // Don't use `Em::at`, because it contains an expensive check whether the result is finite. + self.0.x_offset.get() as f32 * size + } + + #[inline(always)] + fn y_offset(&self, _: f32) -> f32 { + 0.0 + } + + #[inline(always)] + fn y_advance(&self, _: f32) -> f32 { + 0.0 + } + + fn location(&self) -> Option { + Some(self.0.span.0.into_raw().get()) + } +} diff --git a/crates/typst-pdf/src/tiling.rs b/crates/typst-pdf/src/tiling.rs deleted file mode 100644 index f8950f344..000000000 --- a/crates/typst-pdf/src/tiling.rs +++ /dev/null @@ -1,184 +0,0 @@ -use std::collections::HashMap; - -use ecow::eco_format; -use pdf_writer::types::{ColorSpaceOperand, PaintType, TilingType}; -use pdf_writer::{Filter, Name, Rect, Ref}; -use typst_library::diag::SourceResult; -use typst_library::layout::{Abs, Ratio, Transform}; -use typst_library::visualize::{RelativeTo, Tiling}; -use typst_utils::Numeric; - -use crate::color::PaintEncode; -use crate::resources::{Remapper, ResourcesRefs}; -use crate::{content, transform_to_array, PdfChunk, Resources, WithGlobalRefs}; - -/// Writes the actual patterns (tiling patterns) to the PDF. -/// This is performed once after writing all pages. -pub fn write_tilings( - context: &WithGlobalRefs, -) -> SourceResult<(PdfChunk, HashMap)> { - let mut chunk = PdfChunk::new(); - let mut out = HashMap::new(); - context.resources.traverse(&mut |resources| { - let Some(patterns) = &resources.tilings else { - return Ok(()); - }; - - for pdf_pattern in patterns.remapper.items() { - let PdfTiling { transform, pattern, content, .. } = pdf_pattern; - if out.contains_key(pdf_pattern) { - continue; - } - - let tiling = chunk.alloc(); - out.insert(pdf_pattern.clone(), tiling); - - let mut tiling_pattern = chunk.tiling_pattern(tiling, content); - tiling_pattern - .tiling_type(TilingType::ConstantSpacing) - .paint_type(PaintType::Colored) - .bbox(Rect::new( - 0.0, - 0.0, - pattern.size().x.to_pt() as _, - pattern.size().y.to_pt() as _, - )) - .x_step((pattern.size().x + pattern.spacing().x).to_pt() as _) - .y_step((pattern.size().y + pattern.spacing().y).to_pt() as _); - - // The actual resource dict will be written in a later step - tiling_pattern.pair(Name(b"Resources"), patterns.resources.reference); - - tiling_pattern - .matrix(transform_to_array( - transform - .pre_concat(Transform::scale(Ratio::one(), -Ratio::one())) - .post_concat(Transform::translate( - Abs::zero(), - pattern.spacing().y, - )), - )) - .filter(Filter::FlateDecode); - } - - Ok(()) - })?; - - Ok((chunk, out)) -} - -/// A pattern and its transform. -#[derive(Clone, PartialEq, Eq, Hash, Debug)] -pub struct PdfTiling { - /// The transform to apply to the pattern. - pub transform: Transform, - /// The pattern to paint. - pub pattern: Tiling, - /// The rendered pattern. - pub content: Vec, -} - -/// Registers a pattern with the PDF. -fn register_pattern( - ctx: &mut content::Builder, - pattern: &Tiling, - on_text: bool, - mut transforms: content::Transforms, -) -> SourceResult { - let patterns = ctx - .resources - .tilings - .get_or_insert_with(|| Box::new(TilingRemapper::new())); - - // Edge cases for strokes. - if transforms.size.x.is_zero() { - transforms.size.x = Abs::pt(1.0); - } - - if transforms.size.y.is_zero() { - transforms.size.y = Abs::pt(1.0); - } - - let transform = match pattern.unwrap_relative(on_text) { - RelativeTo::Self_ => transforms.transform, - RelativeTo::Parent => transforms.container_transform, - }; - - // Render the body. - let content = content::build( - ctx.options, - &mut patterns.resources, - pattern.frame(), - None, - None, - )?; - - let pdf_pattern = PdfTiling { - transform, - pattern: pattern.clone(), - content: content.content.wait().clone(), - }; - - Ok(patterns.remapper.insert(pdf_pattern)) -} - -impl PaintEncode for Tiling { - fn set_as_fill( - &self, - ctx: &mut content::Builder, - on_text: bool, - transforms: content::Transforms, - ) -> SourceResult<()> { - ctx.reset_fill_color_space(); - - let index = register_pattern(ctx, self, on_text, transforms)?; - let id = eco_format!("P{index}"); - let name = Name(id.as_bytes()); - - ctx.content.set_fill_color_space(ColorSpaceOperand::Pattern); - ctx.content.set_fill_pattern(None, name); - Ok(()) - } - - fn set_as_stroke( - &self, - ctx: &mut content::Builder, - on_text: bool, - transforms: content::Transforms, - ) -> SourceResult<()> { - ctx.reset_stroke_color_space(); - - let index = register_pattern(ctx, self, on_text, transforms)?; - let id = eco_format!("P{index}"); - let name = Name(id.as_bytes()); - - ctx.content.set_stroke_color_space(ColorSpaceOperand::Pattern); - ctx.content.set_stroke_pattern(None, name); - Ok(()) - } -} - -/// De-duplicate patterns and the resources they require to be drawn. -pub struct TilingRemapper { - /// Pattern de-duplicator. - pub remapper: Remapper, - /// PDF resources that are used by these patterns. - pub resources: Resources, -} - -impl TilingRemapper<()> { - pub fn new() -> Self { - Self { - remapper: Remapper::new("P"), - resources: Resources::default(), - } - } - - /// Allocate a reference to the resource dictionary of these patterns. - pub fn with_refs(self, refs: &ResourcesRefs) -> TilingRemapper { - TilingRemapper { - remapper: self.remapper, - resources: self.resources.with_refs(refs), - } - } -} diff --git a/crates/typst-pdf/src/util.rs b/crates/typst-pdf/src/util.rs new file mode 100644 index 000000000..3b85d0b8a --- /dev/null +++ b/crates/typst-pdf/src/util.rs @@ -0,0 +1,120 @@ +//! Basic utilities for converting typst types to krilla. + +use krilla::geom as kg; +use krilla::geom::PathBuilder; +use krilla::paint as kp; +use typst_library::layout::{Abs, Point, Size, Transform}; +use typst_library::text::Font; +use typst_library::visualize::{Curve, CurveItem, FillRule, LineCap, LineJoin}; + +pub(crate) trait SizeExt { + fn to_krilla(&self) -> kg::Size; +} + +impl SizeExt for Size { + fn to_krilla(&self) -> kg::Size { + kg::Size::from_wh(self.x.to_f32(), self.y.to_f32()).unwrap() + } +} + +pub(crate) trait PointExt { + fn to_krilla(&self) -> kg::Point; +} + +impl PointExt for Point { + fn to_krilla(&self) -> kg::Point { + kg::Point::from_xy(self.x.to_f32(), self.y.to_f32()) + } +} + +pub(crate) trait LineCapExt { + fn to_krilla(&self) -> kp::LineCap; +} + +impl LineCapExt for LineCap { + fn to_krilla(&self) -> kp::LineCap { + match self { + LineCap::Butt => kp::LineCap::Butt, + LineCap::Round => kp::LineCap::Round, + LineCap::Square => kp::LineCap::Square, + } + } +} + +pub(crate) trait LineJoinExt { + fn to_krilla(&self) -> kp::LineJoin; +} + +impl LineJoinExt for LineJoin { + fn to_krilla(&self) -> kp::LineJoin { + match self { + LineJoin::Miter => kp::LineJoin::Miter, + LineJoin::Round => kp::LineJoin::Round, + LineJoin::Bevel => kp::LineJoin::Bevel, + } + } +} + +pub(crate) trait TransformExt { + fn to_krilla(&self) -> kg::Transform; +} + +impl TransformExt for Transform { + fn to_krilla(&self) -> kg::Transform { + kg::Transform::from_row( + self.sx.get() as f32, + self.ky.get() as f32, + self.kx.get() as f32, + self.sy.get() as f32, + self.tx.to_f32(), + self.ty.to_f32(), + ) + } +} + +pub(crate) trait FillRuleExt { + fn to_krilla(&self) -> kp::FillRule; +} + +impl FillRuleExt for FillRule { + fn to_krilla(&self) -> kp::FillRule { + match self { + FillRule::NonZero => kp::FillRule::NonZero, + FillRule::EvenOdd => kp::FillRule::EvenOdd, + } + } +} + +pub(crate) trait AbsExt { + fn to_f32(self) -> f32; +} + +impl AbsExt for Abs { + fn to_f32(self) -> f32 { + self.to_pt() as f32 + } +} + +/// Display the font family of a font. +pub(crate) fn display_font(font: &Font) -> &str { + &font.info().family +} + +/// Convert a typst path to a krilla path. +pub(crate) fn convert_path(path: &Curve, builder: &mut PathBuilder) { + for item in &path.0 { + match item { + CurveItem::Move(p) => builder.move_to(p.x.to_f32(), p.y.to_f32()), + CurveItem::Line(p) => builder.line_to(p.x.to_f32(), p.y.to_f32()), + CurveItem::Cubic(p1, p2, p3) => builder.cubic_to( + p1.x.to_f32(), + p1.y.to_f32(), + p2.x.to_f32(), + p2.y.to_f32(), + p3.x.to_f32(), + p3.y.to_f32(), + ), + CurveItem::Close => builder.close(), + } + } +} From 12699eb7f415bdba6797c84e3e7bf44dde75bdf9 Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski <133046678+wrzian@users.noreply.github.com> Date: Wed, 2 Apr 2025 05:30:04 -0400 Subject: [PATCH 090/172] Parse multi-character numbers consistently in math (#5996) Co-authored-by: Laurenz --- crates/typst-syntax/src/parser.rs | 8 +++----- .../ref/issue-4828-math-number-multi-char.png | Bin 0 -> 465 bytes tests/ref/math-frac-precedence.png | Bin 5504 -> 3586 bytes tests/suite/math/frac.typ | 4 ++-- tests/suite/math/syntax.typ | 4 ++++ 5 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 tests/ref/issue-4828-math-number-multi-char.png diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index c5d13c8b3..ecd0d78a5 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -271,11 +271,9 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { } SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::MathShorthand => { - continuable = !p.at(SyntaxKind::MathShorthand) - && matches!( - math_class(p.current_text()), - None | Some(MathClass::Alphabetic) - ); + // `a(b)/c` parses as `(a(b))/c` if `a` is continuable. + continuable = math_class(p.current_text()) == Some(MathClass::Alphabetic) + || p.current_text().chars().all(char::is_alphabetic); if !maybe_delimited(p) { p.eat(); } diff --git a/tests/ref/issue-4828-math-number-multi-char.png b/tests/ref/issue-4828-math-number-multi-char.png new file mode 100644 index 0000000000000000000000000000000000000000..ff0a9bab97de1957f3ea99de261346c14f8ccd9d GIT binary patch literal 465 zcmV;?0WSWDP)|ER(H<~| zY15fS>tQtA{}XtTVCdm*c-i;*JA9768pl*k6|TZn_hfVZBb`zLU zu}s@P^Y-#l!KFB0edr+gWfqVu9ueHV4bU7M05<^?+C#&I=3AT8;l;r86B6U-X$jKF za=L2`P;6~j!>cYf`lp^vQWqM|=kr@!WGr_t9x0mZz~X6-bI-fy7r^iQUO)9tl5%1- z#^oR^9F0bMsV8vm3s&yKnOscn)a^gub#$+3kwt>F^Kh*g7B-VXEpJLEO)(aS^x2H5 z;KpF747*FrtuipA^fu91SfEx|IQ_Y;k4A3B?xZqAmSN`-9qo4u4+mkcQ8L^4%lZSK z8oHWdWg0`nTS2m)v3YDC_`}G^hC`YiT`~W@dW%)K3RmbaR+3c8Kj6b400000NkvXX Hu0mjf!|2wg literal 0 HcmV?d00001 diff --git a/tests/ref/math-frac-precedence.png b/tests/ref/math-frac-precedence.png index 973c433e2c0d267aa57d746e586736c2b77b1a96..fd16f2e6bf3e043a81e4e6e3146a6b15c05fc559 100644 GIT binary patch literal 3586 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 IM6N<$f*h$L1poj5 literal 5504 zcmYjVXEYp8x7A1Qql;dGQHSVt2BQrqW9>cOOz0A z^1W}p^?uxYf84X~J!{=__t|^H^>oxo2pI@5Ffd49>JS6;8i*cE_*m$f(X09d0|TfA zgD4pWE*=%!e5CH98Mq;`PCTLgaSImWJl0f9l*jbOet`i5>MMQW+r+DlFj0i+Taqj3 zf8mQ8P8L<@Le&;LKA! zOEw&;e|fYxULcdg_$2wLDE!v7JBEM{OX%xqRD6HZlhx}5=OS-~OsbUK_3+Q-7MB+j z3JUhU_*vvO;Wu1I>YbrmX=;CO4~2k{UC{tv9zz_#FI$vcygEo8Z3IM@=>2L?Pvp`t7HbG25w za=&JlUUZV!yt+G6TX%}ksJ-u_wF&)MKX7|>GBI?aei;wsukE;XlL-F(^Jf1;5ly-a z+wn@k8j0Vl1=FvAXKSw>#(wGMiaIScIQ{v3>0vfgqWJFRL1R@_iXs7=@};QOPkp^OEU zrL-z-!fBO-po5_;M-Q}NU_(V;)_=GUAJnIv7u!{0xbWF8_*V$VkD#NDW= z`EA>045z-?uc*#snm~mn*tmjjNuu1x%M|_gIT*lt*EFj1YwW z&=9xt=D3gP)9pIkr4Bt=ZFfLMDo`J!Cc$v=!kpMvDo%eh(WV3G6ht`?zBLJW9hnUg zJavM=@SAPgf7^77%;@bYih@V{CbhlT3BD7}9t)evG?8;QukH&1R`ty=I5e*+bx|_1 zVxyf7b%X_Pgay&>8>YnqjxH73s~KFiBf81{Q1iSdaj+-1Iw z?UR?<=!^T?UwjhRul=|e2h5xcnnG{RrCAwzCc?P)io3skWipv-o&f@ZC+$~-U%&oH zmT818eGL-J>H0-pohqkANF!kS?oX_bGE(sTMr=YhgLGrhjgKHM&@0zt*>^(j`iH;b zrS_8*NPc3j=^Z&B`zldvoVEM0kc|HUC!^oXU^(p;-6DBQvMZJ9J1Sk-V#>|kqF3_2 zW}fcr^0|ZEXMg6s=%~O(PRN|d?wfsH3vY{$WtMqK{!ZjYR6j9$ zzxJm8KAh9#!t8oH3Z6bA&MWLy*_kL3>g=3^5cT00)gy_=_TVr!3e=QLOO^0-pSDu0k#ASlsviieQI=1(>T^ zd+;wGz+DUfvkaaNjXGuJ{k8BvM`#=pSu2}k8&}a)qxtbs`W_W-W4iSdGe&Y z>b*6WjV-rJts}}FKmIu!wJd>X&SAtko6;2WAH(c7!cpev9XeWS(CVDTibRljrE~$&%>4qouC_q z{%X)}0mMlTMNj3a%L?Bo2c1E7(_BWu2n_?_-e+>C1*#g3pb6OvV%Jb4uYL7&WSw00 zfEEtbmcunj4XOVueK~kN5}>E8p_+F3cxvq79veom$V#iy&+6y*ox)ssx0jSaFmK3G z)%-+_$21Vb=9*LeE(>$3xnzNSw0J+qV*y?A+Ru!4g9oufkW}wr8QhI7-50Tsg3Cn} z%Sbx&jJ3`aM`@yPgA#kA^z3a^j-ih$i^n2Kh;3!regW4BnAH$6q=5h8M?5A4&-KQ&&SCacB84TiX zy$zZ;(u-0TVm+Dfx;A5nwkGp2H*wvq#Ij9@P;#jKuAj*Z00gduNRmXZ-5B7n7&fVp zrr4RH$2K3$GF1WQTMhC#S_0;(QS2XVk)%b#!+5=1rxTptQri0QP+SB+3bJnK1;iOI zpX9)Re6&~NZ4WC#gh~9r{dEKW`(J-Oc;iCjy1=FEct%!RA5X&MW~{VS{~_e0J!j7s z3566BNqW&cF*^`?v03W41W^kra3IwZjY<9j|IU4l3P27 zf+#z({^nas<`hw&GGZ%|laPM{=co!OP-1vhj~y@9vc>j01^riF`5|IS z+yhFoCA`k*#y{c(~?~_S2%6(t*p@Kdk1LZL3}^3WJrQ<`sFiy z(anaW6t%TjJsgxPtcq&`lpOnG7xPFdBbGm*m-HkAKn^rb!-T4^68iKRP>96s>efhs!-B^QeZ#tr*ph>u8?l!*CP;!m;vge z3K2uB$(y6KzU|IhbB`Pv)Ot6O8u$?%jnw6HnMxVkffR+mfj^^Hqt9gxH&ZYPY6AM+ zr4(^M%~1lv^)uTI%0y{r4$4&tKh2g_H;j+gI`ooBj+*{GItgmI8@<-R5#+naEZt znlUfS{M1!~_e=HoZB{iHf^^u_t&p*i2@-Thtb0Q|MUK{!9bq9>VoQ$MOSY@exO@4q zaJ1?|OugDLxMm!D4a!9y6AhCoQmgmA_kxHaU|FR-q%vA~+q{!o^aW#j;?teHOG!j* z=gU>Kt;4^Wu!enOUs9KHKF52%tps4kQHxYAM6$tf##M_2ymQCp|NQKxs7aU~ss>w3 zZsodFnn!oV`#ADFoz&EOphoNoTD1;hWU;x-Wr~bK)L?Q(vVK|2btl!C8>6^f3}Q)o z6q+FYJS_|%d(5%;_Z87f^E9CY)r4HaF{xzHPT>|Cab@X(>{6Fov{rr;s>1v$VBjMu z9v^GuR3**30IK#MhB;O*&5SjvTddguhMlY#W|ByHs-3NG?@C0meoGpVv6ohJt z)AZ4S5G8>@5Vqa-N$FoL?xU5ONiG3@sgXnv?W0#3`7YgA>1*$Ixw#NQ!YISshJgp` zKl4*Oh$?0T_99E6Nn~-NE?7Ji|5K{gyF#+q46e9`b{~NyPL?W@gf-No+ zExejlKY=^3FN|F?3Xg`3RU4DkAk2KuOSFjxxqLdm$6womHKGzK*mq?e zw4F6ik_-w%Rvh;6c@=VM?nFU6*^xoylEuhr9T^f3Ctm|(En%Xko|Av;Urw_z?CDj| znaCACD(yoKK6lkMm~uLq%A)2~fUNqwafXk;-+U%b#ah2ORaSG&D^h84t_7#AW!3x_ zX|1n`!<;r&^=Zb&N9KQGm)!ge;IyDJ(p~RTLw;ggC`<5WA0axji{sXa-@7D7W<)ap z>EaEN{Zo71*hd_wNIwoS{{Ti`o7Vsz|vd3+M!$uyqncGT* z+;Ps*4eQtA<)#p;eyk^;QpbIbe90j+V;4%fcPJMiLYg^%f+9*D;T{>z!pLb${f-@g% zp(m&RsZ7*APUW}CUs_e&b*uPjqxX&VGAukB9(Y5keZ)U&vm?Z;wVvsfwQAM@$q;i? zE#yIB@~2&&z>NijIB#MwB7+=uAQ@m=ZytR!PMK{TKeVVQ-JYX zigVQqf*ohoS)xOS)A|Y^Qo{(BU~mOSeF^7hQ69wha3Hgz2P`5>02OG`%#bE0TLb=v z&wwibuwrX8NQh?))vM-cbW7F-?|bo8)aMs@7`sv?CF=8a%-g=YBYsRI8W>*)b16Sp zpW(<{^^>G4g@Mnh)2J406;AqX{1YcfEQzAKlG3NU-dD(%0s+V=>RgaGQY87Ak}wXw zPiyHzpBzsfRe89oGv8xmn$aE(Ax-WShKHWj zo7f(_!bDtJaXb$>)jSE$^)v|ejituop0q>xmy%_S@9tjNO}$nf?fnGudhC$twu)`m zr2?6NGFD+mw34Yc>!fj3gfKiM8PuTySu|%7I!N+dN6E??{C>(Cg%T1z%V-;cl}UF2 zGzR?0J0>ANTvJV;(mll5orgLZhlNT@&Ogu;893bq1Ec0G-Qv6_MyIo_E_I&BHHyB~ zlr^gc`5gv~?WA}M17UG*{oY4ImI+GiBeA7`Hy%=;5r^&C7-QAGo{j8G0o3IoZ<^>I z|09!8{-*iKxn{TT8Zs-@zTUF4#8zKxl30=`H`BW};8e9f0t z<*kelMd7Uzb-bJMNa``FmITSTZ5?oXVI!3a5+_R(@LCII?|t6DjR1~ZzbaRuZ}7@C znzhv!vSj-Fp|Y~F8eUfO>mINv=14SaIymGyx2hWQCGtOZ%Ky^6X;~rwL3TVw3X~Pu zYnVqI0V#jn{odX|oz#2h{)|Nn0_C?Hy;TF#2BDn}X>`S#YgzHWN7Hahr(^=cd3BuO zdvbdAmjd>QxgSne+5$dWw;y1l@WnVyYPc{sYnaIvM5oo!dNExg37#h9Xz?m)*Y!I# zGKjFC5kHuvbkKr>c2JhfY|SEh5Rs8rery>Xn{j(W!gS51bb8}_P!bp;5b z?LIvlW0F9qE0jfRy_0p$_OL_lK8wU|mE)vLkv`z{Fw6%GzZ=|eCMOqiL((tAacf2y zO+_5P9QOP+s4>f-3qqi>cv)2%LlYQFTCO?2-JDU;DA)}#>bbw;ReTd6T&N~%{b;q0dmnIWRUO}dy_K**-CT^>3M;sP4@ayZ4S& zG_&|%wG%N#940enS)`Qh2^2?ZRqPid5m~t0q^zstc_G&uu{^=->|^xEFR%e7-$X)e zk>@l(R%&bvJ{o@c86GL`Nyj=$7GBjpmTaW}YS9ktRL zT>N;eshu_GFJBEr-i$1+-#J23nMFipexK&3&L^TCMM3uMrCsthJ^q|O>yrc5#l-JE zBW6*&Mb!Yv)6Oim*Wn>Xai0nL{_Xmu=>UE(=FWPCr~;)%g9?v}wFQ zD5nG!?MU_d>pmT@XYrYWmu4QcL8vGbqJ`Ww8jLh>n!zhJUu+BX8I|Aaq7xAGc}t2% xe8)mhwtdzvU%vHU3C^?F^MB?g>QU~-L&V!-oomkhmuP1d0|wQB)GFIX{SRe(a;^XX diff --git a/tests/suite/math/frac.typ b/tests/suite/math/frac.typ index 7f513930a..3bd00eab2 100644 --- a/tests/suite/math/frac.typ +++ b/tests/suite/math/frac.typ @@ -37,8 +37,8 @@ $ 1/2/3 = (1/2)/3 = 1/(2/3) $ // Test precedence. $ a_1/b_2, 1/f(x), zeta(x)/2, "foo"[|x|]/2 \ 1.2/3.7, 2.3^3.4 \ - 🏳️‍🌈[x]/2, f [x]/2, phi [x]/2, 🏳️‍🌈 [x]/2 \ - +[x]/2, 1(x)/2, 2[x]/2 \ + f [x]/2, phi [x]/2 \ + +[x]/2, 1(x)/2, 2[x]/2, 🏳️‍🌈[x]/2 \ (a)b/2, b(a)[b]/2 \ n!/2, 5!/2, n !/2, 1/n!, 1/5! $ diff --git a/tests/suite/math/syntax.typ b/tests/suite/math/syntax.typ index 7091d908c..32b9c098c 100644 --- a/tests/suite/math/syntax.typ +++ b/tests/suite/math/syntax.typ @@ -28,6 +28,10 @@ $ dot \ dots \ ast \ tilde \ star $ $floor(phi.alt.)$ $floor(phi.alt. )$ +--- issue-4828-math-number-multi-char --- +// Numbers should parse the same regardless of number of characters. +$1/2(x)$ vs. $1/10(x)$ + --- math-unclosed --- // Error: 1-2 unclosed delimiter $a From 417f5846b68777b8a4d3b9336761bd23c48a26b5 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Wed, 2 Apr 2025 11:41:45 +0200 Subject: [PATCH 091/172] Support comparison functions in `array.sorted` (#5627) Co-authored-by: +merlan #flirora Co-authored-by: Laurenz --- Cargo.lock | 7 + Cargo.toml | 1 + crates/typst-library/Cargo.toml | 1 + crates/typst-library/src/foundations/array.rs | 149 +++++++++++++++--- tests/suite/foundations/array.typ | 6 + 5 files changed, 140 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c13c64819..f9c0cb189 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -913,6 +913,12 @@ dependencies = [ "weezl", ] +[[package]] +name = "glidesort" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2e102e6eb644d3e0b186fc161e4460417880a0a0b87d235f2e5b8fb30f2e9e0" + [[package]] name = "half" version = "2.4.1" @@ -3052,6 +3058,7 @@ dependencies = [ "ecow", "flate2", "fontdb", + "glidesort", "hayagriva", "icu_properties", "icu_provider", diff --git a/Cargo.toml b/Cargo.toml index cbe69a05d..b9ec25054 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,7 @@ fastrand = "2.3" flate2 = "1" fontdb = { version = "0.23", default-features = false } fs_extra = "1.3" +glidesort = "0.1.2" hayagriva = "0.8.1" heck = "0.5" hypher = "0.1.4" diff --git a/crates/typst-library/Cargo.toml b/crates/typst-library/Cargo.toml index 71729b63a..b210637a8 100644 --- a/crates/typst-library/Cargo.toml +++ b/crates/typst-library/Cargo.toml @@ -29,6 +29,7 @@ csv = { workspace = true } ecow = { workspace = true } flate2 = { workspace = true } fontdb = { workspace = true } +glidesort = { workspace = true } hayagriva = { workspace = true } icu_properties = { workspace = true } icu_provider = { workspace = true } diff --git a/crates/typst-library/src/foundations/array.rs b/crates/typst-library/src/foundations/array.rs index b647473ab..c1fcb6b49 100644 --- a/crates/typst-library/src/foundations/array.rs +++ b/crates/typst-library/src/foundations/array.rs @@ -808,7 +808,7 @@ impl Array { /// function. The sorting algorithm used is stable. /// /// Returns an error if two values could not be compared or if the key - /// function (if given) yields an error. + /// or comparison function (if given) yields an error. /// /// To sort according to multiple criteria at once, e.g. in case of equality /// between some criteria, the key function can return an array. The results @@ -832,33 +832,134 @@ impl Array { /// determine the keys to sort by. #[named] key: Option, + /// If given, uses this function to compare elements in the array. + /// + /// This function should return a boolean: `{true}` indicates that the + /// elements are in order, while `{false}` indicates that they should be + /// swapped. To keep the sort stable, if the two elements are equal, the + /// function should return `{true}`. + /// + /// If this function does not order the elements properly (e.g., by + /// returning `{false}` for both `{(x, y)}` and `{(y, x)}`, or for + /// `{(x, x)}`), the resulting array will be in unspecified order. + /// + /// When used together with `key`, `by` will be passed the keys instead + /// of the elements. + /// + /// ```example + /// #( + /// "sorted", + /// "by", + /// "decreasing", + /// "length", + /// ).sorted( + /// key: s => s.len(), + /// by: (l, r) => l >= r, + /// ) + /// ``` + #[named] + by: Option, ) -> SourceResult { - let mut result = Ok(()); - let mut vec = self.0; - let mut key_of = |x: Value| match &key { - // NOTE: We are relying on `comemo`'s memoization of function - // evaluation to not excessively reevaluate the `key`. - Some(f) => f.call(engine, context, [x]), - None => Ok(x), - }; - vec.make_mut().sort_by(|a, b| { - // Until we get `try` blocks :) - match (key_of(a.clone()), key_of(b.clone())) { - (Ok(a), Ok(b)) => ops::compare(&a, &b).unwrap_or_else(|err| { - if result.is_ok() { - result = Err(err).at(span); + match by { + Some(by) => { + let mut are_in_order = |mut x, mut y| { + if let Some(f) = &key { + // We rely on `comemo`'s memoization of function + // evaluation to not excessively reevaluate the key. + x = f.call(engine, context, [x])?; + y = f.call(engine, context, [y])?; } - Ordering::Equal - }), - (Err(e), _) | (_, Err(e)) => { - if result.is_ok() { - result = Err(e); + match by.call(engine, context, [x, y])? { + Value::Bool(b) => Ok(b), + x => { + bail!( + span, + "expected boolean from `by` function, got {}", + x.ty(), + ) + } } - Ordering::Equal - } + }; + // If a comparison function is provided, we use `glidesort` + // instead of the standard library sorting algorithm to prevent + // panics in case the comparison function does not define a + // valid order (see https://github.com/typst/typst/pull/5627). + let mut result = Ok(()); + let mut vec = self.0.into_iter().enumerate().collect::>(); + glidesort::sort_by(&mut vec, |(i, x), (j, y)| { + // Because we use booleans for the comparison function, in + // order to keep the sort stable, we need to compare in the + // right order. + if i < j { + // If `x` and `y` appear in this order in the original + // array, then we should change their order (i.e., + // return `Ordering::Greater`) iff `y` is strictly less + // than `x` (i.e., `compare(x, y)` returns `false`). + // Otherwise, we should keep them in the same order + // (i.e., return `Ordering::Less`). + match are_in_order(x.clone(), y.clone()) { + Ok(false) => Ordering::Greater, + Ok(true) => Ordering::Less, + Err(err) => { + if result.is_ok() { + result = Err(err); + } + Ordering::Equal + } + } + } else { + // If `x` and `y` appear in the opposite order in the + // original array, then we should change their order + // (i.e., return `Ordering::Less`) iff `x` is strictly + // less than `y` (i.e., `compare(y, x)` returns + // `false`). Otherwise, we should keep them in the same + // order (i.e., return `Ordering::Less`). + match are_in_order(y.clone(), x.clone()) { + Ok(false) => Ordering::Less, + Ok(true) => Ordering::Greater, + Err(err) => { + if result.is_ok() { + result = Err(err); + } + Ordering::Equal + } + } + } + }); + result.map(|()| vec.into_iter().map(|(_, x)| x).collect()) } - }); - result.map(|_| vec.into()) + + None => { + let mut key_of = |x: Value| match &key { + // We rely on `comemo`'s memoization of function evaluation + // to not excessively reevaluate the key. + Some(f) => f.call(engine, context, [x]), + None => Ok(x), + }; + // If no comparison function is provided, we know the order is + // valid, so we can use the standard library sort and prevent an + // extra allocation. + let mut result = Ok(()); + let mut vec = self.0; + vec.make_mut().sort_by(|a, b| { + match (key_of(a.clone()), key_of(b.clone())) { + (Ok(a), Ok(b)) => ops::compare(&a, &b).unwrap_or_else(|err| { + if result.is_ok() { + result = Err(err).at(span); + } + Ordering::Equal + }), + (Err(e), _) | (_, Err(e)) => { + if result.is_ok() { + result = Err(e); + } + Ordering::Equal + } + } + }); + result.map(|()| vec.into()) + } + } } /// Deduplicates all items in the array. diff --git a/tests/suite/foundations/array.typ b/tests/suite/foundations/array.typ index 61b5decb3..0c820d7f2 100644 --- a/tests/suite/foundations/array.typ +++ b/tests/suite/foundations/array.typ @@ -359,6 +359,12 @@ #test((2, 1, 3, 10, 5, 8, 6, -7, 2).sorted(), (-7, 1, 2, 2, 3, 5, 6, 8, 10)) #test((2, 1, 3, -10, -5, 8, 6, -7, 2).sorted(key: x => x), (-10, -7, -5, 1, 2, 2, 3, 6, 8)) #test((2, 1, 3, -10, -5, 8, 6, -7, 2).sorted(key: x => x * x), (1, 2, 2, 3, -5, 6, -7, 8, -10)) +#test(("I", "the", "hi", "text").sorted(by: (x, y) => x.len() < y.len()), ("I", "hi", "the", "text")) +#test(("I", "the", "hi", "text").sorted(key: x => x.len(), by: (x, y) => y < x), ("text", "the", "hi", "I")) + +--- array-sorted-invalid-by-function --- +// Error: 2-39 expected boolean from `by` function, got string +#(1, 2, 3).sorted(by: (_, _) => "hmm") --- array-sorted-key-function-positional-1 --- // Error: 12-18 unexpected argument From ed2106e28d4b0cc213a4789d5e59c59ad08e9f29 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Wed, 2 Apr 2025 13:47:42 +0200 Subject: [PATCH 092/172] Disallow empty font lists (#6049) --- crates/typst-library/src/text/mod.rs | 16 ++++++++++++++-- tests/suite/text/font.typ | 4 ++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index 3aac15ba5..462d16060 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -42,7 +42,7 @@ use ttf_parser::Tag; use typst_syntax::Spanned; use typst_utils::singleton; -use crate::diag::{bail, warning, HintedStrResult, SourceResult}; +use crate::diag::{bail, warning, HintedStrResult, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ cast, dict, elem, Args, Array, Cast, Construct, Content, Dict, Fold, IntoValue, @@ -891,9 +891,21 @@ cast! { } /// Font family fallback list. +/// +/// Must contain at least one font. #[derive(Debug, Default, Clone, PartialEq, Hash)] pub struct FontList(pub Vec); +impl FontList { + pub fn new(fonts: Vec) -> StrResult { + if fonts.is_empty() { + bail!("font fallback list must not be empty") + } else { + Ok(Self(fonts)) + } + } +} + impl<'a> IntoIterator for &'a FontList { type IntoIter = std::slice::Iter<'a, FontFamily>; type Item = &'a FontFamily; @@ -911,7 +923,7 @@ cast! { self.0.into_value() }, family: FontFamily => Self(vec![family]), - values: Array => Self(values.into_iter().map(|v| v.cast()).collect::>()?), + values: Array => Self::new(values.into_iter().map(|v| v.cast()).collect::>()?)?, } /// Resolve a prioritized iterator over the font families. diff --git a/tests/suite/text/font.typ b/tests/suite/text/font.typ index 60a1cd94d..6e21dfd23 100644 --- a/tests/suite/text/font.typ +++ b/tests/suite/text/font.typ @@ -149,3 +149,7 @@ The number 123. #set text(-1pt) a + +--- empty-text-font-array --- +// Error: 17-19 font fallback list must not be empty +#set text(font: ()) From bf8751c06352c305a8132a2bd0a06ced557a3819 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 4 Apr 2025 10:35:51 +0200 Subject: [PATCH 093/172] Switch to released `krilla` version (#6137) --- Cargo.lock | 37 +++++++++++++++++++++---------------- Cargo.toml | 6 ++---- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f9c0cb189..8c485ea7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -787,9 +787,9 @@ checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" [[package]] name = "font-types" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d868ec188a98bb014c606072edd47e52e7ab7297db943b0b28503121e1d037bd" +checksum = "1fa6a5e5a77b5f3f7f9e32879f484aa5b3632ddfbe568a16266c904a6f32cdaf" dependencies = [ "bytemuck", ] @@ -1360,8 +1360,9 @@ dependencies = [ [[package]] name = "krilla" -version = "0.3.0" -source = "git+https://github.com/LaurenzV/krilla?rev=14756f7#14756f7067cb1a80b73b712cae9f98597153e623" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ee6128ebf52d7ce684613b6431ead2959f2be9ff8cf776eeaaad0427c953e9" dependencies = [ "base64", "bumpalo", @@ -1388,8 +1389,9 @@ dependencies = [ [[package]] name = "krilla-svg" -version = "0.3.0" -source = "git+https://github.com/LaurenzV/krilla?rev=14756f7#14756f7067cb1a80b73b712cae9f98597153e623" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3462989578155cf620ef8035f8921533cc95c28e2a0c75de172f7219e6aba84e" dependencies = [ "flate2", "fontdb", @@ -1837,8 +1839,9 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "pdf-writer" -version = "0.12.1" -source = "git+https://github.com/typst/pdf-writer?rev=0d513b9#0d513b9050d2f1a0507cabb4898aca971af6da98" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ea27c5015ab81753fc61e49f8cde74999346605ee148bb20008ef3d3150e0dc" dependencies = [ "bitflags 2.8.0", "itoa", @@ -2097,9 +2100,9 @@ dependencies = [ [[package]] name = "read-fonts" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f004ee5c610b8beb5f33273246893ac6258ec22185a6eb8ee132bccdb904cdaa" +checksum = "600e807b48ac55bad68a8cb75cc3c7739f139b9248f7e003e01e080f589b5288" dependencies = [ "bytemuck", "font-types", @@ -2425,9 +2428,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "skrifa" -version = "0.28.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e7936ca3627fdb516e97aca3c8ab5103f94ae32fe5ce80a0a02edcbacb7b53" +checksum = "6fa1e5622e4f7b98877e8a19890efddcac1230cec6198bd9de91ec0e00010dc8" dependencies = [ "bytemuck", "read-fonts", @@ -2522,8 +2525,9 @@ dependencies = [ [[package]] name = "subsetter" -version = "0.2.0" -source = "git+https://github.com/typst/subsetter?rev=460fdb6#460fdb66d6e0138b721b1ca9882faf15ce003246" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35539e8de3dcce8dd0c01f3575f85db1e5ac1aea1b996d2d09d89f148bc91497" dependencies = [ "fxhash", ] @@ -3755,8 +3759,9 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "xmp-writer" -version = "0.3.1" -source = "git+https://github.com/LaurenzV/xmp-writer?rev=a1cbb887#a1cbb887a84376fea4d7590d41c194a332840549" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce9e2f4a404d9ebffc0a9832cf4f50907220ba3d7fffa9099261a5cab52f2dd7" [[package]] name = "xz2" diff --git a/Cargo.toml b/Cargo.toml index b9ec25054..16c6a7d63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,8 +72,8 @@ if_chain = "1" image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] } indexmap = { version = "2", features = ["serde"] } kamadak-exif = "0.6" -krilla = { git = "https://github.com/LaurenzV/krilla", rev = "14756f7", default-features = false, features = ["raster-images", "comemo", "rayon"] } -krilla-svg = { git = "https://github.com/LaurenzV/krilla", rev = "14756f7" } +krilla = { version = "0.4.0", default-features = false, features = ["raster-images", "comemo", "rayon"] } +krilla-svg = "0.1.0" kurbo = "0.11" libfuzzer-sys = "0.4" lipsum = "0.9" @@ -87,7 +87,6 @@ oxipng = { version = "9.0", default-features = false, features = ["filetime", "p palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] } parking_lot = "0.12.1" pathdiff = "0.2" -pdf-writer = "0.12.1" phf = { version = "0.11", features = ["macros"] } pixglyph = "0.6" png = "0.17" @@ -114,7 +113,6 @@ sigpipe = "0.1" siphasher = "1" smallvec = { version = "1.11.1", features = ["union", "const_generics", "const_new"] } stacker = "0.1.15" -subsetter = "0.2" syn = { version = "2", features = ["full", "extra-traits"] } syntect = { version = "5", default-features = false, features = ["parsing", "regex-fancy", "plist-load", "yaml-load"] } tar = "0.4" From 387a8b48951b0e7e283c81557852e3eba3afb446 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Fri, 4 Apr 2025 13:53:14 +0200 Subject: [PATCH 094/172] Display color spaces in the order in which they are presented in the doc (#6140) --- crates/typst-library/src/visualize/gradient.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/typst-library/src/visualize/gradient.rs b/crates/typst-library/src/visualize/gradient.rs index d59175a4e..45f388ccd 100644 --- a/crates/typst-library/src/visualize/gradient.rs +++ b/crates/typst-library/src/visualize/gradient.rs @@ -120,12 +120,12 @@ use crate::visualize::{Color, ColorSpace, WeightedColor}; /// #let spaces = ( /// ("Oklab", color.oklab), /// ("Oklch", color.oklch), -/// ("linear-RGB", color.linear-rgb), /// ("sRGB", color.rgb), +/// ("linear-RGB", color.linear-rgb), /// ("CMYK", color.cmyk), +/// ("Grayscale", color.luma), /// ("HSL", color.hsl), /// ("HSV", color.hsv), -/// ("Grayscale", color.luma), /// ) /// /// #for (name, space) in spaces { From ea336a6ac71ba9d84da6caa5d64291c87b0bca44 Mon Sep 17 00:00:00 2001 From: Markus Langgeng Iman Saputra Date: Fri, 4 Apr 2025 15:50:13 +0000 Subject: [PATCH 095/172] Add Indonesian translation (#6108) Co-authored-by: Malo <57839069+MDLC01@users.noreply.github.com> --- crates/typst-library/src/text/lang.rs | 4 +++- crates/typst-library/translations/id.txt | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 crates/typst-library/translations/id.txt diff --git a/crates/typst-library/src/text/lang.rs b/crates/typst-library/src/text/lang.rs index c75e5225f..2cc66a261 100644 --- a/crates/typst-library/src/text/lang.rs +++ b/crates/typst-library/src/text/lang.rs @@ -14,7 +14,7 @@ macro_rules! translation { }; } -const TRANSLATIONS: [(&str, &str); 38] = [ +const TRANSLATIONS: [(&str, &str); 39] = [ translation!("ar"), translation!("bg"), translation!("ca"), @@ -31,6 +31,7 @@ const TRANSLATIONS: [(&str, &str); 38] = [ translation!("el"), translation!("he"), translation!("hu"), + translation!("id"), translation!("is"), translation!("it"), translation!("ja"), @@ -82,6 +83,7 @@ impl Lang { pub const HEBREW: Self = Self(*b"he ", 2); pub const HUNGARIAN: Self = Self(*b"hu ", 2); pub const ICELANDIC: Self = Self(*b"is ", 2); + pub const INDONESIAN: Self = Self(*b"id ", 2); pub const ITALIAN: Self = Self(*b"it ", 2); pub const JAPANESE: Self = Self(*b"ja ", 2); pub const LATIN: Self = Self(*b"la ", 2); diff --git a/crates/typst-library/translations/id.txt b/crates/typst-library/translations/id.txt new file mode 100644 index 000000000..bea5ee18c --- /dev/null +++ b/crates/typst-library/translations/id.txt @@ -0,0 +1,8 @@ +figure = Gambar +table = Tabel +equation = Persamaan +bibliography = Daftar Pustaka +heading = Bagian +outline = Daftar Isi +raw = Kode +page = halaman From d55abf084263c15b4eac8efcf4faa3aafdd3af11 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 7 Apr 2025 19:46:46 +0200 Subject: [PATCH 096/172] Update community section in README (#6150) --- README.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 41f465152..9526f3df4 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ Typst's CLI is available from different sources: - You can install Typst through different package managers. Note that the versions in the package managers might lag behind the latest release. - - Linux: + - Linux: - View [Typst on Repology][repology] - View [Typst's Snap][snap] - macOS: `brew install typst` @@ -177,22 +177,22 @@ If you prefer an integrated IDE-like experience with autocompletion and instant preview, you can also check out [Typst's free web app][app]. ## Community -The main place where the community gathers is our [Discord server][discord]. -Feel free to join there to ask questions, help out others, share cool things -you created with Typst, or just to chat. +The main places where the community gathers are our [Forum][forum] and our +[Discord server][discord]. The Forum is a great place to ask questions, help +others, and share cool things you created with Typst. The Discord server is more +suitable for quicker questions, discussions about contributing, or just to chat. +We'd be happy to see you there! -Aside from that there are a few places where you can find things built by -the community: - -- The official [package list](https://typst.app/docs/packages) -- The [Awesome Typst](https://github.com/qjcg/awesome-typst) repository +[Typst Universe][universe] is where the community shares templates and packages. +If you want to share your own creations, you can submit them to our +[package repository][packages]. If you had a bad experience in our community, please [reach out to us][contact]. ## Contributing -We would love to see contributions from the community. If you experience bugs, -feel free to open an issue. If you would like to implement a new feature or bug -fix, please follow the steps outlined in the [contribution guide][contributing]. +We love to see contributions from the community. If you experience bugs, feel +free to open an issue. If you would like to implement a new feature or bug fix, +please follow the steps outlined in the [contribution guide][contributing]. To build Typst yourself, first ensure that you have the [latest stable Rust][rust] installed. Then, clone this repository and build the @@ -243,6 +243,8 @@ instant preview. To achieve these goals, we follow three core design principles: [docs]: https://typst.app/docs/ [app]: https://typst.app/ [discord]: https://discord.gg/2uDybryKPe +[forum]: https://forum.typst.app/ +[universe]: https://typst.app/universe/ [tutorial]: https://typst.app/docs/tutorial/ [show]: https://typst.app/docs/reference/styling/#show-rules [math]: https://typst.app/docs/reference/math/ From 14928ef9628d084af370463ccbf2f3bae3f70794 Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Mon, 7 Apr 2025 20:47:29 +0300 Subject: [PATCH 097/172] Fix typo in module docs (#6146) Co-authored-by: Alberto Corbi --- crates/typst-library/src/foundations/module.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-library/src/foundations/module.rs b/crates/typst-library/src/foundations/module.rs index 8d9626a1a..d6d5e831d 100644 --- a/crates/typst-library/src/foundations/module.rs +++ b/crates/typst-library/src/foundations/module.rs @@ -7,7 +7,7 @@ use typst_syntax::FileId; use crate::diag::{bail, DeprecationSink, StrResult}; use crate::foundations::{repr, ty, Content, Scope, Value}; -/// An module of definitions. +/// A module of definitions. /// /// A module /// - be built-in From bd2e76e11d487d1e825217db155e45d3fb6f6584 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 7 Apr 2025 20:20:27 +0200 Subject: [PATCH 098/172] Bump OpenSSL (#6153) --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c485ea7d..ab2d2cc83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1702,9 +1702,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.70" +version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ "bitflags 2.8.0", "cfg-if", @@ -1743,9 +1743,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.105" +version = "0.9.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" +checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" dependencies = [ "cc", "libc", diff --git a/Cargo.toml b/Cargo.toml index 16c6a7d63..12870b809 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,7 +82,7 @@ native-tls = "0.2" notify = "8" once_cell = "1" open = "5.0.1" -openssl = "0.10" +openssl = "0.10.72" oxipng = { version = "9.0", default-features = false, features = ["filetime", "parallel", "zopfli"] } palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] } parking_lot = "0.12.1" From 14a0565d95b40bb58a07da554b7d05d4712efcd7 Mon Sep 17 00:00:00 2001 From: +merlan #flirora Date: Mon, 7 Apr 2025 14:42:29 -0400 Subject: [PATCH 099/172] Show warnings from eval (#6100) Co-authored-by: Laurenz --- crates/typst-cli/src/query.rs | 3 +++ crates/typst-eval/src/lib.rs | 4 ++-- crates/typst-library/src/foundations/mod.rs | 12 +++++++++++- crates/typst-library/src/model/bibliography.rs | 6 ++++-- crates/typst-library/src/routines.rs | 1 + tests/suite/foundations/eval.typ | 6 ++++++ 6 files changed, 27 insertions(+), 5 deletions(-) diff --git a/crates/typst-cli/src/query.rs b/crates/typst-cli/src/query.rs index 610f23cd4..7806e456f 100644 --- a/crates/typst-cli/src/query.rs +++ b/crates/typst-cli/src/query.rs @@ -2,6 +2,7 @@ use comemo::Track; use ecow::{eco_format, EcoString}; use serde::Serialize; use typst::diag::{bail, HintedStrResult, StrResult, Warned}; +use typst::engine::Sink; use typst::foundations::{Content, IntoValue, LocatableSelector, Scope}; use typst::layout::PagedDocument; use typst::syntax::Span; @@ -58,6 +59,8 @@ fn retrieve( let selector = eval_string( &typst::ROUTINES, world.track(), + // TODO: propagate warnings + Sink::new().track_mut(), &command.selector, Span::detached(), EvalMode::Code, diff --git a/crates/typst-eval/src/lib.rs b/crates/typst-eval/src/lib.rs index 5eae7c1df..586da26be 100644 --- a/crates/typst-eval/src/lib.rs +++ b/crates/typst-eval/src/lib.rs @@ -101,6 +101,7 @@ pub fn eval( pub fn eval_string( routines: &Routines, world: Tracked, + sink: TrackedMut, string: &str, span: Span, mode: EvalMode, @@ -121,7 +122,6 @@ pub fn eval_string( } // Prepare the engine. - let mut sink = Sink::new(); let introspector = Introspector::default(); let traced = Traced::default(); let engine = Engine { @@ -129,7 +129,7 @@ pub fn eval_string( world, introspector: introspector.track(), traced: traced.track(), - sink: sink.track_mut(), + sink, route: Route::default(), }; diff --git a/crates/typst-library/src/foundations/mod.rs b/crates/typst-library/src/foundations/mod.rs index 8e3aa060d..d42be15b1 100644 --- a/crates/typst-library/src/foundations/mod.rs +++ b/crates/typst-library/src/foundations/mod.rs @@ -77,6 +77,7 @@ pub use { indexmap::IndexMap, }; +use comemo::TrackedMut; use ecow::EcoString; use typst_syntax::Spanned; @@ -297,5 +298,14 @@ pub fn eval( for (key, value) in dict { scope.bind(key.into(), Binding::new(value, span)); } - (engine.routines.eval_string)(engine.routines, engine.world, &text, span, mode, scope) + + (engine.routines.eval_string)( + engine.routines, + engine.world, + TrackedMut::reborrow_mut(&mut engine.sink), + &text, + span, + mode, + scope, + ) } diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index b11c61789..51e3b03b0 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -6,7 +6,7 @@ use std::num::NonZeroUsize; use std::path::Path; use std::sync::{Arc, LazyLock}; -use comemo::Tracked; +use comemo::{Track, Tracked}; use ecow::{eco_format, EcoString, EcoVec}; use hayagriva::archive::ArchivedStyle; use hayagriva::io::BibLaTeXError; @@ -20,7 +20,7 @@ use typst_syntax::{Span, Spanned}; use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr}; use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult}; -use crate::engine::Engine; +use crate::engine::{Engine, Sink}; use crate::foundations::{ elem, Bytes, CastInfo, Content, Derived, FromValue, IntoValue, Label, NativeElement, OneOrMultiple, Packed, Reflect, Scope, Show, ShowSet, Smart, StyleChain, Styles, @@ -999,6 +999,8 @@ impl ElemRenderer<'_> { (self.routines.eval_string)( self.routines, self.world, + // TODO: propagate warnings + Sink::new().track_mut(), math, self.span, EvalMode::Math, diff --git a/crates/typst-library/src/routines.rs b/crates/typst-library/src/routines.rs index b283052a4..6f0cb32b1 100644 --- a/crates/typst-library/src/routines.rs +++ b/crates/typst-library/src/routines.rs @@ -55,6 +55,7 @@ routines! { fn eval_string( routines: &Routines, world: Tracked, + sink: TrackedMut, string: &str, span: Span, mode: EvalMode, diff --git a/tests/suite/foundations/eval.typ b/tests/suite/foundations/eval.typ index f85146b23..85f43911c 100644 --- a/tests/suite/foundations/eval.typ +++ b/tests/suite/foundations/eval.typ @@ -52,3 +52,9 @@ _Tiger!_ #eval(mode: "math", "f(a) = cases(a + b\, space space x >= 3,a + b\, space space x = 5)") $f(a) = cases(a + b\, space space x >= 3,a + b\, space space x = 5)$ + +--- issue-6067-eval-warnings --- +// Test that eval shows warnings from the executed code. +// Warning: 7-11 no text within stars +// Hint: 7-11 using multiple consecutive stars (e.g. **) has no additional effect +#eval("**", mode: "markup") From 43c3d5d3afc945639a576535e48806112c161743 Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Mon, 7 Apr 2025 22:47:02 +0300 Subject: [PATCH 100/172] Improved ratio and relative length docs (#5750) Co-authored-by: PgBiel <9021226+PgBiel@users.noreply.github.com> Co-authored-by: Laurenz --- crates/typst-library/src/layout/ratio.rs | 32 ++++++++++++--- crates/typst-library/src/layout/rel.rs | 51 +++++++++++++++++++++--- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/crates/typst-library/src/layout/ratio.rs b/crates/typst-library/src/layout/ratio.rs index 1c0dcd298..cf826c2b5 100644 --- a/crates/typst-library/src/layout/ratio.rs +++ b/crates/typst-library/src/layout/ratio.rs @@ -8,15 +8,35 @@ use crate::foundations::{repr, ty, Repr}; /// A ratio of a whole. /// -/// Written as a number, followed by a percent sign. +/// A ratio is written as a number, followed by a percent sign. Ratios most +/// often appear as part of a [relative length]($relative), to specify the size +/// of some layout element relative to the page or some container. /// -/// # Example /// ```example -/// #set align(center) -/// #scale(x: 150%)[ -/// Scaled apart. -/// ] +/// #rect(width: 25%) /// ``` +/// +/// However, they can also describe any other property that is relative to some +/// base, e.g. an amount of [horizontal scaling]($scale.x) or the +/// [height of parentheses]($math.lr.size) relative to the height of the content +/// they enclose. +/// +/// # Scripting +/// Within your own code, you can use ratios as you like. You can multiply them +/// with various other types as shown below: +/// +/// | Multiply by | Example | Result | +/// |-----------------|-------------------------|-----------------| +/// | [`ratio`] | `{27% * 10%}` | `{2.7%}` | +/// | [`length`] | `{27% * 100pt}` | `{27pt}` | +/// | [`relative`] | `{27% * (10% + 100pt)}` | `{2.7% + 27pt}` | +/// | [`angle`] | `{27% * 100deg}` | `{27deg}` | +/// | [`int`] | `{27% * 2}` | `{54%}` | +/// | [`float`] | `{27% * 0.37037}` | `{10%}` | +/// | [`fraction`] | `{27% * 3fr}` | `{0.81fr}` | +/// +/// When ratios are displayed in the document, they are rounded to two +/// significant digits for readability. #[ty(cast)] #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Ratio(Scalar); diff --git a/crates/typst-library/src/layout/rel.rs b/crates/typst-library/src/layout/rel.rs index 76d736785..7fe5d9c05 100644 --- a/crates/typst-library/src/layout/rel.rs +++ b/crates/typst-library/src/layout/rel.rs @@ -14,17 +14,58 @@ use crate::layout::{Abs, Em, Length, Ratio}; /// addition and subtraction of a length and a ratio. Wherever a relative length /// is expected, you can also use a bare length or ratio. /// -/// # Example -/// ```example -/// #rect(width: 100% - 50pt) +/// # Relative to the page +/// A common use case is setting the width or height of a layout element (e.g., +/// [block], [rect], etc.) as a certain percentage of the width of the page. +/// Here, the rectangle's width is set to `{25%}`, so it takes up one fourth of +/// the page's _inner_ width (the width minus margins). /// -/// #(100% - 50pt).length \ -/// #(100% - 50pt).ratio +/// ```example +/// #rect(width: 25%) /// ``` /// +/// Bare lengths or ratios are always valid where relative lengths are expected, +/// but the two can also be freely mixed: +/// ```example +/// #rect(width: 25% + 1cm) +/// ``` +/// +/// If you're trying to size an element so that it takes up the page's _full_ +/// width, you have a few options (this highly depends on your exact use case): +/// +/// 1. Set page margins to `{0pt}` (`[#set page(margin: 0pt)]`) +/// 2. Multiply the ratio by the known full page width (`{21cm * 69%}`) +/// 3. Use padding which will negate the margins (`[#pad(x: -2.5cm, ...)]`) +/// 4. Use the page [background](page.background) or +/// [foreground](page.foreground) field as those don't take margins into +/// account (note that it will render the content outside of the document +/// flow, see [place] to control the content position) +/// +/// # Relative to a container +/// When a layout element (e.g. a [rect]) is nested in another layout container +/// (e.g. a [block]) instead of being a direct descendant of the page, relative +/// widths become relative to the container: +/// +/// ```example +/// #block( +/// width: 100pt, +/// fill: aqua, +/// rect(width: 50%), +/// ) +/// ``` +/// +/// # Scripting +/// You can multiply relative lengths by [ratios]($ratio), [integers]($int), and +/// [floats]($float). +/// /// A relative length has the following fields: /// - `length`: Its length component. /// - `ratio`: Its ratio component. +/// +/// ```example +/// #(100% - 50pt).length \ +/// #(100% - 50pt).ratio +/// ``` #[ty(cast, name = "relative", title = "Relative Length")] #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] pub struct Rel { From 9829bd8326fc67ebf78593bf4e860390c5ae8804 Mon Sep 17 00:00:00 2001 From: alluring-mushroom <86041465+alluring-mushroom@users.noreply.github.com> Date: Tue, 8 Apr 2025 05:56:20 +1000 Subject: [PATCH 101/172] Document exceptions and alternatives to using `type` (#6027) Co-authored-by: Zedd Serjeant Co-authored-by: Laurenz --- crates/typst-library/src/foundations/ty.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/typst-library/src/foundations/ty.rs b/crates/typst-library/src/foundations/ty.rs index 40f7003c3..9d7690283 100644 --- a/crates/typst-library/src/foundations/ty.rs +++ b/crates/typst-library/src/foundations/ty.rs @@ -39,11 +39,25 @@ use crate::foundations::{ /// #type(image("glacier.jpg")). /// ``` /// -/// The type of `10` is `int`. Now, what is the type of `int` or even `type`? +/// The type of `{10}` is `int`. Now, what is the type of `int` or even `type`? /// ```example /// #type(int) \ /// #type(type) /// ``` +/// +/// Unlike other types like `int`, [none] and [auto] do not have a name +/// representing them. To test if a value is one of these, compare your value to +/// them directly, e.g: +/// ```example +/// #let val = none +/// #if val == none [ +/// Yep, it's none. +/// ] +/// ``` +/// +/// Note that `type` will return [`content`] for all document elements. To +/// programmatically determine which kind of content you are dealing with, see +/// [`content.func`]. #[ty(scope, cast)] #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub struct Type(Static); From 94a497a01ffd60743b0a2ae67367be168bbde076 Mon Sep 17 00:00:00 2001 From: Approximately Equal Date: Mon, 7 Apr 2025 13:18:52 -0700 Subject: [PATCH 102/172] Add HTML meta tags for document authors and keywords (#6134) --- crates/typst-html/src/lib.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs index aa769976e..7d78a5da4 100644 --- a/crates/typst-html/src/lib.rs +++ b/crates/typst-html/src/lib.rs @@ -263,13 +263,13 @@ fn handle( /// Wrap the nodes in `` and `` if they are not yet rooted, /// supplying a suitable ``. fn root_element(output: Vec, info: &DocumentInfo) -> SourceResult { + let head = head_element(info); let body = match classify_output(output)? { OutputKind::Html(element) => return Ok(element), OutputKind::Body(body) => body, OutputKind::Leafs(leafs) => HtmlElement::new(tag::body).with_children(leafs), }; - Ok(HtmlElement::new(tag::html) - .with_children(vec![head_element(info).into(), body.into()])) + Ok(HtmlElement::new(tag::html).with_children(vec![head.into(), body.into()])) } /// Generate a `` element. @@ -302,6 +302,24 @@ fn head_element(info: &DocumentInfo) -> HtmlElement { ); } + if !info.author.is_empty() { + children.push( + HtmlElement::new(tag::meta) + .with_attr(attr::name, "authors") + .with_attr(attr::content, info.author.join(", ")) + .into(), + ) + } + + if !info.keywords.is_empty() { + children.push( + HtmlElement::new(tag::meta) + .with_attr(attr::name, "keywords") + .with_attr(attr::content, info.keywords.join(", ")) + .into(), + ) + } + HtmlElement::new(tag::head).with_children(children) } From c21c1c391b48f843c8671993a28eaf1fe0d40b89 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Thu, 10 Apr 2025 11:27:42 +0200 Subject: [PATCH 103/172] Use `measure` `width` argument in `layout` doc (#6160) --- crates/typst-library/src/layout/layout.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/typst-library/src/layout/layout.rs b/crates/typst-library/src/layout/layout.rs index cde3187d3..88252e5e3 100644 --- a/crates/typst-library/src/layout/layout.rs +++ b/crates/typst-library/src/layout/layout.rs @@ -22,7 +22,8 @@ use crate::layout::{BlockElem, Size}; /// #let text = lorem(30) /// #layout(size => [ /// #let (height,) = measure( -/// block(width: size.width, text), +/// width: size.width, +/// text, /// ) /// This text is #height high with /// the current page width: \ From 7e072e24930d8a7524f700b62cabd97ceb4f45e6 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 17 Apr 2025 14:10:27 +0000 Subject: [PATCH 104/172] Add test for flattened accents in math (#6188) --- tests/ref/math-accent-flattened.png | Bin 0 -> 464 bytes tests/suite/math/accent.typ | 8 ++++++++ 2 files changed, 8 insertions(+) create mode 100644 tests/ref/math-accent-flattened.png diff --git a/tests/ref/math-accent-flattened.png b/tests/ref/math-accent-flattened.png new file mode 100644 index 0000000000000000000000000000000000000000..f7764cb74144af9ca15f4a817511bfc5b2b291cd GIT binary patch literal 464 zcmV;>0WbcEP)8mP!bM$Vnv$5`(6eC>tD#OcbrCA+=02DzbvC49pjT#BwPi zkRnWQ(CWaeG`cvo!I#UsF7Ny_-~N=#^Zf@t+wYfWjmit*5Dwvg3BM|zlhfGEmr-)- z>S6x$VnFaWSbd%xlGDg&cLprZlKKvG|04b(82It=_t4%3q^fHka%@+mXI-!RCDZ&Y z<9AsCp9>C8xwpO}7V<^H?69mznqk2gZpM3k1G~eZ-|9wf<~LH0+Jm zgG4b(<}g)=s{TOW2iHAnG8WqlJVDrVrLls}Z>pJX&zO&dL3k2@0n%Q?F zauVA|&|TNRXQDaAjzK4yfaaE{cqvqNdXg>{2)uBny{GNkm5ev1u41XacjSB=PZ?MN z2Ai@5TJg5JHQjpa!+%_^m6_VFYciSfnn-vOAsoUX4EzOh@I@3%PB~xz0000 Date: Fri, 18 Apr 2025 17:27:07 +0300 Subject: [PATCH 105/172] Fix frac syntax section typo (#6193) --- crates/typst-library/src/math/frac.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-library/src/math/frac.rs b/crates/typst-library/src/math/frac.rs index f5c4514d6..dd5986b5f 100644 --- a/crates/typst-library/src/math/frac.rs +++ b/crates/typst-library/src/math/frac.rs @@ -15,7 +15,7 @@ use crate::math::Mathy; /// # 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 +/// expression using round grouping parentheses. Such parentheses are removed /// from the output, but you can nest multiple to force them. #[elem(title = "Fraction", Mathy)] pub struct FracElem { From 14241ec1aae43ce3bff96411f62af76a01c7f709 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Thu, 1 May 2025 17:43:07 +0200 Subject: [PATCH 106/172] Use the right field name for `figure.caption.position` (#6226) --- crates/typst-library/src/model/figure.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-library/src/model/figure.rs b/crates/typst-library/src/model/figure.rs index 78a79a8e2..5a137edbd 100644 --- a/crates/typst-library/src/model/figure.rs +++ b/crates/typst-library/src/model/figure.rs @@ -457,7 +457,7 @@ impl Outlinable for Packed { /// customize the appearance of captions for all figures or figures of a /// specific kind. /// -/// In addition to its `pos` and `body`, the `caption` also provides the +/// In addition to its `position` and `body`, the `caption` also provides the /// figure's `kind`, `supplement`, `counter`, and `numbering` as fields. These /// parts can be used in [`where`]($function.where) selectors and show rules to /// build a completely custom caption. From b322da930fe35ee3d19896de6ab653e2f321e301 Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Tue, 6 May 2025 10:26:55 +0200 Subject: [PATCH 107/172] Respect RTL cell layouting order in grid layout (#6232) Co-authored-by: PgBiel <9021226+PgBiel@users.noreply.github.com> --- crates/typst-layout/src/grid/layouter.rs | 60 +++----- crates/typst-layout/src/grid/rowspans.rs | 16 +- tests/ref/grid-rtl-counter.png | Bin 0 -> 272 bytes tests/ref/grid-rtl-rowspan-counter-equal.png | Bin 0 -> 272 bytes .../ref/grid-rtl-rowspan-counter-mixed-1.png | Bin 0 -> 360 bytes .../ref/grid-rtl-rowspan-counter-mixed-2.png | Bin 0 -> 361 bytes .../grid-rtl-rowspan-counter-unequal-1.png | Bin 0 -> 361 bytes .../grid-rtl-rowspan-counter-unequal-2.png | Bin 0 -> 360 bytes tests/suite/layout/grid/rtl.typ | 140 ++++++++++++++++++ 9 files changed, 173 insertions(+), 43 deletions(-) create mode 100644 tests/ref/grid-rtl-counter.png create mode 100644 tests/ref/grid-rtl-rowspan-counter-equal.png create mode 100644 tests/ref/grid-rtl-rowspan-counter-mixed-1.png create mode 100644 tests/ref/grid-rtl-rowspan-counter-mixed-2.png create mode 100644 tests/ref/grid-rtl-rowspan-counter-unequal-1.png create mode 100644 tests/ref/grid-rtl-rowspan-counter-unequal-2.png diff --git a/crates/typst-layout/src/grid/layouter.rs b/crates/typst-layout/src/grid/layouter.rs index dc9e2238d..99b85eddb 100644 --- a/crates/typst-layout/src/grid/layouter.rs +++ b/crates/typst-layout/src/grid/layouter.rs @@ -11,7 +11,7 @@ use typst_library::layout::{ use typst_library::text::TextElem; use typst_library::visualize::Geometry; use typst_syntax::Span; -use typst_utils::{MaybeReverseIter, Numeric}; +use typst_utils::Numeric; use super::{ generate_line_segments, hline_stroke_at_column, layout_cell, vline_stroke_at_row, @@ -574,7 +574,7 @@ impl<'a> GridLayouter<'a> { // Reverse with RTL so that later columns start first. let mut dx = Abs::zero(); - for (x, &col) in self.rcols.iter().enumerate().rev_if(self.is_rtl) { + for (x, &col) in self.rcols.iter().enumerate() { let mut dy = Abs::zero(); for row in rows { // We want to only draw the fill starting at the parent @@ -643,18 +643,13 @@ impl<'a> GridLayouter<'a> { .sum() }; let width = self.cell_spanned_width(cell, x); - // In the grid, cell colspans expand to the right, - // so we're at the leftmost (lowest 'x') column - // spanned by the cell. However, in RTL, cells - // expand to the left. Therefore, without the - // offset below, cell fills would start at the - // rightmost visual position of a cell and extend - // over to unrelated columns to the right in RTL. - // We avoid this by ensuring the fill starts at the - // very left of the cell, even with colspan > 1. - let offset = - if self.is_rtl { -width + col } else { Abs::zero() }; - let pos = Point::new(dx + offset, dy); + let mut pos = Point::new(dx, dy); + if self.is_rtl { + // In RTL cells expand to the left, thus the + // position must additionally be offset by the + // cell's width. + pos.x = self.width - (dx + width); + } let size = Size::new(width, height); let rect = Geometry::Rect(size).filled(fill); fills.push((pos, FrameItem::Shape(rect, self.span))); @@ -1236,10 +1231,9 @@ impl<'a> GridLayouter<'a> { } let mut output = Frame::soft(Size::new(self.width, height)); - let mut pos = Point::zero(); + let mut offset = Point::zero(); - // Reverse the column order when using RTL. - for (x, &rcol) in self.rcols.iter().enumerate().rev_if(self.is_rtl) { + for (x, &rcol) in self.rcols.iter().enumerate() { if let Some(cell) = self.grid.cell(x, y) { // Rowspans have a separate layout step if cell.rowspan.get() == 1 { @@ -1257,25 +1251,17 @@ impl<'a> GridLayouter<'a> { let frame = layout_cell(cell, engine, disambiguator, self.styles, pod)? .into_frame(); - let mut pos = pos; + let mut pos = offset; if self.is_rtl { - // In the grid, cell colspans expand to the right, - // so we're at the leftmost (lowest 'x') column - // spanned by the cell. However, in RTL, cells - // expand to the left. Therefore, without the - // offset below, the cell's contents would be laid out - // starting at its rightmost visual position and extend - // over to unrelated cells to its right in RTL. - // We avoid this by ensuring the rendered cell starts at - // the very left of the cell, even with colspan > 1. - let offset = -width + rcol; - pos.x += offset; + // In RTL cells expand to the left, thus the position + // must additionally be offset by the cell's width. + pos.x = self.width - (pos.x + width); } output.push_frame(pos, frame); } } - pos.x += rcol; + offset.x += rcol; } Ok(output) @@ -1302,8 +1288,8 @@ impl<'a> GridLayouter<'a> { pod.backlog = &heights[1..]; // Layout the row. - let mut pos = Point::zero(); - for (x, &rcol) in self.rcols.iter().enumerate().rev_if(self.is_rtl) { + let mut offset = Point::zero(); + for (x, &rcol) in self.rcols.iter().enumerate() { if let Some(cell) = self.grid.cell(x, y) { // Rowspans have a separate layout step if cell.rowspan.get() == 1 { @@ -1314,17 +1300,19 @@ impl<'a> GridLayouter<'a> { let fragment = layout_cell(cell, engine, disambiguator, self.styles, pod)?; for (output, frame) in outputs.iter_mut().zip(fragment) { - let mut pos = pos; + let mut pos = offset; if self.is_rtl { - let offset = -width + rcol; - pos.x += offset; + // In RTL cells expand to the left, thus the + // position must additionally be offset by the + // cell's width. + pos.x = self.width - (offset.x + width); } output.push_frame(pos, frame); } } } - pos.x += rcol; + offset.x += rcol; } Ok(Fragment::frames(outputs)) diff --git a/crates/typst-layout/src/grid/rowspans.rs b/crates/typst-layout/src/grid/rowspans.rs index 21992ed02..5ab0417d8 100644 --- a/crates/typst-layout/src/grid/rowspans.rs +++ b/crates/typst-layout/src/grid/rowspans.rs @@ -3,7 +3,6 @@ use typst_library::engine::Engine; use typst_library::foundations::Resolve; use typst_library::layout::grid::resolve::Repeatable; 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::{layout_cell, Cell, GridLayouter}; @@ -23,6 +22,10 @@ pub struct Rowspan { /// specified for the parent cell's `breakable` field. pub is_effectively_unbreakable: bool, /// The horizontal offset of this rowspan in all regions. + /// + /// This is the offset from the text direction start, meaning that, on RTL + /// grids, this is the offset from the right of the grid, whereas, on LTR + /// grids, it is the offset from the left. pub dx: Abs, /// The vertical offset of this rowspan in the first region. pub dy: Abs, @@ -118,10 +121,11 @@ impl GridLayouter<'_> { // Nothing to layout. return Ok(()); }; - let first_column = self.rcols[x]; let cell = self.grid.cell(x, y).unwrap(); let width = self.cell_spanned_width(cell, x); - let dx = if self.is_rtl { dx - width + first_column } else { dx }; + // In RTL cells expand to the left, thus the position + // must additionally be offset by the cell's width. + let dx = if self.is_rtl { self.width - (dx + width) } else { dx }; // Prepare regions. let size = Size::new(width, *first_height); @@ -185,10 +189,8 @@ impl GridLayouter<'_> { /// Checks if a row contains the beginning of one or more rowspan cells. /// If so, adds them to the rowspans vector. 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)); - for (x, dx) in (0..self.rcols.len()).rev_if(self.is_rtl).zip(offsets) { + let offsets = points(self.rcols.iter().copied()); + for (x, dx) in (0..self.rcols.len()).zip(offsets) { let Some(cell) = self.grid.cell(x, y) else { continue; }; diff --git a/tests/ref/grid-rtl-counter.png b/tests/ref/grid-rtl-counter.png new file mode 100644 index 0000000000000000000000000000000000000000..fb0df44ad40da59bfc8ee7d98b1445de8c70d3a3 GIT binary patch literal 272 zcmeAS@N?(olHy`uVBq!ia0vp^6+o=Y0VEjK$QP*tsq3CDjv*Ddl7HAcG$dYm6xi*q zE4Q@*#5lR}$N9$H%qfMZf+Rj4Ul(fcY4Y(nv{N3;QLEU8< zD>nBvMb>BE7Z5$5uXcuyIY=aW&y(;2^ACqNtodcEuP&Tic=DpR>E*9$tG7JM57(nv{N3;QLEU8< zD>nBvMb>BE7Z5$5uXcuyIY=aW&y(;2^ACqNtodcEuP&Tic=DpR>E*9$tG7JM57}9D*9cp;sUck_{3~S`;=krMT5lXeI?g1rAn75JqAY#E}hMh4fD= zC^+cO&ie+z?dM&b1i$a}Y@h83(4ztZ3oNj}0t@U0HjmUE!Qs3;_5Ce_?o#hv!=wuK zihQl6>8f)7fd@FckmckGXtEc_L7yA(I=im`bB7+_HGM<*t? z%Ra)5fn1g$4O)w}(6^5;`niq?aHm*uAOZ_4u)qTUGx!I#s*xWevRUx}0000{9D*9cp+_JMk_{3~S`;=krMT5lXeI?g1rAn75JqAY#E}hMh4fD= zC^+cO&U*)ew4R6jg8F`@_x9R8K7MIFfdv*=V1Wfz1KS6Bw_*L(6&%dA)7;&#x&Rk@ z_i(dBu3e#z<)ml$eJxsL`x#(n-yIyDOLKGq)Y*x8hL5i+;~l~)_#eMvn!z{E@Ka)G z1n30L5h?-DioM_&o+zCM*lYkqw%oz1&0+gc1Ex>vDbMg(yb#gzuYfigH*m4SLgI?@ zB`_Nv%gb{KEU>`;5q1{Wrja8>3}Eg9cC=%BY01DJ(1i=b`6LA)Ji; zbg-xpuJvWo3>eUuuLM37!kDKfHo%SM$bkqfu)qQf{O92>(vXoKDr-mq00000NkvXX Hu0mjfwc?u% literal 0 HcmV?d00001 diff --git a/tests/ref/grid-rtl-rowspan-counter-unequal-1.png b/tests/ref/grid-rtl-rowspan-counter-unequal-1.png new file mode 100644 index 0000000000000000000000000000000000000000..c091f3a806bb3bbd5cc37d2e5372c59005093466 GIT binary patch literal 361 zcmV-v0ha!WP){9D*9cp+_JMk_{3~S`;=krMT5lXeI?g1rAn75JqAY#E}hMh4fD= zC^+cO&U*)ew4R6jg8F`@_x9R8K7MIFfdv*=V1Wfz1KS6Bw_*L(6&%dA)7;&#x&Rk@ z_i(dBu3e#z<)ml$eJxsL`x#(n-yIyDOLKGq)Y*x8hL5i+;~l~)_#eMvn!z{E@Ka)G z1n30L5h?-DioM_&o+zCM*lYkqw%oz1&0+gc1Ex>vDbMg(yb#gzuYfigH*m4SLgI?@ zB`_Nv%gb{KEU>`;5q1{Wrja8>3}Eg9cC=%BY01DJ(1i=b`6LA)Ji; zbg-xpuJvWo3>eUuuLM37!kDKfHo%SM$bkqfu)qQf{O92>(vXoKDr-mq00000NkvXX Hu0mjfwc?u% literal 0 HcmV?d00001 diff --git a/tests/ref/grid-rtl-rowspan-counter-unequal-2.png b/tests/ref/grid-rtl-rowspan-counter-unequal-2.png new file mode 100644 index 0000000000000000000000000000000000000000..fffccc5664edfcd379a237268f14dd21e18fa39a GIT binary patch literal 360 zcmV-u0hj)XP)}9D*9cp;sUck_{3~S`;=krMT5lXeI?g1rAn75JqAY#E}hMh4fD= zC^+cO&ie+z?dM&b1i$a}Y@h83(4ztZ3oNj}0t@U0HjmUE!Qs3;_5Ce_?o#hv!=wuK zihQl6>8f)7fd@FckmckGXtEc_L7yA(I=im`bB7+_HGM<*t? z%Ra)5fn1g$4O)w}(6^5;`niq?aHm*uAOZ_4u)qTUGx!I#s*xWevRUx}0000 ([\##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten() ) + +--- grid-rtl-counter --- +// Test interaction between RTL and counters +#set text(dir: rtl) +#let test = counter("test") +#grid( + columns: (1fr, 1fr), + inset: 5pt, + align: center, + [ + a: // should produce 1 + #test.step() + #context test.get().first() + ], + [ + b: // should produce 2 + #test.step() + #context test.get().first() + ], +) + +--- grid-rtl-rowspan-counter-equal --- +// Test interaction between RTL and counters +#set text(dir: rtl) +#let test = counter("test") +#grid( + columns: (1fr, 1fr), + inset: 5pt, + align: center, + grid.cell(rowspan: 2, [ + a: // should produce 1 + #test.step() + #context test.get().first() + ]), + grid.cell(rowspan: 2, [ + b: // should produce 2 + #test.step() + #context test.get().first() + ]), +) + +--- grid-rtl-rowspan-counter-unequal-1 --- +// Test interaction between RTL and counters +#set text(dir: rtl) +#let test = counter("test") +#grid( + columns: (1fr, 1fr), + inset: 5pt, + align: center, + grid.cell(rowspan: 5, [ + b: // will produce 2 + #test.step() + #context test.get().first() + ]), + grid.cell(rowspan: 2, [ + a: // will produce 1 + #test.step() + #context test.get().first() + ]), + grid.cell(rowspan: 3, [ + c: // will produce 3 + #test.step() + #context test.get().first() + ]), +) + +--- grid-rtl-rowspan-counter-unequal-2 --- +// Test interaction between RTL and counters +#set text(dir: rtl) +#let test = counter("test") +#grid( + columns: (1fr, 1fr), + inset: 5pt, + align: center, + grid.cell(rowspan: 2, [ + a: // will produce 1 + #test.step() + #context test.get().first() + ]), + grid.cell(rowspan: 5, [ + b: // will produce 2 + #test.step() + #context test.get().first() + ]), + grid.cell(rowspan: 3, [ + c: // will produce 3 + #test.step() + #context test.get().first() + ]), +) + +--- grid-rtl-rowspan-counter-mixed-1 --- +// Test interaction between RTL and counters +#set text(dir: rtl) +#let test = counter("test") +#grid( + columns: (1fr, 1fr), + inset: 5pt, + align: center, + [ + a: // will produce 1 + #test.step() + #context test.get().first() + ], + grid.cell(rowspan: 2, [ + b: // will produce 2 + #test.step() + #context test.get().first() + ]), + [ + c: // will produce 3 + #test.step() + #context test.get().first() + ], +) + +--- grid-rtl-rowspan-counter-mixed-2 --- +// Test interaction between RTL and counters +#set text(dir: rtl) +#let test = counter("test") +#grid( + columns: (1fr, 1fr), + inset: 5pt, + align: center, + grid.cell(rowspan: 2, [ + b: // will produce 2 + #test.step() + #context test.get().first() + ]), + [ + a: // will produce 1 + #test.step() + #context test.get().first() + ], + [ + c: // will produce 3 + #test.step() + #context test.get().first() + ] +) From 9b09146a6b5e936966ed7ee73bce9dd2df3810ae Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Tue, 6 May 2025 16:03:48 +0200 Subject: [PATCH 108/172] Use list spacing for attach spacing in tight lists (#6242) --- crates/typst-library/src/model/enum.rs | 9 +++++---- crates/typst-library/src/model/list.rs | 9 +++++---- crates/typst-library/src/model/terms.rs | 8 +++++--- .../ref/issue-6242-tight-list-attach-spacing.png | Bin 0 -> 410 bytes tests/suite/model/list.typ | 8 ++++++++ 5 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 tests/ref/issue-6242-tight-list-attach-spacing.png diff --git a/crates/typst-library/src/model/enum.rs b/crates/typst-library/src/model/enum.rs index 2d95996ab..f1f93702b 100644 --- a/crates/typst-library/src/model/enum.rs +++ b/crates/typst-library/src/model/enum.rs @@ -259,10 +259,11 @@ impl Show for Packed { .spanned(self.span()); if tight { - let leading = ParElem::leading_in(styles); - let spacing = - VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); - realized = spacing + realized; + let spacing = self + .spacing(styles) + .unwrap_or_else(|| ParElem::leading_in(styles).into()); + let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack(); + realized = v + realized; } Ok(realized) diff --git a/crates/typst-library/src/model/list.rs b/crates/typst-library/src/model/list.rs index d93ec9172..3c3afd338 100644 --- a/crates/typst-library/src/model/list.rs +++ b/crates/typst-library/src/model/list.rs @@ -166,10 +166,11 @@ impl Show for Packed { .spanned(self.span()); if tight { - let leading = ParElem::leading_in(styles); - let spacing = - VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); - realized = spacing + realized; + let spacing = self + .spacing(styles) + .unwrap_or_else(|| ParElem::leading_in(styles).into()); + let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack(); + realized = v + realized; } Ok(realized) diff --git a/crates/typst-library/src/model/terms.rs b/crates/typst-library/src/model/terms.rs index e197ff318..3df74cd9e 100644 --- a/crates/typst-library/src/model/terms.rs +++ b/crates/typst-library/src/model/terms.rs @@ -189,13 +189,15 @@ impl Show for Packed { .styled(TermsElem::set_within(true)); if tight { - let leading = ParElem::leading_in(styles); - let spacing = VElem::new(leading.into()) + let spacing = self + .spacing(styles) + .unwrap_or_else(|| ParElem::leading_in(styles).into()); + let v = VElem::new(spacing.into()) .with_weak(true) .with_attach(true) .pack() .spanned(span); - realized = spacing + realized; + realized = v + realized; } Ok(realized) diff --git a/tests/ref/issue-6242-tight-list-attach-spacing.png b/tests/ref/issue-6242-tight-list-attach-spacing.png new file mode 100644 index 0000000000000000000000000000000000000000..48920008b1350f8b604d4e06842d25b282ae1afd GIT binary patch literal 410 zcmV;L0cHM)P)0004DNkl_mzthIoO<(BZ&hpc2JIEVmAj-D~C-E!p%iy#TpKiDY3^`zc26O_^cr~*i$nC=fnOZLAW@u z4gf5kZj1@SXHC5TQ1Mz;cS#N=Rseud8kRTZGrsj6P+jrmwLlbBSYd_#0i2By8sZG5zg4v90VZUCk*?O0PyXO zLgqz2W6G<6ohvO6g%ws<;l_k>sj><9b^1Iucx(V3zlXQ5Ap8jc^$y{Tv#pZ=l-h;i zo0bs(aAtuYIhc~YlkUsne#XQ*RpXI&Z7zisR=8o|U!MOHPD9hDwg3PC07*qoM6N<$ Eg2vyu4*&oF literal 0 HcmV?d00001 diff --git a/tests/suite/model/list.typ b/tests/suite/model/list.typ index 9bed930bb..796a7b069 100644 --- a/tests/suite/model/list.typ +++ b/tests/suite/model/list.typ @@ -304,3 +304,11 @@ World - C - = D E + +--- issue-6242-tight-list-attach-spacing --- +// Nested tight lists should be uniformly spaced when list spacing is set. +#set list(spacing: 1.2em) +- A + - B + - C +- C From 54c5113a83d317ed9c37a129ad90165c100bd25d Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Mon, 12 May 2025 10:06:18 +0200 Subject: [PATCH 109/172] Catch indefinite loop in realization due to cycle between show and grouping rule (#6259) --- crates/typst-realize/src/lib.rs | 11 +++++++++++ tests/suite/styling/show.typ | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/crates/typst-realize/src/lib.rs b/crates/typst-realize/src/lib.rs index 151ae76ba..7d2460a89 100644 --- a/crates/typst-realize/src/lib.rs +++ b/crates/typst-realize/src/lib.rs @@ -655,6 +655,7 @@ fn visit_grouping_rules<'a>( let matching = s.rules.iter().find(|&rule| (rule.trigger)(content, &s.kind)); // Try to continue or finish an existing grouping. + let mut i = 0; while let Some(active) = s.groupings.last() { // Start a nested group if a rule with higher priority matches. if matching.is_some_and(|rule| rule.priority > active.rule.priority) { @@ -670,6 +671,16 @@ fn visit_grouping_rules<'a>( } finish_innermost_grouping(s)?; + i += 1; + if i > 512 { + // It seems like this case is only hit when there is a cycle between + // a show rule and a grouping rule. The show rule produces content + // that is matched by a grouping rule, which is then again processed + // by the show rule, and so on. The two must be at an equilibrium, + // otherwise either the "maximum show rule depth" or "maximum + // grouping depth" errors are triggered. + bail!(content.span(), "maximum grouping depth exceeded"); + } } // Start a new grouping. diff --git a/tests/suite/styling/show.typ b/tests/suite/styling/show.typ index e8ddf5534..f3d9efd55 100644 --- a/tests/suite/styling/show.typ +++ b/tests/suite/styling/show.typ @@ -258,3 +258,11 @@ I am *strong*, I am _emphasized_, and I am #[special]. = Hello *strong* + +--- issue-5690-oom-par-box --- +// Error: 3:6-5:1 maximum grouping depth exceeded +#show par: box + +Hello + +World From 26c19a49c8a73b1e7f7c299b9e25e57acfcd7eac Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Mon, 12 May 2025 10:07:43 +0200 Subject: [PATCH 110/172] Use the infer crate to determine if pdf embeds should be compressed (#6256) --- Cargo.lock | 7 ++++ Cargo.toml | 1 + crates/typst-pdf/Cargo.toml | 1 + crates/typst-pdf/src/embed.rs | 70 ++++++++++++++++++++++++++++++++++- 4 files changed, 78 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index ab2d2cc83..4b70e06bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1259,6 +1259,12 @@ dependencies = [ "serde", ] +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" + [[package]] name = "inotify" version = "0.11.0" @@ -3127,6 +3133,7 @@ dependencies = [ "comemo", "ecow", "image", + "infer", "krilla", "krilla-svg", "serde", diff --git a/Cargo.toml b/Cargo.toml index 12870b809..bc563b980 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ icu_segmenter = { version = "1.4", features = ["serde"] } if_chain = "1" image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] } indexmap = { version = "2", features = ["serde"] } +infer = { version = "0.19.0", default-features = false } kamadak-exif = "0.6" krilla = { version = "0.4.0", default-features = false, features = ["raster-images", "comemo", "rayon"] } krilla-svg = "0.1.0" diff --git a/crates/typst-pdf/Cargo.toml b/crates/typst-pdf/Cargo.toml index f6f08b5bc..5745d0530 100644 --- a/crates/typst-pdf/Cargo.toml +++ b/crates/typst-pdf/Cargo.toml @@ -23,6 +23,7 @@ bytemuck = { workspace = true } comemo = { workspace = true } ecow = { workspace = true } image = { workspace = true } +infer = { workspace = true } krilla = { workspace = true } krilla-svg = { workspace = true } serde = { workspace = true } diff --git a/crates/typst-pdf/src/embed.rs b/crates/typst-pdf/src/embed.rs index 6ed65a2b6..f0cd9060a 100644 --- a/crates/typst-pdf/src/embed.rs +++ b/crates/typst-pdf/src/embed.rs @@ -34,6 +34,8 @@ pub(crate) fn embed_files( }, }; let data: Arc + Send + Sync> = Arc::new(embed.data.clone()); + // TODO: update when new krilla version lands (https://github.com/LaurenzV/krilla/pull/203) + let compress = should_compress(&embed.data).unwrap_or(true); let file = EmbeddedFile { path, @@ -41,7 +43,7 @@ pub(crate) fn embed_files( description, association_kind, data: data.into(), - compress: true, + compress, location: Some(span.into_raw().get()), }; @@ -52,3 +54,69 @@ pub(crate) fn embed_files( Ok(()) } + +fn should_compress(data: &[u8]) -> Option { + let ty = infer::get(data)?; + match ty.matcher_type() { + infer::MatcherType::App => None, + infer::MatcherType::Archive => match ty.mime_type() { + #[rustfmt::skip] + "application/zip" + | "application/vnd.rar" + | "application/gzip" + | "application/x-bzip2" + | "application/vnd.bzip3" + | "application/x-7z-compressed" + | "application/x-xz" + | "application/vnd.ms-cab-compressed" + | "application/vnd.debian.binary-package" + | "application/x-compress" + | "application/x-lzip" + | "application/x-rpm" + | "application/zstd" + | "application/x-lz4" + | "application/x-ole-storage" => Some(false), + _ => None, + }, + infer::MatcherType::Audio => match ty.mime_type() { + #[rustfmt::skip] + "audio/mpeg" + | "audio/m4a" + | "audio/opus" + | "audio/ogg" + | "audio/x-flac" + | "audio/amr" + | "audio/aac" + | "audio/x-ape" => Some(false), + _ => None, + }, + infer::MatcherType::Book => None, + infer::MatcherType::Doc => None, + infer::MatcherType::Font => None, + infer::MatcherType::Image => match ty.mime_type() { + #[rustfmt::skip] + "image/jpeg" + | "image/jp2" + | "image/png" + | "image/webp" + | "image/vnd.ms-photo" + | "image/heif" + | "image/avif" + | "image/jxl" + | "image/vnd.djvu" => None, + _ => None, + }, + infer::MatcherType::Text => None, + infer::MatcherType::Video => match ty.mime_type() { + #[rustfmt::skip] + "video/mp4" + | "video/x-m4v" + | "video/x-matroska" + | "video/webm" + | "video/quicktime" + | "video/x-flv" => Some(false), + _ => None, + }, + infer::MatcherType::Custom => None, + } +} From 22a117a091f2d5936533d361098e7483f2997568 Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Mon, 12 May 2025 11:16:38 +0200 Subject: [PATCH 111/172] Prohibit some line break opportunities between LTR-ISOLATE and OBJECT-REPLACEMENT-CHARACTER (#6251) Co-authored-by: Max Co-authored-by: Laurenz --- crates/typst-layout/src/inline/linebreak.rs | 25 ++++++++++++++++-- .../ref/issue-5489-matrix-stray-linebreak.png | Bin 0 -> 644 bytes tests/suite/layout/inline/linebreak.typ | 8 ++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 tests/ref/issue-5489-matrix-stray-linebreak.png diff --git a/crates/typst-layout/src/inline/linebreak.rs b/crates/typst-layout/src/inline/linebreak.rs index 31512604f..ada048c7d 100644 --- a/crates/typst-layout/src/inline/linebreak.rs +++ b/crates/typst-layout/src/inline/linebreak.rs @@ -690,13 +690,34 @@ fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) { let breakpoint = if point == text.len() { Breakpoint::Mandatory } else { + const OBJ_REPLACE: char = '\u{FFFC}'; match lb.get(c) { - // Fix for: https://github.com/unicode-org/icu4x/issues/4146 - LineBreak::Glue | LineBreak::WordJoiner | LineBreak::ZWJ => continue, LineBreak::MandatoryBreak | LineBreak::CarriageReturn | LineBreak::LineFeed | LineBreak::NextLine => Breakpoint::Mandatory, + + // https://github.com/typst/typst/issues/5489 + // + // OBJECT-REPLACEMENT-CHARACTERs provide Contingent Break + // opportunities before and after by default. This behaviour + // is however tailorable, see: + // https://www.unicode.org/reports/tr14/#CB + // https://www.unicode.org/reports/tr14/#TailorableBreakingRules + // https://www.unicode.org/reports/tr14/#LB20 + // + // Don't provide a line breaking opportunity between a LTR- + // ISOLATE (or any other Combining Mark) and an OBJECT- + // REPLACEMENT-CHARACTER representing an inline item, if the + // LTR-ISOLATE could end up as the only character on the + // previous line. + LineBreak::CombiningMark + if text[point..].starts_with(OBJ_REPLACE) + && last + c.len_utf8() == point => + { + continue; + } + _ => Breakpoint::Normal, } }; diff --git a/tests/ref/issue-5489-matrix-stray-linebreak.png b/tests/ref/issue-5489-matrix-stray-linebreak.png new file mode 100644 index 0000000000000000000000000000000000000000..2d278bd5c9cadb4b26a5f26dd0565f6a4bfafedf GIT binary patch literal 644 zcmV-~0(FAT!-&LSWKIn|;D1$L9BQHvo)N3y(o! zS^G3^ywWxJ5Y)`U>Po{Kj5rF?Ij!(01~NLFa2Ngby2A*Mfxxfs9@;=E=}=+VWfyTy zDLf8Rpj);=g+Xcye&U=~c=84WJ<1lSysCzuK@Zu@B(?CoAF_@h2uUI05ri6KJ>i?Z zaPIuG{t1Zu^VGu81pscAW#cuE;v-~jkKzL$T8L5$;~^CL!D~H2uJmddiVWMpND28$Q z9Fi2@V5|9T^`HX94_n7lygY3NE&yP;c+c?b=?&7-`o^8CFR7l>TLXtnp#%lTCG97n eg|)C29=6|_=gtD%*Z=JR0000 Date: Mon, 12 May 2025 20:12:35 +0200 Subject: [PATCH 112/172] Expand text link boxes vertically by half the leading spacing (#6252) --- crates/typst-layout/src/inline/shaping.rs | 4 +- crates/typst-layout/src/modifiers.rs | 48 ++++++++++++++---- tests/ref/bibliography-basic.png | Bin 7552 -> 7676 bytes tests/ref/bibliography-before-content.png | Bin 17122 -> 17010 bytes tests/ref/bibliography-grid-par.png | Bin 8757 -> 8821 bytes tests/ref/bibliography-indent-par.png | Bin 9096 -> 9120 bytes tests/ref/bibliography-math.png | Bin 4610 -> 4567 bytes tests/ref/bibliography-multiple-files.png | Bin 16310 -> 16308 bytes tests/ref/bibliography-ordering.png | Bin 11795 -> 11741 bytes tests/ref/block-consistent-width.png | Bin 920 -> 947 bytes tests/ref/cite-footnote.png | Bin 13532 -> 13383 bytes tests/ref/cite-form.png | Bin 10863 -> 10698 bytes tests/ref/cite-group.png | Bin 4745 -> 4806 bytes tests/ref/cite-grouping-and-ordering.png | Bin 841 -> 869 bytes tests/ref/figure-basic.png | Bin 7911 -> 7850 bytes tests/ref/footnote-basic.png | Bin 395 -> 417 bytes tests/ref/footnote-block-at-end.png | Bin 617 -> 643 bytes tests/ref/footnote-block-fr.png | Bin 833 -> 867 bytes .../ref/footnote-break-across-pages-block.png | Bin 1263 -> 1280 bytes .../ref/footnote-break-across-pages-float.png | Bin 1428 -> 1459 bytes .../footnote-break-across-pages-nested.png | Bin 1315 -> 1342 bytes tests/ref/footnote-break-across-pages.png | Bin 5473 -> 5489 bytes tests/ref/footnote-duplicate.png | Bin 7510 -> 7555 bytes tests/ref/footnote-entry.png | Bin 1793 -> 1831 bytes tests/ref/footnote-float-priority.png | Bin 1433 -> 1450 bytes tests/ref/footnote-in-caption.png | Bin 6154 -> 6044 bytes tests/ref/footnote-in-columns.png | Bin 1248 -> 1283 bytes tests/ref/footnote-in-list.png | Bin 2507 -> 2541 bytes tests/ref/footnote-in-place.png | Bin 1110 -> 1132 bytes tests/ref/footnote-in-table.png | Bin 12727 -> 12817 bytes tests/ref/footnote-invariant.png | Bin 1080 -> 1099 bytes tests/ref/footnote-multiple-in-one-line.png | Bin 699 -> 739 bytes .../footnote-nested-break-across-pages.png | Bin 1324 -> 1369 bytes tests/ref/footnote-nested.png | Bin 2539 -> 2579 bytes tests/ref/footnote-ref-call.png | Bin 515 -> 547 bytes tests/ref/footnote-ref-forward.png | Bin 1202 -> 1227 bytes tests/ref/footnote-ref-in-footnote.png | Bin 2524 -> 2580 bytes tests/ref/footnote-ref-multiple.png | Bin 4407 -> 4425 bytes tests/ref/footnote-ref.png | Bin 1466 -> 1497 bytes tests/ref/footnote-space-collapsing.png | Bin 749 -> 772 bytes tests/ref/footnote-styling.png | Bin 828 -> 850 bytes tests/ref/issue-1433-footnote-in-list.png | Bin 524 -> 558 bytes tests/ref/issue-1597-cite-footnote.png | Bin 508 -> 531 bytes tests/ref/issue-2531-cite-show-set.png | Bin 981 -> 989 bytes tests/ref/issue-3481-cite-location.png | Bin 500 -> 508 bytes tests/ref/issue-3699-cite-twice-et-al.png | Bin 2297 -> 1857 bytes .../ref/issue-4454-footnote-ref-numbering.png | Bin 802 -> 841 bytes ...ue-4618-bibliography-set-heading-level.png | Bin 5129 -> 5175 bytes ...ue-5256-multiple-footnotes-in-footnote.png | Bin 796 -> 820 bytes ...354-footnote-empty-frame-infinite-loop.png | Bin 2342 -> 1105 bytes ...ssue-5435-footnote-migration-in-floats.png | Bin 448 -> 475 bytes ...ssue-5496-footnote-in-float-never-fits.png | Bin 399 -> 409 bytes ...ssue-5496-footnote-never-fits-multiple.png | Bin 1211 -> 1230 bytes tests/ref/issue-5496-footnote-never-fits.png | Bin 399 -> 409 bytes ...sue-5496-footnote-separator-never-fits.png | Bin 226 -> 242 bytes ...03-cite-group-interrupted-by-par-align.png | Bin 1487 -> 1216 bytes tests/ref/issue-5503-cite-in-align.png | Bin 393 -> 396 bytes tests/ref/issue-622-hide-meta-cite.png | Bin 2470 -> 2429 bytes tests/ref/issue-758-link-repeat.png | Bin 1836 -> 1848 bytes tests/ref/issue-785-cite-locate.png | Bin 9441 -> 9284 bytes tests/ref/issue-footnotes-skip-first-page.png | Bin 524 -> 567 bytes tests/ref/linebreak-cite-punctuation.png | Bin 10391 -> 10455 bytes tests/ref/linebreak-link-end.png | Bin 2081 -> 2051 bytes tests/ref/linebreak-link-justify.png | Bin 12210 -> 11661 bytes tests/ref/linebreak-link.png | Bin 6423 -> 3994 bytes tests/ref/link-basic.png | Bin 6240 -> 5991 bytes tests/ref/link-bracket-balanced.png | Bin 3948 -> 2506 bytes tests/ref/link-bracket-unbalanced-closing.png | Bin 2183 -> 1648 bytes tests/ref/link-show.png | Bin 2599 -> 2563 bytes tests/ref/link-to-label.png | Bin 962 -> 1013 bytes tests/ref/link-to-page.png | Bin 981 -> 892 bytes tests/ref/link-trailing-period.png | Bin 2989 -> 2958 bytes tests/ref/link-transformed.png | Bin 1247 -> 1239 bytes tests/ref/math-equation-numbering.png | Bin 4699 -> 4615 bytes tests/ref/measure-citation-deeply-nested.png | Bin 711 -> 711 bytes tests/ref/measure-citation-in-flow.png | Bin 729 -> 726 bytes tests/ref/par-semantic-align.png | Bin 3082 -> 3104 bytes tests/ref/quote-cite-format-author-date.png | Bin 2131 -> 2119 bytes .../quote-cite-format-label-or-numeric.png | Bin 2170 -> 2144 bytes tests/ref/quote-cite-format-note.png | Bin 2889 -> 2800 bytes tests/ref/quote-inline.png | Bin 1437 -> 1476 bytes tests/ref/ref-basic.png | Bin 4001 -> 4006 bytes tests/ref/ref-form-page-unambiguous.png | Bin 2859 -> 2929 bytes tests/ref/ref-form-page.png | Bin 3592 -> 3561 bytes tests/ref/ref-supplements.png | Bin 8266 -> 8167 bytes tests/ref/show-text-citation-smartquote.png | Bin 403 -> 434 bytes tests/ref/show-text-citation.png | Bin 524 -> 496 bytes tests/ref/show-text-in-citation.png | Bin 795 -> 811 bytes tests/ref/table-header-citation.png | Bin 626 -> 632 bytes 89 files changed, 40 insertions(+), 12 deletions(-) diff --git a/crates/typst-layout/src/inline/shaping.rs b/crates/typst-layout/src/inline/shaping.rs index 8236d1e36..ca723c0a5 100644 --- a/crates/typst-layout/src/inline/shaping.rs +++ b/crates/typst-layout/src/inline/shaping.rs @@ -20,7 +20,7 @@ use unicode_bidi::{BidiInfo, Level as BidiLevel}; use unicode_script::{Script, UnicodeScript}; use super::{decorate, Item, Range, SpanMapper}; -use crate::modifiers::{FrameModifiers, FrameModify}; +use crate::modifiers::FrameModifyText; /// The result of shaping text. /// @@ -327,7 +327,7 @@ impl<'a> ShapedText<'a> { offset += width; } - frame.modify(&FrameModifiers::get_in(self.styles)); + frame.modify_text(self.styles); frame } diff --git a/crates/typst-layout/src/modifiers.rs b/crates/typst-layout/src/modifiers.rs index ac5f40b04..b0371d63e 100644 --- a/crates/typst-layout/src/modifiers.rs +++ b/crates/typst-layout/src/modifiers.rs @@ -1,6 +1,6 @@ use typst_library::foundations::StyleChain; -use typst_library::layout::{Fragment, Frame, FrameItem, HideElem, Point}; -use typst_library::model::{Destination, LinkElem}; +use typst_library::layout::{Abs, Fragment, Frame, FrameItem, HideElem, Point, Sides}; +use typst_library::model::{Destination, LinkElem, ParElem}; /// Frame-level modifications resulting from styles that do not impose any /// layout structure. @@ -52,14 +52,7 @@ pub trait FrameModify { impl FrameModify for Frame { fn modify(&mut self, modifiers: &FrameModifiers) { - if let Some(dest) = &modifiers.dest { - let size = self.size(); - self.push(Point::zero(), FrameItem::Link(dest.clone(), size)); - } - - if modifiers.hidden { - self.hide(); - } + modify_frame(self, modifiers, None); } } @@ -82,6 +75,41 @@ where } } +pub trait FrameModifyText { + /// Resolve and apply [`FrameModifiers`] for this text frame. + fn modify_text(&mut self, styles: StyleChain); +} + +impl FrameModifyText for Frame { + fn modify_text(&mut self, styles: StyleChain) { + let modifiers = FrameModifiers::get_in(styles); + let expand_y = 0.5 * ParElem::leading_in(styles); + let outset = Sides::new(Abs::zero(), expand_y, Abs::zero(), expand_y); + modify_frame(self, &modifiers, Some(outset)); + } +} + +fn modify_frame( + frame: &mut Frame, + modifiers: &FrameModifiers, + link_box_outset: Option>, +) { + if let Some(dest) = &modifiers.dest { + let mut pos = Point::zero(); + let mut size = frame.size(); + if let Some(outset) = link_box_outset { + pos.y -= outset.top; + pos.x -= outset.left; + size += outset.sum_by_axis(); + } + frame.push(pos, FrameItem::Link(dest.clone(), size)); + } + + if modifiers.hidden { + frame.hide(); + } +} + /// Performs layout and modification in one step. /// /// This just runs `layout(styles).modified(&FrameModifiers::get_in(styles))`, diff --git a/tests/ref/bibliography-basic.png b/tests/ref/bibliography-basic.png index 0844eaf81ab407044d1d0b527749d44807d691c4..86d02cc697884337c0bc077f6532359da5451858 100644 GIT binary patch literal 7676 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 literal 17122 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 diff --git a/tests/ref/bibliography-indent-par.png b/tests/ref/bibliography-indent-par.png index 02278124b81abe597746918206f3a211949a7e5c..031e289743ec3bbc86a185baf1725649157f93b2 100644 GIT binary patch delta 8914 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 P_GlU^I?A<*Ht_!e-Ez^E delta 8938 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 x+r1P|($I1Wz diff --git a/tests/ref/bibliography-math.png b/tests/ref/bibliography-math.png index 6b60fef4a2cb9f91c71783df66d09c1999e90ca8..6d6d88d53a0d4ba6dfb5b44789df9eba98dae224 100644 GIT binary patch literal 4567 zcmV;|5h(77P)i000r5Nkl zR(Lk7pZxmygINJ#!S{u=TrOV2%s^wW?IAOePZwg;52xrBcCQ@Rr(OFc^(SI-M?=OiGdzkH?KhVmgP9kU@#DoLZQ&>^`a;mjmG2gNJNv##N+W`UxamEFc{oy zHbgX=&AeW3x7)2$DuqIUWm%uk$1qGTms_n?L==m~n$2dVQcg!QlI4`v0tHUkI?5EdXT0E7hy z3lJ6n!UBW^2nzsV0m1@=1@KpfB}vkLzyHn7-GgEqg>e913>K7;GFX&N7G)4hJ_ZA& zFc@seM^W-oObVq8qR3z{Sx`ihfijTMS1BwCgM3VSe|73k&y(Tp?R|QN`wXY+d|l@p z*Z;Y$bL;$1a=m-ZZ*Onuu-oV7_~u1=0Bd7oV}5?#HBonw7Zw&=`uq6!h*}>W9wsIx zUS3{Yc(1Om&dkgl8XEdJ+&$8YiVD;*_e(>~&d$!#(o%{y3Gr=hZQ0q`ZcE1|PYYnV z>RwV(f`Ho7({p-yT2)o$Zu{ouW@ueoTSKGR+uI|fROmJ`GLoQSd3jl%si~<@tgo*} zvPk_t{RS+rva&KZdAfwc?(S|TSy@>jqY9D{b$%i*2wPiQnnp)Q6Nay^um1!VX5wFk zMX{CJ`}civtCr`75t}?+DE1gQk$ic1DHq-3&MR<+SaE!O9Ce_PKNApDCaXf-;n?t9 zzrMag&H4Gc;@H^O{{DW%oPK(GLQd@M?Hw2x&?J8GH)ye_i=ANT;t>8larHX2Mb3%dVpYfc$hbBX=$O5T0jSD zvslL2*%`HJqUGJ(+!zAi^mu_nrKxSrF7os96;Y3zDMic}8ULN7!*hsbj0Z`(ySq6# zIYmW9D1@3E>RD@XaWRBK-9jkTncUvqo*W9x%gYUcFxni~a9l%uQ&W?A0pcOeE_m4LMSAMPzb)cxw-h+WQ(s?S zI4MGN9LQ4o9JjwdGdo~iUtfE3Cinn(atf^c&(F`~rfA6H<0B{p5~9TXa^D9WSV2L7 z4}!b9I~sVDXm)vFVWAu>>~2yK3KJfUi5T?q-%=n$*VWa9;mJxQp@+x1zrTlx5Lo35oRyWu(w>BH z65{Rnp5JEC&3`UtXJ=tC;Vc-^`};fPd}Q)62$9U&39mjWywl_p_z!z`uM*7?gaJ7B z3QPnOBgMc#K`>KH3{((IL=^l>OcWF&BU3Rm6A=uK2;P8!mK#iG>^X z?*0AU9`9lE#^NYP%^_!IxKB<_VgT8lE5>toX^wof-`?IH7#MJdy}iAHiNE1vU0z;p zZf>#;eobgT@593b=s0BM6^HEi5eLYCCv=y$@@WyztlGw})gt#rV^xl`6C{v=&NC zWV*GQ-!v5eM$X;b+=z~M{`#S)c~*tzaV1TL(Una!0BIey{c=5&U+KIfFynr#|9w80 zE@^&hJgUe{PhzzUls|*Iv@PkyzYyq-!EeriU0+|vrE%!33q7o?n0j@OY4h~-WMgr0 z5u0DDW;0nBAZ0L?oSlV0O7-d=7r{_riPrrG2M2OMe)p;nS8g8XU+di8-+z97My3Fr zgm(Y7m(X&wk+wjQ*brK|y}gx&B{nBIU<0zvOTAndWoiShFK5dbNIcsM zAqxvuGyAl$;fgM;uWg_i9UYazkj)Siw6YB(J}p(NW-~0t$Hzx(Ao?YHiF7%S>>Y!l z9baBvqNP}t(c)>NIyE&F8`5}?{su`08;=A=`cI%;JW6+Lur|)=?((`>bZJehCJMut zo}M-aRlzjl(bCis+A;>oO9cL-fkh&bucDBJi9!&W^r0Ij9H11ral2PrcV`fJrH zu&5kRj;evVv<*=pr?nxI!p$aPmVsxYOzR=PyWhAL3f_jweT>t-V3pUxSq=o1(t!%&(AYtDNsT;U#rkb%(Vkn5a=`ksinOz)w&Ym z;9Eritb{o>p1@E|*0fqC$U_w!CLPoZLXjtE4`9())m~jj98h>K@)_j>mO1 z2uBd~C2n|QC4%&t{ZL?;WTCeWk&=+|YeKja*m4N$j9%WAsUr`Vv7ptnuLq-L=pGN% zlb{BO&mP($Z>fWzaAyVIUa5N;L(kd?+BFh^%- zXb8RyjLM;1yiVs9{8bFOxp&z?s^*)tZ*K{}qIqi$Fs72Na{xr-Jh>_~ug1p4%mLdp z9~iBzt?4c&7DmlRek3_FtON*Zzdod677|>#Vtg-l^V^o3tq3#!7o}H^l`bUxdiDP( zIe5Y$q7o~tzX}%SPkM1XQoyV#(X`H3hp+;y-inofflkS8D5aDFtWpZFN-4nVp;+`< z>Z2z*w6W6m?U&ByL3&wH+vq9$Y^{zJ?Aj6shA+j)w|uZND=mYdbyQ5X!*c}yk2Y_19jPNAWiHQkS*g{za z|Jk7kD#ZKWNF!u@(DY&worC57{M+8W<+u&QP#9iA7M9oSD4Us8_5*%^5G){lD2nl5 zq=&-M*t$@Mq|8IPUtccv;-IDch|V|_*X?BX>-7?{`WG6TK8xqkfyfWj7eDI*Edp$0 zvIZ2Lwh}hG3$U=lLVV%9=&%@FdW;suTaj0BWWegcKGHA(1Z!IWv!n5DW-8W?5Q(Z2 z2fS+B1rYbAp#(Z3U)QCSSn?uj8YkI$!6j?WBBio>VWlvJU5ok(nyVQLBu0vHA8l?` zge6^=EOVt<8T5WOYthjeDzXwI?!s3p#P{7S)VXKK2<=Y2YXU61?h&EdjH1~!HOL@O z#>|kIIzQdE)7UJum^0H`mITU@padPERvrMO*!Vr$&BC)I2P`eS%{i5&~K<&s6BgAs=8y+Ag~gyi!E#sK03PB-G6K+5W{{XF%JfK?$HNlQ38Qp0tqF&QAsuuF$B zbi4{mB$83`py}aK+!o^eGpo3L_H5l+MX$EenUWA; zaZ)tx?)R=iS(1-UkGumq=VvRVwbtgZ1#Kr)VGcR=z0BBLh9Ly3rZfPBFu0dr#MOc$ zOq(g7bKi`R^=nZL&&vUm1BGk9DkDZ2Z~bObWIJZKl&6JAn z#D;#2r#5q}xjc8GIDskq$9uTJig1hGUG4%b<1_luL$Mfb9>l63D7@4#(R?{Rk!Jx8 zmGlp`Yzy2*H0rIWsj4SsiA|cC#;u=(2#W-x1nq{EO;ZESr7&1{9Z7593T?g1BVY!0 zag!b4UYv!mmDpOvtFTxuD0(S32Bw`gOH2#glgu35K}-r@rd&K@!xAvSww^@cHaDv$ zBJH!CY&Lw%n=&q$)yW|~9uF=RMD%EP$P{sofx8?s`)(G+?ARg1Ka0p@`kYfau{H@> zUbhOw!mDEFWyY4friNWiNr=mfyy4B(khAWqpN7qWd^522FF_`$QSM1uRt%RcM*G{> z&w}~uyxh%G)Ny+8zV^B6tWJ%z>#DCyn|saLk&7zVv0XY)zF(xLxTnv(1j1scl!F?SLT)bE^ z6iWMf@eI$iex7$90qlZ4OaKTA5EdXT0E7hy3lJ6n!h-!0R)4Klqh83ejAaql4tSog zR;$UEtApn*jE#pTLXn|i%f0(klB{$)^?F?tMU0EEy3%#sX0s8|%Io*ymu*Q_-n{#G za^P&a()dqUMC5QdbX~{32+N!x2tJ>Wh>T*smg^@@4X%ISjps~cF8X*r_GCell~3&F z&}4LMJe18Bh=0iM_ovfo?2E9>#bPmrVg817<(g|IGH2YG#aEZFI+qeFhYt6bD~+LH z%L^vu@dmEn7$Kr~Jnr#$urI^MxhL#Y;w2rElNeH+>Jovm4nhju$H1 zKaTVt<#Qs(3tNRM5v5Y8a5#*85tccIVFG~w5%D?k*njcUf>@DcC7Ule-QH*OaicSi z7bIC3ICFk2vo6UByRk_`!C;W%IP8nC%xRitx7&#*nOeT>8;!=EPEALgu3L-onp|=$gt9c_N8K$dFKk48?;&N%CTR5G9F}A(0_ujEKCL%M?;xP@bi{;KdM< zvB>>u)$VlMd+VnD&cnYSPHWGb^_{iW{`=RNpYQAI>+kRH?SJhZ9314Ozn7O6)q8n) z+0@i@cXvldYhq%et*vcMO^xC7larIUxHuado95Z1Wkq_Yr>D)$&9}C;kmu&+7$z^s8krx&gkTWte49OE=b#;}l z`ucjK=_L6Wg7yCX{$Fkp--_A$`fgrx!U#0i>_?Q*I5r4C%GN-Suu0}>iDk>_-Oymf77f_V#v1M+XT)bX8YZudS^afrZf1)6>}4 z$Q1GQoUOIBb!uu#{tBuC0|Po48i9pMl9Q9ItgK>VW6__eKP@edE+Q~d2>^wLhN6C7 zUtbFgi+{bnJtMHtx3jY|Q||BYsZLH#T3cIhZf?Q~LVkY!=jSI~US3|H33H4jwj)A< zhV1NYs_E(J0RaJac6QIt&k#ufhyC;L@DLyO@bHkEo6E3>hzMBW%xq0aCtx9adwbW` z))p5RBj@Dgh(!zx45YfVvqSai>B-a6lXv`eGJmuHi~lbboYI*#jJt0!vveRF2U?kRK7D9SS9{A6y8q z$kV6#H?Xp@vZA7*@XLjTg=}$lc4hWTpQxb5BDow}ui$)Z^xGnR-` zZr4Stgp7%Q0jzGf>s^}dIs!+l)$+dMf9I#uY5d7`uMG%nO@r?G`1r7FGzZ_8am0R^ zz7Td=uh-#^fQw9ErBZQcQ}1}kUO&eK2ax1gE|<-wwTk*&(Y5%^3}s3@qOB`T5y=3*ls` zIGId}x3yO)dV(iKr-4hsR)2gbyO=lR&1o~aWMJizft5=x7Yi(bKTOhkz0NP@!hyBh z?HY|nr_({N2WeUS*5WO=)1aLS7I_B}r>yKurQ7YcKoElTtob`(Sz_Aaw~A~ukNQGu zp~RNUfIZqFQQ93;RauKb1e0==vp4P%y-`f@9%2T*dBC;O`kTw&U0&G&` zoE#UK1tBACfgw@ZZhyDpLi72Y*RvP=39rZM5Kw_^-=$XiMVYFg^<`7h!H6e$(QBy_ z`Ks`M9rgu=$GbYJ52WxwxSH{=!aoK zQf!L0c-lCXz!R0lVuAFhBpXa0aZL8lZ^NusE6uUNWZcu!rGI`?(d0O(+E7qKw3H69 z?<;*s`n2pBONYT=V8H)tU>T9fS5b_G55+(Tn+Vu3(T1bf>v5ES0+w-vB)BBT47|L& zAgf$&Hk(Y#UxB35;CLv}Fn@KcBES;8rHNkPGIG)AOr_7ypX}YuiNi1u z1>i~oA-6P?0Sby0zVuNY;bHQzCncTTI7gjhpL0-jJ zY&`X=pPTq~U0noxYzzgkYT;E$m~rif#?%2#RYO>+{`js22?RmkDFi20lBCz{punPJ zvD>=HNZ?3O2qS?lkHAjvk>jWiCNXDGyeq}TXc^mMQq2T2Kz!lOha<}NWa7I*YLbSO z(tqdb=Iixh=K^<1uQOiKo@C_s1hY)3^rDQACkvx#oiFE*DJXh`}urU zrEmB99d8>J=`9`LOrOtZb%2Pxr&fjZ3V*ke4ul{MM%b5rjvxu6v{4*M&KxTNg4(~| z$D)M*VVr2_Sf1~=>C7mZ6A{rZ{{ZX*EKL>BNHU((a*h2gn z>ku}8^;xmool!;^z#3%$>$|cGMoeBtP4$V+G&F#DU>UG4lJR zGvIks9wO^h`}0J5OJtz$(L4aO zqZ#3;P4D-+pRk3p3jcWsM1`EDL>eLML(|hF&SH6vOzwf#5dR2FY0rP>WcK6nKv{i9 z#x|budB#A*57oz?<$@Ljn>JYqMW?OMW}gBqsxXQ#cn>>_qsurZi_)#MSATe9z{-Jr zqG1FOs%@nVN8?3iD%MMgu$;Ta@-S)=*RsilmMk@$^e3{4 zmFnq5tS{2sn_3_oDaE~`IlY3G^q{iLm1KF)`8ndyy}1j$lEAswNX9{{76^7}TC z(X&$f6kx#$nj2Fe1`84IKr`xGYdSWgFBMl01z79~CS9$N<2n-N^amykp|3Mg5-#vq zJr^9thY4BG=5&=2FY(oqlci}I##v?%abB+%8P2x^O-i3{%bp*C(|_28b(Ir^WOt<; zKMMmL)vkn%kAm_1mG#}R$d7>GCx_e`~#Mj(O=7V+X0qTOggOiMLB$wC7& z#)uk+gia_|D(5KuRco6TSjXG#CNDn;3`$sJHx$_<6D^gBu&soj^+Ou>EH0 zPXShgrs1?iv(sv{4u5Y{2FPOAxkD*BU4d{g}i{NcARdh?}PslUcdZ_wh zb)bj#0+cXh&}*HX(x%h#q)?<-f^%Rb7=~U{0Za-UpD;$$S_-?n%7{_%ykS!zz%&PT zfKKW1UVnp&1bcX2YEUAV-B--m@VP^gW}=K1vue(?5t`!61Ai6_Bz+9vNR2U~3g%x~ zSYmSfp?UB1GQ2@N1~2|a#vX3~7d0A4skn8jf6W+X9!bgKBY>sC)sM9WU}a%M*fE*| zEQ~zUhf+I|;DWIFa86o|bPE|ac119HkoZrg`ql~wF zV^MfJ#@boEnfS8gXLk*usJB@#F`_BCni`_JupwXNNt&5hGv$d%;smDHKeLAjdc?G7 z@A4_YQa+^*dJHT^+YF*t2oyFoOtiV2o@izP4lj*A*nhGt5KYr~uSZqYd#%i|Nm5N* z|8a=WNieFRMOf7|Z=g9Y3_4y$+*(Y9mfrCrUsN+jbFp7LXC12} zk?z}@*P7=3X3fY$qqS_8Oa(LY9>M#y0L{ILw`dAY=TZjACUVnh3YxA3TT9!Fd<%=q zCkK diff --git a/tests/ref/bibliography-multiple-files.png b/tests/ref/bibliography-multiple-files.png index 3be3763f49a222b39e2dfef80b8704ea1fdf3bf3..b2e7b0144ae5cd81de570f3c5fcc29bad80c85cd 100644 GIT binary patch literal 16308 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 iss4kUf0Xfw4uOFlFTOCQ@ozsqn6$V8utwA<SZn diff --git a/tests/ref/bibliography-ordering.png b/tests/ref/bibliography-ordering.png index c19b7e7d00d078cad06db5590149caae7593fe89..ad5e86019490b2e659da36cc79dc572832017499 100644 GIT binary patch literal 11741 zcmYj%V{j!vvv!;lpV;Qcww-Kj+jg?S#?FbIjcwbujg4*Z#&^H>e)rb>Gu2boRnyho zGtNIsZCq|n+MAiqs_@Hviz1O24u)mgM~5EPxSKE87+JHVu`xixvsbHt4ORx)OB5+G@cbH@Jx{sIkEG5R0XoH>SZ6ubZz?p$qGC)I))xLWlzb0N`X$w}z5! zZDs02nI&0RVRx11l*KBM?y#x&d+(#66Xd;0%nUtWN|gp=yk@`uY2#53eD=FuqxRl= z%;2fKuTS;_Hexe@Hp#nun2z1r9e?L&E#&ySRlTYC(2&?)gwm;o_JcPw$fXw|Y!_)6q$lbVIUQ?ULZ+`Mo+EK1#M1 z2k(_~Xz`67XNtJ-eG)Iv1=+a1nt!-QPqiIH1OoywC26E?2bPbdxCI_$wufqL43e@x zI)eVNFZEMvQ~R-k$c z5iwiQyg?lBZXGg?E&H}tBSx65I?J{KAWx9yCTNkO5%jzkth1dJNg(?Q>6;+8)89;e zi*LPY@BNXMq>FLq#h*K?OWx&jWwCDWJx_rO!69|Zz0SpbS?{>Qr|7vm+L2(PwgY)J$sLT+}Gh+$I(Flf_6D3g$8a0bbqPM$GBb?-rw5B8fcu1n17&1TO7ePmN)%gHKOSfB*h{e&&1_!@XbuG_GI1zIq!P?tFas z(O_)s?BwR;v}n;EE}yx5e0(^x;%`{7o2;FjSgyj$7NeQ|^%dlZi;J_gu+R~5@G{)> z^6~MRBDbviGcn)U$rn;9TO7ohAvb4fX_=jzSf^9%J(osi)xJrFMiQ{W*o$Ur~7-8*3C|mka%eddaUS@U8Y?-ej>%n<%M2hRoWER zg#NYKVBnsQn;SbjdsAa$uoktNf`Y>C04QMqyUyOu4lJd(xHvEnymw~@nshBaGjkwW zG(Vt3r~02A9_@O&@cYNdCG%E&96~}UG5yq%`;1Zegk$Y(ZNd+ak8bYn?sj%ndW^va zdfW+`|NK-w^Un%RO--DZ_I9`$Cks0e9gPT}yRPo`^Yf>V4}#y1A2Jyp_V!5fwX4w< zka>4lK>Ehzz?3|$SFMv)00s`~X8uVCf#{FxC&okK))gB?zv*EF@dipJ`O$$S_LGp+I=YB~9nf!QO3LPAo>V9mr{xw~MV{%x=f|_)) zUB;6-z2li|Tz&qXoYgkh65vGj?8$)!0cQ;?>5z31lI9yQF&6`H^1y@CAh-}mJZWFA zrB|)78z_=BZ6QaG0neJZY{Np5HaU-_UAww$!;vBz`3)Z=>1Lt{Es6{F>z`}+fMn^N z7Z5nno>A&<+yq@*>If+hLGfjF_yb=2GYl-7r~qdsYfW0r!I@oxYvBk=D3lmd zq*t~K#1G(izHEhHEO6t(`Q2w8K(<2Zut`4`GE8vSkQ?vsKwy~_n_LXFJvm<#ES*!- z?XwmEYzjctUG(UZqzEkHH4!0SRI7HCiIMSNf*kgK6f~SVaAsjj<6t3&kNcZIPFpmp zVZfoD#fg(O;O&vAQtZjVgi5L5S!c*PJt474jPsI1h%1%8-qY+y85TkLU6^xBQ z2CV3Dw@%sPK- zgeoZmxDFK@92{aq$s|?wXVF8MRkr3AOM%lSAX39-5ed?*2w2-@ok)~#rwOcAZoH+l zgmI(E?6(%*G=o+m8*fePTPaM{@zrKfMUfqOJ_}*LmzBN$QO$_`dN(*v3szIZ5XDSc zMcNxJ#XFg~$yNOOIjDNDc!CYPXU3|{nD*M=%Wd4aXT+TM_~h-j>jv2A@*8b`7}%cO zt98sKUiiF!;-h{d3W>M3y=s;-s3L5q9oUO>tb{Q8-D(O?x@n3KbG!DIk zbf1TF!Y>2gyJ1tioI}i_Sn3=v2v^8u>z5=lj!3CbDk-U_?GwU@uI;d*6! znuH`s;}=T{7OW5tXMUC+hVMsQKX(iNbTmi+?pskf!CGam{Twx! zvC2dlTWDI+j006CvM+u;!~OXKFb3msnB_E8l*G$*N;}5a_``t1$OX^1O@P_L&3nvN^H`mkq!))!(1=^P#!8-CW%(XXN6aHu^<6#m=ie;L}Fwn$O~2B~5BM#mRk zM}@-dk};`d{2KXn7&?FhPh(6vu{-?&BR{$(?U2|KSMfZ8@5r3n6?kFz;|O#0q1Yi! zjOJz>Ov&7=Bbf8?Di-GrP`vr*8>x1Fqo^hrJnH0>gICxaC-_R^6>#l3D=DnK9JR54|Io$b$G8%&i??dY>zpWQK7f ze*maz@;A-PNURzOf$ri_i6+f>In17B?#l+M2#sIAQ$bupwDGn>b2RUD+W|65@ETm| zSfwlbSnLPZ95V3L=>Wuq)(Y&!C>@EcS~ zb|woCwt0Q}m(9Q7M5C_H7}s#k8D^141=g2!32b8ckEc{YZiSNDKTfhgiyooMnpu92 z2U-XFhKQeKj|`{HsLx5??*u80g6Mskp!1#7BEWAm0`xi@I8Ue6`EI&gAEvpu+F&}^ zRHyigsyFVZEy_cFf!a_p7}tR-T}Q#Y#9=&#$o|Yh7{NfOlXFXf;lXB0xPgn>Ucrc1 zmyBTM?^SaL)s$@8#T+cZ3NlX*KDoG}!}Z}pkbJ3j)Lh|2vz}O#c}B1ey!x9$&c%*d zG4>oi6_^jTWM=Y8eCWyiD#{6(CvY)b;Z`~Zk7Qi+cPG?hqcabg6!1we_mfNrxv3pFW2cPRmPM+uRG5bb!lz$8*X`6n(Q*TS*X?XhLEG~3)+%^| zW_b4|q`nT4tITQC->U8(F+8=)jCfS*olJf zy9c@yT#3+UpMpm8vpO7$DF5#JX_=chMllgl0gJIL7wHH0J*1cuOL;-ysL%sLe`uev zfWm5G2&rO}#v6!a;W_(@r;y0h5kfL${b&6|#YY~c_Zb`C?{K|eduF!7rl!9MSlO-| z&hYh|EVfmPZ0p*51tnOdihqP@NWJS|>_P!0b>c_bn6W}eqFlMLgr+mM1Vv4_GGkZ> z#A&Z7!Qo4DmPIk3ITSsxr%Ns>pQDv!s>p+AYeY~&whR97ssPwla>2>9QEM-ty_SWY zv+A08Cr(cfw3g|&ri(iCarRB>NQGO*xy)#A<_)RF^_C8-f}HoR-9TnVGr))lG5_J* zfC*TL6e~T-A=@pPEFL+jvE&BW;UKF%aJGDY7gx3NLrIHgviZINLfzGOX18IPp&Vh|>^JbhUqcK3_!QYRX`Z+q*n?Xn%=f>9ej0iqq|@Ge6i(1rrMqka|3mO<(Y<}Gtu|HP=QD`J zH8+1nv!+m&>^MyP{rFbCVLf=wZM(1}loQPEb+e&SzS)m3TR(m#OTf2Y-UPPSK5j7W zxg(VwWz!V)&c$A0V}XPvi^ZdBW|n)OZM}S^jFd-7$cG3UHpG5qRJ88G{HryA+l0l7 zu-!j>0FY2(-;oHn!O#Tm=VjY%-PrYC9O;Y}jmR$=vaM zkSBHuuyY~mk%^F%;(=OkB(T5^o*lV-4q~7zyS@~PtjwxA)=q`vybP-s^VpNnuA#7v zN?$jP`+bckYZ0Cnb7_*svg@-1vbt%xtnYa4#RJJVxL(=4i=<+gH>`8E@j)#cuuTuv zFDM{yLmMONB=rvua`y-92Rt zG*s6T_%w6PkTu|E#rp1B4muJh9)mpgVgc<1Ie+&*51k5V)cNV)q2i`7Boiw61Q>xf zVd3YR?LqER7$#TU4wwerd1)*`R}A+IwWJ7yXY6W&@=hd` z*En@i`AE=!fc}aQ$Oy9opUn^cMivR?0M{Z^%@t1hjYJkjXn+j7d!<+)=q?yVFoM&X zf3DLC=Ys^*M0D>W#L*0z6r}5xvmH$<6K;vFaRes=6*m2+z_UEu;>JHZx-Vda8Jtql z4ci@-f>itt@;hRIWEm8j<_anoT8%MQi@An26n*tb^KD^)x{ib0srU8pZ5#J14U&@p zr7)K6c+}5}%pgj9fHnc1V)whaCJsFz{W>eKWMUBAgggV}^i-Kyju&F(+ob-;JD8u~ zK&Z%W|b@T5>-98h&a!SxD)VE!$_@|`bjV@d~CDwc*h2|qmp znPOR|#(o3X{d1iVwvW|P**}6ykmPx40eye&ky_DKZz*`HEi<^B6n=&o*t+x6X9jF` zMeFT}1Zcw$)Mopb7U&@ED`RR+=p!3G%mjnsnyyiqk_<+#Uop7As$z`5(j6`B7Tf@i z*_*9~^weP~Mg_}NV3~8_aSAOeU|L}w@^^8mG8fM?qk{Q&Ui22Dz)ZiYCS0Y^{%Rc1 zwbF$RjRiTQGgXj*;fTRuYUkSNTgqohM>a{a-U_)^ndr6gYyWISV~mo(!0rIqP+t!Z zAS4Lc!h5QtISJF~BbtXJ&UL1ZjZVC%Iqb|W5GcjsOgBaP)Vv1#&P~OoVF@3|G;uHr zh8(h+dDYZr0=5+dPs5AJH>_Z0@?-3LT(ZQ5EF;+BM4-S1mCz5kKJpCJr~$ETy{5ul z@VI`O%Cy+{NM?w^l3f_QiTRL~RGvNmus+|RVUU71L46)6rlG{5l~$!#6It`9XG6ic zg4l75-=!2sqg~Xw7E3nBnwZMLgqd~|mCl$s@hd4^qd2=AAWPhw=^pL@R*6K};{+{G zOp{C*^>i*dt3$PsMnd*$vi`emz<9-iXSXdHjUcUzi(5Et63@Z%@Q98-Tc&UWR6dU} z%-Fx5g>ltu8bVF5jyg!bBBzG(h*!O38rkcp!9uQ)V_I)Z!3rGh8vzT4m|-0`;67qR zzdgOOP@Ehmj}C)IX&DJ;R{&U5&i%KZs|v%0gGWW13nJe@d8mr~`xF54*~wFm&nrTm z21*K^8E*2u$hT`*8wcJukz03JJbJtCH2c>LYSRQ66kCaX71dxqn$o;iT;6&|-(615sHrtON)k+MlB{{`4i^-!Oa7|0O`G?)| zCQk`NZt`2;1$cG~bapH%2{-bAWqFpk7=(8sn=#6t70b#f)A0Blv7&Pqnx>_E`&_Bf zQ!>Gr&q_FK`%S4ZT#FX$N$vAcMD|o`+*{v+MK!`^IS^rSm}H7Dg5_8n;>WLS6G=&k z75~gx%vXByEXwp}PBx5D$uii!Y=-1JeysC`>ZIsO!@| zl+G0SR%SMv^hqt8ll15zjZ>_X;YcCX@7E_cgpQwHOxApHCeVM)N{Cw+ONbPK*CmnK zCM`oINCGoM^U)wXpDG`ZLk4x-`Xd>RlTKiz<0tYwT zl$*=!Y+EVX=B1|4diAFRQ4%zj6M#l4h26Am((UQdX(^Gte3ARR*SUSJ)m6?D;%}B7 zQ$6M-ipS(5!D_;5VYAb zUdbl5s~!dVqo0@4NzZnq;2oFf8(H>+zP46#N;n+4zQUol=5fd$PHv#s;R;>M&48@{ zKOOuS)O33W8?ntI!8ORH?9Jh4&=pAEfuw3}4a8S)!s(wt>XQMbqSa^glbdnJh-l5t zV(AAQQ57AeE36}zON(DxeF4Zv4UIV6w@HS`(fYhVHKtMcYV~c$=uhd@>UJDq<=Ozf z-IAI^(y>H}mUWlFsqoQ!0-R}vEoql|>DA^&fCUr%!)xw21$-CV;AOsB8KjfFrH_>b z*{0pB-x&`sD@2uXw)m;!lP#Qk)txR74dQcf#-2!ifD#OD^U;WTK{dTDTRWCkL`XPq zoe?=0Tw=9CMYxPxBdn!91-3hk5-3UtAKkZY@*Na!F^plF*Lww`*y+us769)Vcg z4ahxCSd`jOax~OFHVS;DeRDcbUQ3|_ry0=A0@x=KX+1sLw*;fb+79izA8mXp_oXip zeH`Zm=FAZl)j#+Ud`36$s1An!#W*@%dq-PVv=B%~MaAg?9ZtC3qJ)x7g;mPMNJ2bh zig#e_ky?D5SxSk5kTMMz5e*BCV?JhwCL3bkQxlB!blXf48P@W$s&itA?Y?)<;@Cj_ zPP4MT^Lwxsm6hgfdPn?)-$+oaako7QB@y%U*&z4;NG8bhe|UzZBHX^=zf| zAu|b^fSKO-ZEnDknOFIjvASX_I=(jlt%i-F<5J`}Tn%IhU-Yl?s~>AI(4}c?)=Crc z5bnozN1WQ|vl594R4Jq?tEH!Atg$$jxbF5SfI`CtHyu&D8fsyK?_3h_@nEDWV{@-H zpU2xU?D{6P))9b~4j!M9itpJ#GhY2G^@2 zhGkoU0P_?Mq|BoCGfpD8>!Ob0pI$51+lmK^OnJXhE{XhNJtJm3X~IAi3b96no@_-Y zXQ>HMQv)5orzVnA`FzQjLFDd~$RyD1SCnt5jwfbGhnyZ)?8OSN5iGCL2fw_Lh>Qj@r?rY%tiVvX;7h4B!Ma~x&>C5hehvYF#sCNGM4Y#Lw{4dmts zWbJ9+XGl`kQs^?F%QgpplBQVqZ9ayp(-dGh{6@%Pzyh9Wg+o}$afoZWB23x+0# z@#7^Ro|etsR9RU%V?m72jU^;xdvAgq8vV9mT5>p@#pjZsu>z(*P4^;7Tk0HvrzGuV zkY=N>Eipob{rAE5z)If;4$Yo?g@&n_^Ii1G?5|3E{?TbWG36qKLg#9Hbp9yG$|3uW|LgCx?kdxr^tQBDKNzmftjS_ zya9sZR)#ulykaNC2%kXW=C0#)A#{TBJ+dCr_@Sgt&|y) zc}`)Yd_3S1*ZW#Hjr&*j+&Z{>r^B>~r9ax+75{!!4vrK$y^CUFW%DqNAKlaF{W+_u z^OhX4Rk07Xp5o!bdO$9@C7ZB!STSUu@qFt_@^Vg;pqP69CiVmh0uTU7ov1wB{vT$w z%rH-d8~BG|Nq(9C_x|V}Y|sC5_3QG(vhc~j6!=f0hvI*A5nu-ly$ zYkfUV_$~@n*QTEQiIFFC&4C>cCWUQ7_k~tbl(CE)?Z8A!9JGd$|e=9{ElToch`>PYkG;X^7 zBfWNuveHWOdUJ)-nRYzGRy!23dr-n_U!&;ld@*;;7Tl3_#3U%ummK@0NaU|6@z=QU zUsa)p@*CejJ3a3ymi}jHv#*W0$na9%dtx@*ALQMBjK$&#O&}nG_E%V){XlfT8Ehqk z$h59&Wm6B@iJX)e_h)itXSjJmbj-xZP3Z}w+KI_7mzKiwV<-gQQd@l!1~DSZ<>dI= z%yX7rnhiIiRvGNgXK+%mdwL%o3K%}JrM}V0o|zM2BZcg|KL2_D8fNMF zvwk)av$GEfZ_Q6uXrR-Lqq0$xygw7OVDv+*0IbW^K*B^B+_kop0Bf}|e)q^#MkzUq z4r@8_?wLmCsQt*_I)nFf#(YxR}fB&yzE}^?| zsk}R8l4Q*S_S;QSXplxkqEp7MS4zW%6%m*lBAi)j;KTqpwE)dJO8jmh>lKULUSmC} zxy*VPr=iHac_s(b)vuHpgaQ^At(;2V;bX|`3Z<@6nDVm0@I^#6mI-l3y{&ohWg|7- z&V2N!vNX=gh+rgM+SU3K?IPyELz5)#IerAYTYH7B%@%zv+ToeY#A<} z3{KrVLlO_ zKH6kPjR+Q$kLsy~sE8pv9STG=51nZ$?Sl1Z5tv8H&qx5M!FJOLNhX^WNP1&+#a7} zY$1Evzt2LSlg7f*kK`!!%@m8M7GOp*El~N|1P@z3%-ZS0D(Wxk!vrZzq7;-N24@BY zQ}j{MM&{9AMyeu-YG*LmI=;!v(ogpWQ{xe-gOi?$DjRz2j{y1*F$v`fGiC*owh$-|wbqPgn3>}iHZ5^Y0nVnhm7>gax?2o{x%r`ju1nUI z&YADz`$u`H4s*(cfk;Z|`lFliIN`Hx?4%*j<`go2oeGZYsuApJ0!)T#5M^z7yaiVH zy-n@T2F8cX4-jcIJ?hj^)P;mn6Vp#EboGV9-b4V_P@=l;?GX9-j=LkfI&u}q!NP0C z{&`tC+a8PUL}(Ev)qp#2yFiMTdV$8Tsd1P17s|(I>a%*W{Y^3 zzZnz|_re|=-`&E>INz##UX#G@$nhflM;?-t;2qt9iJ|5W@f zf!)OR)~e>`j)!L7*d(UR8$uTvY%n;E9@X=%cos^~!xYN_(|v)h0nPJ{R)zOB=jeMN zqb4Mea1OjiL@0FVp1DUAy#8WzTOL^`{%5$>Zc;~QKpTDEtK^3qTKXBsQ5XTdh!0LO zgeE-2rv56RJ^5L=Nw2gCyhWajT~H5~@F^+sbWruZ;_kbu&a(stFG8??a@f5)WH6m*MEgKb+Tuf&SeN}@VBKY{~&%{6#Qb-P*q;7Sr zEXfuaHe`6_3*$5Hg&yMC%Om}1M}cJDzS#lrMf%HCgeyHpve;&tI7VP$pNT%`+oc+c zkW9>egOYc?@ZTlup8NID+}Ej-pKr50pPFwXR(mC5qXJZN=DKn<%fLq3A?-9@9; z7Dg}x1+|r{nsHEBJW*Y_AdcEX>5(urb@#q6$ID^F5)1>Of{3TC6bl7IA-bojb|Ar^ zMH7vcNV#@TYMhK+xS%=ehuI~_Fx%waJ3fr%orhp|{(=CTt%&c1j+YiDILUcw;(4fN zMrtWB_LprD9k8HfDjfFA6n3vq3~i2SwtyI{FnC0iJ-_+Wt@e5`G3a4gx$Gw^EP8Jp z$NszM4~O1kPBnb6paZ~5^go8cX<#^qbUtrd@2Y9K_~5ihiZ49{AKIcwqCSrQ+!v?m)w_*S+VqmX)iGNb*?sl; zf5m;}G!_0UeahwjF#;molk|;x1tde?1v9Y0^yZRMs^PNx#$z=cbh9X%9%@GSbBb35 z-p60?ZYQBaG)`*UkMTNHoUf7vv;nB}9$5IE_LnVD_6TF=jg)^cYC3CIAp*<1UU8^TV)O;1!DKQGgHFjb+ikwo!3Z%bs= zw(Z07!7A4@5!es!gE&0DKc_Ac247JlHm_Bfo}91fIyNs9g8w77|Fx&vKYA8-8Su2* zT5r9ie~p41vPdTwmD$Oj1|~9o$8v@*D^@(yQk^S|p{rn2rX|2V6Q?+8yC`~$FKZgg zW4|r>oWrQDZ#21kz;`qsT^-Y1kxN8I)Fg<~IOEMzvG0;-WUt2bt*uoVW+a{xY7s(a zI1#nKbRooA5pU2CcdCD{0Q#p(k8+ZGeAg`x|IwRvRh2C+l;wD3(*mB(ux7Z=pD-oO zD%@$zvM#de*Rq?0a}SpHY{Q$n#5pMb8!k{uE!e*=@K%(iotumu+o!WTZpMO+9$Iv4 z&!er40q{Mn!B(iP5neQG!rrpe=*=o8ALaLg20e@-18!9|0Fr-j7X@l5>bU;&T~0yk zt$vO5eN4#%`SPLVp~std^F}rX-Ut6V_J!*W_>Q;&?p<_!V7cyJ8cma2X(vV@*tv;` zANg}x;paDqU96Bu>43hI-8@Mpn8^sKj=3C)?@(%YEfD#gN@n*)m;|rYk$dhkM76_U zS1id97FPlV`4o%;Gq@FG$a@B?M$dV19wlz|NnnXH;kiT@_<~#xl z4Hp#jK;Q!fTPBkqB=nG!nPhlwy4~HnD9xcI$TnFp^xb4o@X+NYk_&I!aVI`mzgzyU z`LNOP;g6i>+c6FhgJ zB>w5GhG2}pbP&4_2Pe3UUb@6i3dGAqi=ZXoWTztbiPldCZV>_+R$cwg%tzg-!B)OmO6$27>@xYPRE%t+xy|%ewOh z+2@HsVg@W#rj88VB}FOCW5}#|#o>iF5iM>!=vDDF{)6c=Q`L1XM>14!HXs>(zkN5( zF#fYDHUgt#Qyy2U08(H% zV|yVpfj2aWfDgAJ|J8xkfyNmUQ+)cgaFRvGfaW-m7m`;1r#&_kT9ZJFb! z_v{z33VR?wy8NSSK5gpkM&F1*?D|s@ zZN*z<@P6)TKd@2QkPTVt_agdYGCStE2GwoAg7IUVjml2ckfAQ&m tnyvqhSN{WC|9|uKe}HWJ(&QJoEQDCJdi5mnzp-sF83_gP8d0O5{{mwEJ7oX> literal 11795 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 BNqPVP diff --git a/tests/ref/block-consistent-width.png b/tests/ref/block-consistent-width.png index 045603cb800ef6260f520b7bc9ee44093f371877..f181bd335b3231559ef800313c59e6eee46aa452 100644 GIT binary patch delta 866 zcmV-o1D*Vs2eSu|B!AaXOjJex|Nnr1fPa5~_xJhT-{+N=pO=}T?(g&a`~3O&`sU~D zb$5Tj!pgqD$bW%}qou8>tFsmuAlKR9*xKSiLP|eDNl{TzQc_Z3VPQ&2N^x;<%gf6* zH#c*0b8l~NZEbCLcXxq-fq{dIgM)*7e0-mupNor&tgNh~qmyF+6D`}@+s)0*x3{ge}|r+ zsGp&!p`)u}WNe9xlg`lCGBi9oIyw^*6BHB_>+9?I`1n>hJ8(aeSUs^xw*Oh{r$7Ev-$b?m45@$>WZ&(F`()6?|y^rE7o zczAfezP_@uvV??$_4W1e@bK8!*xK6KU0q#NR8*dxo`QmclOF*af0dP$o12@Gl9I;8 z#+jL!prD|Rj*hmrwhs>v00004GM&u;00EatL_t(|+U?k7QyXCvhT*61mI`fYON+Y` zJh($}4esu4!Ly46m*5uovA{ye=bhPonSpcvfotY`ICJKJq9{s%KFMlKIk&=2MkAxj z!FG_*@DRc853Z~zf8Q{cf5ERTEUJ*qf4Rj&g;Z-19 zgHY~p$r=#mi?BV;aHW2I!{qO`85m@u1wydAEO&h}>K%k)^~-92PBx07-mLpKiY8A{ z6n%XZzpKC6+cNMx=)W*q_U{&+SYEQ2tnklYPLkT%(yR)ce~=gnPcLsX08CCT_H0^V zhk!@*w=-k)O{&010^qugr)H+Hz4Z!zW~}hfU&b0$f|CV7a4{lRS73OC&o3?wFEIP> ztR9>qilUcM$jQr}mEipYTozm5e=5SM00xa&*$eY?61=yIs}c^dISqjO;ZgG9K!0r= zc1}-r4A&P{RJglKHCUIP0Z)$u^RYf&TT@-7KZE7=M*9M{cXUcH*9GEG#GY&53}-qz sNp73t53ca|cU#}+>qB23ilRP758}-I)ERzEVgLXD07*qoM6N<$g8n$ea{vGU delta 855 zcmV-d1E~D72bc$tBo50^OjJex|Nnr1fPa5~lMn$Le{*wlH#axS%gb?baY{-`Qc_Y; zQBglZNkBqM*xKUP+2Ix#AgQafrKP1JA|fFnAtxs%A0HnmC@2;d7Hn*6t*x!Vz`$Bs zT0cKOyu7@qsHnfczfMk0k&%&=m6e;Do05`}#>U2(nVF!VppK4?wzjqp4-YyzIx;jo z&d}JBf0LJJX>F{nvF`8lmzklJm!I9==ZcJ!qNJ?%_xbz#{H?CCl$Du_i;JJ1pL~3L zf`Wpco}N@xR9#(N+S=OK*x2y!@b&fegoK3M-QD2e;Ej!qw6wH|iHXO@$MW*>{QUg2 zwY9suyV23nnwpxJn3$lUrJ6`Z*OmHZEbgVcY%R{frE>UkC%vuh+bY^va+(izP@;Pc%q`B^z`)8 z)6>t-&-3&1@$vEH<>lk!$l73e6B82v002XOBKMyF00D?eL_t(|+U?loP6I&{hT#t>?(R-; zDZVhcLvd*F;!@mww|H@v%O$12|4q6>Am_P(H@JKKfr zA;ZHXh5ujJ(P6noyRI@Om0n8c z40m=RedjwIpkO!(z>_I1{^a!RGhA1Xj9m_4T{Hln?w*L_=NDHuH1zd0m^0J9!+H6N zh1D^!@bd1`=~cr97iT9`KQ!4{2B3UsP!Ng=3k0F6vV!XXnI3>u^#!;lhNmVcV4-hq hqghQ?A3_LG)(0tUE3<&}loS8}002ovPDHLkV1h_cz~}$~ diff --git a/tests/ref/cite-footnote.png b/tests/ref/cite-footnote.png index dd2cf8bdb7bcc974a7f0a860e7f745310ff5dd28..e89b1dc123bfb2612189dfce81b1e760c7e5e9a8 100644 GIT binary patch literal 13383 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 uX?$D1Q4?88(r`~F80L`qCvv{u+4=zjq2k!T|T literal 10863 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 diff --git a/tests/ref/cite-group.png b/tests/ref/cite-group.png index 02772f499029d5e93c65e7ac3d62aaf4ca2023ad..d512d07e02f04b19962bb82b11b9b4c8f74244b4 100644 GIT binary patch literal 4806 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 literal 4745 zcmV;45_av0P)4T1ggVY>L$&Lb8^oOq9}$?$kkvAcBa5mWD4S?v!C;`* z>ldK`bo(KtfUfL*h2ypBTdC(S=bJ zM;<*&1MOLNT<^Xt$dsdg+jC0q$ny72rL5)ceLlmo%Wsp zO9k}t)iIe&R4NrhpCrq&$KyGk$K$bDtp+0n^xgXp?vY^_x?QneuLCr3VZB^!eB{c} z$Is@WDT?y@{V;U9U9DD&&?hAl37`>05#~~<yEAC`kw75e^?F&B&E;~#;Sixu&uBC*E(Jm0d46%vaU4M7 z|LC(sl4L5CTJnV@91in5kI)!GBQ&-m=okN5@{VR0rdF#V^io-^)_%V~JL+`0(daKb z=JTRN7>DtHL=Z&9yNH*59E1|Y2-;n&DJho4VvE65*-PdjJjJR=o2aP5k~PAi^U{MrBi3liIO38ZK=`x zykNVYd(H}hWkxEM3fJLqsL^N$XhDHMfa5rm$s}1SJy=^8fo8W};&#P-Hxf-rg4XGD zXjyTbJsuAM{YQhrz~yqG>p*C3x7#vHCDa|4sYRS?NzhuYHl0qp-EOEzd=(MYe?yN&foQWCU6q2S9Yv>&KG2S}IW#l}IF}pbrm! zlwa@OyhShHUa9RhTCI;Zo2~cd+oicH^9xt!=Bdrn_Hoc~9q6M&p%A7=KA$Hu`XpH_ z7RgenR4N_c#}gu@(m#U6F^|XNG6Rdx=PMKn1T+x=O+XXQ< zY!c9Nzkah3&R{U~dOg@XxaQ8MuRGN`7Dq>8{Kb7g==M0~S>aW=icL82WHKqTcQ7r; zDUnE|QmGKof^Z(KR%xAar}5ow)|aT%xw<-pAXBm^Lo=g5jYX^2R>X}84AM>=FH=DTMNkF|c`Fuq8+B|D zv}=L-0uyxTYHSf_Vya<3d%N4aoa5m<)R>raU3p@T_oL^-|GCe7uIqpOuj`75LE*Sy z!2*s8m!uMONARjp=#Hn6N<`jh6VNZfYYWhi3>uoSyP%)|N)y$R8=xWnA(hU5d~rxp ziF3nGq|{@5a9$xA#3R`uqROu?x_fBDTUPv7HZP9Dmvw&7HZ3 zkB{ev6rcx^latr4U$04}Wy_Wkc@H9$)`tEjY~4mkrHdCY1qKE}D*fzLe@La}zX&QV zJ7Y^KkwYMv0I8Iln=5CulLZ6>SQk0L%gf8H>n$xULMjatXmYjgNu?b-b`%yC3eaK# zv;ZykoPg&2MgSfG=Z zy%tpe*Jj0O*t&PimoJAzC9O_GPKYl+40nB&Kqvfu|K5Z@ zD4^N?0R6_+w?P@WIYUgK2St()Bdx8iT-4UqLYaCRX!bJVBTXv7-dRZ{_@mV!p^!># zZS5;otbkOSJ8vpgPw2e)kJelmDm2#qi7#C1?P|vE`5cZB0z@=>V z$kF`x#C=qR!}GBS<;0&gH8mqfjL6H&gXV?<5ul$KHC43hA#G>{nk+3B^tU6=@`IPA zn?UdXPyX{umo!$~2( z0R32KKw}Lqs;jFvZ{F+^&~LpH2g(=zdC3IY)JLOM<#7?92N1r5hlgve^!V}P=|f`+ zG~)B`jhVm;5TFs`_V(UPKbRfy`kVQuPE*oi0{!5@gHfYKq4$8!rd4S}LxTW40AY_% zT&J=qr9%2|1sWWWX6NqPpL+6C5&13hEFXMORZ;mND}0XUo@9e-pgA$P4+&X>(&QoK zj2_wmnjASw1Zx!{+jh_*j{>h#30Q_>bb@AslV%+l%I@8}S5#C)7b^i;On?@k#hyLT zwjxAW3%qQ+5jSTvq!OYh0eT3VJ$p8te5_svYA!C6su6O-3|JsQa`TFj1m)r6J7#d$$?b*ZCvvB2~$jd$df+J4@O`y!PPlG|9NF^#~6@5By!A~84rghDdLP7 zGXTO@Qi-DQGu4gGKrdaow7I#N9#`wut@AF>lxuf&brIL8x_h61<~(7#6Eui3&{{Hy zESTOPUIZHIhfWV%sNpHFZk!>uK%<5pJ8qH!nnLAUw{B-;pWOQ9E`Z*=CE5fUUetW) zH8wVKbf_4e3rhRF2sDyW4rom((Vi4$&X!cl$jpw}{kz-~ry!LWY*SHDSy*&>cWfM= zl+=SJsdVGU4g5UqbxDc4)jHUw?F1Y|bgwX{Ffq>QPKwF~&7KY3Y z3Rnt@E8pMur;q-2Uir}!lv_z9Ts?-3Kq@i3lifRKc2J5IRvMe?(*f! z#P{@*@Gj7t7*v6y;G*$sM@NURq!K+27%}7wH0AspL8x;mr1dV)Y-jS^jP*s}0W0kj z(9FW&nC8MIsRR>CzX~q`jh2}Vi0&srDunV_TcA0nHSy00@a30x&?q4Z`$x;C+oNLv z+T=Rg+S*LlMYWfu!wp*-NsSkQM%xHYtVt!ZO(Xy}iBSN86K1rY;S#0zL7EGZfHpMtAStZjws+nWs z*9Ieqc2a=;#*_pasF_pgSFT*C1WNGwO!j4nuO4`#jla@hkYl&I>83<$ zK{!09L3k?x`mrI_qQfOLo^@7MWmP>*z-VD$3$z-96SeIfgwm!ej1goD7cNwFUF>lI z`myoILqpG+HH*_=$PrH3bd|Dyz-sDh2LU~4(j-dfID*>S+Xd*y#5|*XVUXE_wEuDd zx~uDI-2aWF6x=_HWDYlM-17FWcWrkxwf57ePe(^8Ko3w_qOvM*{ETVybw~3lE~#1( z7-9=F77`LdKM=12O*0>6WhqTG%H`3cM+3JB^e1hdYk$2lY<&c8MJv#p8^{g#mban_ zG?{_1W5-fIz#~HI2z(Eo7&0&31sZ*=cEWHHP?+O;=n0))ef6NH=Xz~zJ)fI5KexJf zd|O^#dG%_Ka{lyxBv-(#@bL6tPHs*vQT*t^#;dHZu1@WAmqvdGdJNJiLIM^ajP@lY zQc-ygxHdK{5_G!C3eaK#v;ZykjDe?&;*J~h|OmAG^xa< zM(XLl=-xC|=p+%mDimqD6==;dF)D?Om9ZmRyKA4HvTzC=an3xz9m=KH8=3sa`TK$8iuLjiCoqHP4x>$za z#&bYJ^n`_lscXp0F=NIc`nLrdI`li=9oy2{ZUwsWqh_`y_X26A3KQtk(o*hBu@DM&-BAy?C1801G~GOWMTCamJS6}v0gGFMA>!cnRi=**hs!MU z48;U!0b1;t0d0Gv;wo(4zI_;^5~aw*LDm!u?^Q4MRMJKkoDi9`p4ICh^{4#k)(fBQ z-jiq>uw4K5Eyh=nEYCUqXZ0;{rq$~p>|_=#yb1+zgp^(cnjVYvAdQWUbs&}Kyk^dv zHLarZY{rojj5@TEO4`g>v;?A4r%v_g%vtyc8VnAbRD$b3Id<^yF=wEO5BX7PGf0Db z&jAe|uV&6N9)#m~c%;&~rV9sBGG@-6f9?9`&O4f8n$Bai=R|$#Eui=8*+T+biD@Du zBfll7bhf6^0cbsomXYD8Og#iN+iKIMOv{+v(})!^6XdhKAD9*;ra) z-rwcL#l_*_;eXoN+K-Qq+}zx=v$NFH)L~<5PEc61wY$#F&dbf!NJvPaprAxXPPe(i z?(XpK@9~h3kk8TCFfux-th8ceY@nj578oFljg|25@b~xkm6es8oSaHZO8WZxot>SR zmzSKLrt0eIgM))*XK(QE@L6jhm(ie8VWz^bot`- zjtPfjyQs24^M%2&ce19WvoC;`jG76tHq)$ delta 818 zcmV-21I_&92FV7HB!8+`}_R*`ug|x`SER z@9**M@AK~N@a^vM>FVz4>gwp}?dj?1=;-L?=k4L);ojfn-QVZj-saof;@aHg+}zyR z+vD5Y+t}LT+S=OK*xT0E+t=6E)z;e7)YQ_`+0W70&(F`y&VSa<&d$rt)y>V#$;;Eo z%F@Tk%*4jd#l^+K#LUCP!@$DIzro4Azr(z~#<#h_xw*NvxWKiyzqhxyw6(jjv%9mi zv#+nOs;soDtgxx8v#6@Fr>Cc-rmCNzsi2^sou8+jo}`?frk$OgoSdAQo1>eXo0plP znVFfEn4p-Ln17X*pO%-KmzS56mY$ZDmXwv5m6er|lbeu|nvjr?kB^Uyj*^Rwm5Pj% zi;IhiiHU}Ykc5VghK7cNg^hxPhl7KIfP#vDfr)l_fOmL&b$5SucYAbod2w@ladLKU zaCmBLbZcyIX=`(6X>Da^Z)0U`VPk7pT4GmOVO3XOR)1DjP*Pe?QCUt{=5Iy^)&G(0deIu;lpKJw%&00046NklcZ8jeRej(gNJKjrLtT@cL7Qzc3dMrJ^MFM{DVH^pC^*GiP z7Pd}uVe^aKD}?b`hiJZ$E*#fP#ULKz_i*M~6@M4Tt*3Fegbo;_567EKCL@EE&9HFa zzVEFCU}#47T9`v8k|YiKvob!Mof7)v+AFpo*4>L2>B6etix4djl7yMO-MxGj zP^yG&LR9prdi5);Ci4fmZO-J0?s7G7)SV|&Bys(yju9OWC?G=A9T*K<*af`GH28rc w04w4wk47F4mjif)jlT}a@REFdWwL+i6P{~FK%j2VrvLx|07*qoM6N<$f&*K|2mk;8 diff --git a/tests/ref/figure-basic.png b/tests/ref/figure-basic.png index 69388755f537155b7bbeef92c17046453f3eeb79..eed77cc3ab5f12182e8d569721b74665e9d667bb 100644 GIT binary patch literal 7850 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 O0000C(~w diff --git a/tests/ref/footnote-block-at-end.png b/tests/ref/footnote-block-at-end.png index 09880aaba44519f4a2cfff67b3e29ad71e9a7efe..15d0bb59325836de2e82028f619f8fa857fb2948 100644 GIT binary patch delta 598 zcmV-c0;&D!1cL>TBqJ|SOjJex|No1Ni}UmI{{H^!>+Adb`&Cs{>gwu3LPEH>xRF5^ ze@IA3G&D5s?(W;$+j)6;Zf{pEWMpK}(9pcRyorg4q@<*^wY6wyXvW6IX=!Pb zlaulB@jgC2Ha0fF!NJwl)s~i)+S=NFe|~;0E-v`^_~qs0b#--ZZEeWN$kWx`Yi)IU ze1y-@*@cIXtgg0%hK`SsnYFjSuCTa;g@vi9sn^%nCMG7>+T!Tw=$Dt5>FMdt(Aa8h zbnNWx`uh4$Pfu1>R{Z?@tgNir+vD@|_WS$%_xJhc=k4zA^WER)0001Wxc3SGe*ggw zNkl41m9i+Qkq%J_+-Q8WNx74uN<^HdMkZ?>wLgq)l=V;E}WOo0V zWI#mJ;JSklv9P%X5s~d3Nbs%i?H)ovWMlu}2#VU#Sd4WKwSs;ay!q#Lab*e}6T@4A+M1y1Xfgi0V>72zQ={h=_=Ys4DFhp~e(9 zTu@ePh;YT8k8dA?9p!a=aw2Rle3wL^ydDiXwz=^A1OC!yr^-ryTA@xG<8$-IzVYw( z+G=8??V}WS2C=wgDjdLUES>;L;l2T^EE@{HzrM^&PmRTf`)#YIH~ReKC^tGz>IX^z kz<-m^aW0?d@lDi7iZe`E7A}veg8%>k07*qoM6N<$f&fuddjJ3c delta 592 zcmV-W0+9<3>bSVLtgNhHU|{w2_4fAm zo}Qj?aBz){jeLB3l$4a|>FJl3mw9=4G&D5s?(W;$+tk$5ySux$x3`9dhPt}Ce}8|N zn3$!drAkUlZf-va+(DpP#C#s)vV%zP`S-wSTpwq@;Cqb>-#dlarHa zX=(BC@jgC2XlQ80#>Umv)xp8RHa0evmX_Mu+J1h1E-o(k`1oyYZOF*T-rwVJadm2J zblKbE>g(^=+Tv?%b<@?|yu7@LiHT%nWYEyi!o$aokCUvfwuFX`kCB;re1xvBxS*h* zg@uKwsj2Mj?0-*BPgYh|*Vor3CML_v%g@o-=;-K3NJ#qn`uzO-LPA1SRaN`@`|j`a z_xJhT-{$}T0C*v>(f|Me_(?=TRCwC$)WwbhQ4odUGjpNo9%Ni#7K;i%=0=UV(VL-OHWWnH*0**b&x-(iyE%w! zj|FhB|IFJaBC2DDN2gG6a(o6A_WS}Wex6cLVXb9YhVT3QDMUon+zLv#c_JbrA|j%y zv{r-~Gh8?c^6zegnerST>W@H5{yv3lc^(LRu3Y%x5g(!X1>=M|oN$|qiODIMu(f!9 zo1IGxwn=oB!rmyZZ=}Lm%p~IppcL*M!PTWu`1k8`dVFj++1KSd-953_mmDOSe;5M* eX`VU})srGA4NMarQ{y-Q0000E-w4~`=FqpZEbBoZh$meX>!HpOoN$hy4f2qH+0GBAUHvI&+$!$oZQ^pwY9aj zwzk5;!g0gn2oDi;$?K@9vZSW2sjIWHw7ig#nr5xhK4^eoqkqL!k+5Z}&SaG4`1tsimX>~ge%jjF zHa0dsK0YTWCx74H-`?KdiHV6yN=j5zROaU9=I8C*-{gwu~laulB@xj5t)z#I; z#>Qx9XpN1H>+9=$e0-FYl=b!XaBy&LaCpYZ&~LorHh)@gG*@ddQeYrAKwF-;jgObP zyTi%L)2^_%@9*#W`ugnb?CtIC&(F`Nr>Cf>sMpum{9dxf0004WNkl#+=>CfWu+xT5QK$Be_=0;@$$BwGF^5=YmQ#WnUl$+qaDwwDnBw9^CVw` WMRaF6&GO{{0000+9={jg9f~@n~ph z_xJg~z{LCe{Pp$s5F8-u?C?H5KI!S{J3Bj2ilOG_=2TQv-{0Tf-rmd0%V}w8#>U2p ziHS-|N}8LWX06e5b$Qj++Hbt#Hd=2qS8G0KfMBD=Wo2c}&410Co13kzt!%p4o1CDS znVp=TriqJ_Vq|Q;!O32r!AN_RRgte~u+((P>Se3WRgthobdF4coz>OV!NI{cHa3=) zmU6}BOoN#qI6+&UxEn7wKW>0DT4{WId~k4Zl$4b9_4Rko@LQd?X|~j0rNL{s*8>Iz zc+c@5H$ZN^-G5`N$bNo)+S=OS;Ns5C(5|q!jgOZwQeaJnoZQ^pb;;|swY8|KvZSW2 zvb4OZtFx}Iu7!n#ii(PFZ*K?>5ul);ZEbCF!{frj!l$RFwzjsAkdR17NYBsD`1tty z`}-~~F3`}>CnqO(czEyc??XdFjEszOa&oGws_g9S?SJj<_V)HtQc{|ln(FH6larI5 zpP$*;*~rMq=;-MB`uf<|*l}@j{{H@`sHoT1*E!v{jsO4wRY^oaRCwC$)>Tu2K^TVN zXFvQT#P05H?C$RF?!dwZq*H&iGaPx~aKRl;>~PTMAgkT7^?QCtb zqGGv4*Tg~uzhTjAeu2p)a$ARvZ#X_>V36gPmbG^_^FDFwP_-PuJIsdr1%#L8W~N5^ z1cVo7rzaVU3g8-MKTw#$6s9nRDa<`gUur@KiGNgTyY<}&{yY{NJZa$1aRVR~{GA>_ zjFV%C;WU^4mmmB+KHD8m8aE*%l6(e*(D@3AF6J+ZU0d~-%-~PjSbur7iU{xT!RHkb zK0FW?(Xg=D1Rs-A|l({+tbzEj*ysj zcYk_(gw)pFK|w*Msj=$o?;s!`nw+G`%hOj`VYIZg$H&Kui;JM5s_X3VzQD-0xxvK7 z&aSYyaBy&Ze0<8x)KXMjqou8ZgNwt(%|1Rp)YjcLHa15`M^#l-Z*OnV(9p@r$%%=H zYHDiH(a|+EHGj3YznPn(l$M^ewY`>>mfGCp?d|R5<>kuD(Pd?2Y;0^?US`qL+uYvf zhKP_~VQHeItk>D$c6flx%+jo`wu6O@!oE=EB0l%gf7YX=zAENE#X%S65e}qN2LGy1>A|wY9Z}hlhiMgL89pnwpx<&d$`- z)QpUbprD{lO-))_TAiJpc6N3qCMM_S=VoSRfq{W*Yip#Wq+wxUySuwMI5?Y|o0XN7 z`T6;)tADG2fPl!z$lcxD;Nak%o}Tpd^uE5nZEbCtnVF7`j-Q{Ot*x!=>+7bbrjwJC z+S=Oc>gxFT`0VWLoSdBFt$*kArEhuRWbR4w|DAomXLje#%svc+ z5F*IEbb;de3nwThq^tzPP(jh^5z2t`3wK!<;$*ng8|b#IOrxO|CFCsUVm@XG%Y%gUuJSoSR;gb;jx{6gS( z{Xz(f)HK@vA}P3h0V%j*A=1+*!PUt4%6|x+KM(lEnOwx+=_#Dx)VzF3Fm2_M9W$^@ z**|)~P#5Ru1BEd;78G;hIKeRm7Bd)JnVGRtu@Kka4+_V#W)B$LPaEB!Xt{M89G!h& zc=WKV9~2!@Y#~H&7)K&+UI-zC5JCtcgb*UcjrxVa@%bemCN#u@!?y+;O>05%wv&Ft|Qud!%4WjgO2Bx7$Cb&qRBoqdaaXmU!xH!{&9q zG#3=pPM*M~n)RtrZEp2;VB-eTgEm3H_ml{65Ra@7$F4ve)gG@W)XsEJ*FM@Ak W7Sbbe55#}~0000gwwC_4V}h^!)t% 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 diff --git a/tests/ref/footnote-break-across-pages-nested.png b/tests/ref/footnote-break-across-pages-nested.png index f87658ce8697ddd422869029b0f687191bc62108..79b09cfb9ff5de59e4081de721311ebe15255b0a 100644 GIT binary patch delta 1315 zcmV+;1>E|h3cd=EEPvhK=kD+Gu&}Vt&(HVw`RnWJC@3iX{r#n-rSkP@z`nk|ySuxrtgP|z@r#R#&CSi$*49u^P>qd^aBy&wlas%{zqYov zX=!OVI5_6#?PzFd$;ruETU&g5e66jml$4bB_xGfxuCB1S`+xiVU0q!uARzPe_GV^g z>+JBZuC7K#M&{<`A|fK<;^MTlw49urx3{-TOG}cHlFZD^NJvP(!O6qJ!-|TEhlhtu zOibV3-`UyOgoK1XK0eIO*2Bfk%gxo()6>ez%F@)_czS}6kdTj$kA;Virl_!~tFx-A zs&jL5N=i!U>3`|5v%7qLhTGlcLPSi6h=|3<&+P5-9v&WFUtgG*nAX_f*xKS%S6{ii z!^q0gy1c~X<>}el-i(fxjgOa?nW4_m*xujcUS3|s#l<8fBrq^A#>U3g)z!hl!8SHF z+S=NFetxH^v2%5Oy1Kf1dwZRooz>Uhyu7@qsHoD?(tps<&^b9dE-o(k`1l$c8dq0W zmX?<7?d?1~JjBGrVPRo`fq~J{(QIsNSy@?tfPk5qnQw1z`T6-}Wo7Q}?oLild3kxB zo}Q+rrYR{YprD|utE;uOwcXv_udlDTxVSMfG1S!5adC0j*w{coKw4T_TwGk-+}wqQ zg`%ROgMWjAFE20l_V$d7jFpv@pP!%J-rnTovxmg1c(UlYr5wQ`AAiEc1D`uzcp2*X#R2#3+Mrxk zh5>=5&)hcA@cAaUn)@JO68akfMSSSY+E7fsC);kE1VtV$xB zlM7VKTwH?T>A^DLfXE;Z+Ob}z;~mqr20H5oAfazCb1G!K?}v!%rFKZTdhLc45~97l zCURor5}}Dn{*Vw7Jk|mc*7SDBSl z#gQBi5$zR~92gOi6F-IUlP4k~A|fIpA|fIpA|e{5A*bLv4yX9V&@kBM1Q{S@bu|Oijb_qeq}z zTw8}js)-PIxzj)?5E2!w@qoPUxs24eI&ou|F(owM0e`4#Hz+_5G5U9rT8SLWc0 z#EHk{&Xx)5G)*pi>a5WVARsyhqtYN?@CUy4Fswp()uxR`2q+B* z3FzVDqgxvv85Mi8ByT=cYkPsBTZVZ;JMRAGN#Q3#VMoR7J7(*rUZ`dVU>vt|-9+@y Z`vp*jBF^;uHhcg8002ovPDHLkV1imZ)cODb delta 1288 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 literal 5473 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 Y%`*Qoxv$Z{t5c$f8hYv#_n(IT7n4q*5dZ)H diff --git a/tests/ref/footnote-duplicate.png b/tests/ref/footnote-duplicate.png index b5a73f74b8dd39cce61113ea61ca3f22144a1ea0..e95228e48cea321b96de52c872c2d2eeaa010440 100644 GIT binary patch delta 7121 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 diff --git a/tests/ref/footnote-entry.png b/tests/ref/footnote-entry.png index dd09acb92e86b61c689976c95e8263beb4f778cc..9e0108f883cda4e11ab6a4448556f51eec265919 100644 GIT binary patch delta 1816 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 U@jJ2cy#N3J07*qoM6N<$f{#`dI{*Lx diff --git a/tests/ref/footnote-in-caption.png b/tests/ref/footnote-in-caption.png index 79b2b5d0f955479b46cdab66ffcfb4f936beb893..cd4f837bb49d21aa9708ec6661488829ef04070f 100644 GIT binary patch literal 6044 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 diff --git a/tests/ref/footnote-in-columns.png b/tests/ref/footnote-in-columns.png index 281ec8836b441f0e3ee4dd266dd49e70f773cfae..8b5f1201dde28b2c9bce100837d1d18084fef55e 100644 GIT binary patch delta 1263 zcmVMc6N6A`~36s_F7t6(9qDz%F4XFysoaUjg5^nGc(uM*X!%+R8&+$ zLqo~Q$?xy)kdTmqf`UXuL|0c=j*gDy<>h>QeEa+RY;0^(Q-4#ImX=^(U^h26r>Cce zhK6%l$4aRw7j38 zslvj-ot>S?$jHph%%rBSkCB;_mY#2KZ)9X-y}!rL(b>4V!d+cm)79P3(%NBTYqq$+ zsH(DxjFenlTz^49K^+|(8X6jhhlib?r+a&QrlzKbh>)qPv#zkXn3|%Cjg`j8(0Y7? zNl8h8fq`;#e8k4ib$5T7oTQ6@FIW@ct|baW695Mp9t=;-Lh#l^wF!O_vtPk&EOxw*OE;NV6^MuUTctgNiI zwzh6=ZiZ+=$ zd3kw~l7EsYC@A9M;?>pFq@<*roSgCT@%Hxi@bK^g0s^V2srvf*?Ck8QsHm^6ug}lV z{{H@mh=}+1_gq-t?*IS-kV!;ARCwC$*XL6cK^(^MXYW!W5UCLhprBZ23JMmmfCYQ+ zz4zWbsMtG7vCu(E;6eiN{yT?PPUhZtyZXDyuz#QT-+AWd?sjG&Ns>gzu7`nG9RmOm zySp1yy!rXseec@KS7LGJHg;_Tm1J~iHE#k{+E*2F-zkcJ^ z5Z$n=r1Yv*JNm@cO`V#HZaAr=)S=xQ9e&i%)&>%-T0gL)B+Xyn9?Y8;uD+S!bAW(t zv41Zs8%uq;2xK_2e5X&H$xM=`j3h}Cnb%>8#%S#pbUlnAyUx?dgNTaT?S>^3MTyGcw}H?|n5)^ec_Re8 z`=N)Xay3;IMy|8cAiFX_;Y<@ zYD))CBRJev_sP!JD2gIaF7p!H?gW^msw#vrMPVRf{cvmGu7{mGK|Y-4^w*qY$Bt7E zH@5&d%c_o24_`PB0Bq->c+m2;Tl8@i90uT-OFxVwIm-)EE%}}7%hJI)EKe@;RDZ@) zc{h^&7>N)Bnc)Zz<(rWH9a~HaED;E<%!Va~SHT+omIbiJoB~+kL4Tk8!=(Sk32VeO z!4jU_EwIG=!mY`$g2&O|hp1l>lJHl0x@sXxk6^>tp)$tVh~F0}8Y5P@8fL}sFNZL; zcONECGT9tFFxrS=dln%)5vIg%h-hf|uyqT8M3c>WL&N!TC2}?5!@=>rJdOD9gtQ)> zCQAR~ajMGIL?L~UqA`l;>~aw_Mh<((NzoX^O|ua+Mz+l94uZyL=8POCPa`iik|d24 ZzX2_&&P-=D;S&G=002ovPDHLkV1lHeu@L|O delta 1228 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 M07*qoM6N<$f_gK$u>b%7 delta 2468 zcmX|?dpy(o1IFER*4PmtBb7sPkL5BEn@i+Q3|Ue%BFU26zL-%-NXRwgH!MV%FipQK zEw_BV4CNY%?MN4c37;LrCG=8Sa=4Zn{zT_N>< zVNu-s`ufOZ^2*A}Nqjg@UFp%l-c-Y)J}D`wwJ4$S7zk(>#UFyWyDjqW1)tH?C8>NZV=|d0_X(6NkilTs+1XJj6om}6Q>TEasNv&=mP%x^SXh9R_VPWo z^FTY|R1BBBIM&=X!gcq(5Mfd#np)YeWNBLRK7Q+ahMVHK@yAiyC(eFWyqR?4HF&kz z^zdO4jkehc9y@jnLJSyt5ytWe> z^P)S`j6Jk*@48_Z%=X6fRZRnZJM(~nM7PdP#Q)B)2@1vEMqv|~WnzhY5 zn46#P-t<7b?LkqMKus2?>Gia@sHljCG%>m5b-9)vxxP*8@%Pi?Zmlm-ga8%?KRrLQ zU(++iJfn^|`J*v+?gT~->KJAcqT#BkB9o_jCeP*7DX!}Vc2Aj{@Z$Q`UI=7#Yz%!N z+n#*XxwIIts&8w9gSDPYM&XkWyDm|eaUmh)^(@yJSVOr#KU`9L6&ED>rh-Ijveu!U z0V^xBOpZ+pKT*btTtH<)JMLrHSWbg-vjnktAYaGNO(Pc}FE4*PJ-rd5h39RJ|2Si} z_4DVjxsZ3#V~-hTe0)52Rn~HWTiLWQi`@wzeC;TUH&-4?V2SSNBc?XxBb|K0gthB< zR9HFWe6;%=EZ|FzS<5HUu}e?R)+|H&W=M8flcS-*!M_m!Vxi+-VDr@p4Gj$;M9x`e zsafnkX=%=J)e!AtGLTIvm?r685qvFz83GERco{GYTK5XEoo51MEmZia>wqAC7awM^ zeCk?$>yaN&(R95$LvI~0i<4XvbZ%f70q@T^gS!wG8A>=(d|LAd7jnQMwnyFY+^ zIQ^tjARSdPz{b-i+HRtA4pW)4ADgat+1fJSX27TDv3p#my_B%^nUV^Fb8jWnW;@g6 z%#{s4b|MwPM4N<*AXmXa(c7|ZVvrn|JOBLp*Cm!R`kA&zGqv61cD?5Gd(rigW7|a!Df1KQ^g~@bOV- zGWF2dk9c-pY}ZKOa+n+aXM&fPk;q3>r1ZBj9ys?|)-ub4I%Iea;nx9#UAe;O^Y#=y zgB~Q@pO;m5Y#tvI7k5r4ADwd{kR8>Q^Wed!4Q>17wS^H=MM-3=1>hcys~@o)#)BU? z_$NbuPEKNR0ZI}${5oHCn z6%w$62aTg2z3neGt0-z*7_JjWFp^c}p$AV!XC+9bcP0k;CNF(>Mo7p${m1Tqx>b31 zw>?c>BiB9=0tJ-X8BeE{($t0r{-Fo|;49_nkg~(2dvV1`fnCcJ#RPY~-^_$5LB*8K z<8H&Jh0rhwi2-*p)0^M4bakg_H#FnJ49$;uUBPt*Q<6=Qt*hL(<>Tv{X4~Sx z5$&RUjgaJ3HK^m7$4auYT>SiYyxE^WUkb?!3Je^WEQs65aAzJNW!PN2bmK!|d$iNM!fYdz)c=^hpCf>(~6b`c*cC z!r^49cm%Slzv5 z4-Ytx(Yig(s{5v-VB4a!nGwoH6~J$_)1*Id8Hlh&(SrtX4u^v*(cuPHR#oL|`+i?B zc@t{54!idu=BxS}IpK-%aR*0ob#=81CQrxDetf&7vNDK~DuHIqvZJ;3@h4PHMuytl zoYwK6q@*M-?`>uqjmDP3Qu8o%NL<(9?9*%mUEN(=)~WupRyk(khk;*9{5R_PtmN~B zhXLygM8U^a46Q^P0STN4_$m^YdD*5kvU7XTUHmz*6n_gJVGHWjW_gO)j6t2(ofsI#raw@pn=o;6L4jnai3=uBkG zc8441fJt3NbLdA@?qPNBK3oMSIF_mTYKk|N@xjK%qU(kJetzb6s43 O|JhkPSv^JhC;tzWKCl%4 diff --git a/tests/ref/footnote-in-place.png b/tests/ref/footnote-in-place.png index b500ac80f47b0d90ad779dd813601a5f5ad2cf3b..2f9681e231e740189037a146baf5275a4c3fac8e 100644 GIT binary patch delta 1091 zcmV-J1ibs!2U38v$IG@NREzif*x1-lpQXlTK~!9qep)z#HsUtitb-8MEhK0ZElbaa`Sne6QB(b3Ty930Bb)HpaeJv}{P zVPXFM{$yli#l^*Ydwb^Q=IQC_!^6Yg-rlaRuD!jzfPjF0fQXQinyIU^jE)R;pJ*+9QJ zBd`Df0)Hb(L_t(|+U?e5SKCk=$MGK&sgpgT~Bne!|vTU~Shm7GeCm8B{mnmGltP2duZJENJ zmaSlD_pRGVOI_c(#n0n#c}g~h2RVc-eL(yD&ws{;v%jl&^B_S3G7rL{DAJdb5JH&A zozNf``pHr?$kHfdwWqE=l9yL6K#} zWZ}z~^bXtq#LLSZVF%L7p9){OItHEfC-BjgL|TPK1ykY9hYEByuxQPvR6K>DXk9~n zBY!GNQ-6nj82aX(To|Cl2|Rt&_E`6DQ{w=H(8wV6!nGL7JrG{MdhhGr1prt8tjq(H zdv_T;F_MUd{44DH;H*M@IbtD1!hxNWVRomwW;+03X?1h)emH)it_DI7i3oN8fUN*H zdW1>XUb{_2p0~MWN&LV;bp4F3&i8td{C`qW2>{X9FpeK%dXJ0zXg|bbKvsE2-yWvP zBxJ72o)*|qr-gY;QWWsoJx<{}9RL<`2w%Ga*!CD-R<7R!N>7|LKHTvV=)ZV@%i-$o zdDi!wLwIddAx2)6+Ti9*Fg)6m zb7#@}!`I5?9rp$*H*g6@f~czI5Vi`xM-$wx6OSk~p|;6JP>GvE5tb?g8D002ov JPDHLkV1i!{EQnwpv{EG#@c zJa2DrmX?;^-{05Q*U-?=+S=NKgM-P*$+@|?tgNhletvj(c)Gf}Y;0_CadFbp(sOfj zVq#)WPEO+D;+UA2@$vCXOG}=fp4iyfaBy&?rKM9-Q+#}Uhku8MwY9bT`}@Jc!9qep z)z#Jb`1q!#rZzSFMdi!^6kP(Za;cnw+GQm7Unw+ODv;qou8^tghMF z*}c8JK|w))fPa9Nmz{!xiLkP|sjIV$j+SwAdwY9(#l^*BWMu2?@JLEh+1umV+~ofL z{$XKZJv}|`?(*#I@myYJh>DUxLQ3G_mVN`awfW{r&xgg@voDt9N&Iq@<*Fc6Po`T6;$ zr>D=)&tqd_jg5`m+}z{i+7KK zxyk?l0)GujL_t(|+U?hMPg`LWfbj<@QcBCJSdk6c#*~e@ySux)Fn49baQBwBl=fcw zC6^HXSyD)v+6Q;k`n9ecv0=ky1Zh*k8Q{9X)yU znz&+bYFB&EkD@5V0bhFxZKqpGT`6d{BVv)}kc0>;r! zN-O~>T82r0ge5Ft2}@YQ5|*%pB`jeH|EJ;fi3FshowQg2(&VfcdU$3+V_DyF`v*df z5b=#LNqzd@J{s#Z^D>Z4TtoSJQ{je(DjMth_2E@GH%yVOy|t}{?w;_xLN7&&asWt& z-hUw6yHWpG^Kes37ZzthF?R|4;ENy2s?D3c2Xe1IvAAk8C>aLIk%Y4JT_sXIa;%}8YBlHuSk$lX* zWbEWIAOpjxCyqnr9^=dE(Ie1v`mFKc!-t^r)EVQ$ib8qwmYQ`*2)}C@qF`+%#u^(O ze47)%onw*-7aDtU?!0Mkb88r*sTVF%>e{{oqg}sziLwRW+N5l*GIz;}q=fA*Dkds1 nSTqtbS(Xl)Wz2*S;!nQ;ry>9lrafK*00000NkvXXu0mjfw?0#P diff --git a/tests/ref/footnote-in-table.png b/tests/ref/footnote-in-table.png index e110eac6d4cbe9fb78c999cecc4b801cbe2e58ee..2c7e423095c7426aaf7cb81f145adf28f3ffb2de 100644 GIT binary patch literal 12817 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 literal 12727 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¥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 delta 1059 zcmV+;1l;?}2)GE4B!AIROjJex|Nj600RH~|C@3i1-{U3g)z!np!{XxNzrVlv`T1H}TE4!%udlD4pP%#d_8uM{>gwv|=k49y-CSH;o12?B zI5m6calSAc+k+<)BM+S=NFets@4F0HMtmX?XlSUYsE&?~va+&#e0-FYl=b!XaBy&+9?L`}_U<{nyvmr>Cd4PftPs00JUOL_t(|+U?utQW{|x#qpEng~bYj8buR( zdhfmW-hX@VJ(?a%5^Dqzlp@I0<_`Q4c+be3-wiwiJO7>iav&nI$Sqq6iqvu+C}zHW z2gBUouRnf)V5X{i5pU{wzoJo#`N7=EVD zoehG(103vtea>4Tvjz4Q2KxI#id&>8Zr-_@X@7xzM{nH*BCxMJ4qzqh%M61f`WYPS zdMbf{1N(*sef719TcijIJGQq6K#*@l&X*v4SL5k3(&6ZH(&4FL*mn<@epLbM()UM_ z@BFG;qzEPt9cb?WLGCczC>T6A;ew&@)X56cFRpaK@B&Cpz`o?mc>kxM>J||Z5fKp) z5q}X85s^|@LZW|OM2e}GG?+Yc-f#w!=7Ql2Cih;v#2HK)V>p9JV+?07X^i0vCcR#- zUMV6XA|fIpBG%0>2&Ge4kRD8V$%CTd@)ckc?DJl`iYgD|@Qv$09pkVu#KR9@Uqe$9 z<{v&%+)_Ml&DtCoHdm)7!Lcy~imEUu{C}&R8Zg8XUuKnvfWp_(Iu4FCc~De_s})$;bHGp=+1(6=n$Rv# zG_SAogCm^;!_MQU&w#->=Bn$mSCI!rY=r}c&etv|KJ+~ffMKxrNgEg@K4ynN5gc?B dwumeS{{XfEe-zC(A87yp002ovPDHLkV1l`(C9MDe diff --git a/tests/ref/footnote-multiple-in-one-line.png b/tests/ref/footnote-multiple-in-one-line.png index 12def79ba83c57057c790866268f258f7196b23c..41faa1fb13bdc257ac3fa3b5fa8e1cb0116275b8 100644 GIT binary patch delta 695 zcmV;o0!aP41>*&fIDh8n?fd)u>FVyjz{vFU^uNK$m6er~larE?l5K5mprD{#U0sKV zhdw?&?CtUE?C|R9>PSdPkCB<;;^L8$n}mjrVq|QIi<6w5rlX^y>FMc-iHV4ah_SP~ zczAfVx4%tKS7T*uX=!Q8%ge~f$jr{xuCTa;hmWkTwx6M?U4LI^mzkkLL`-UIbS^F~ z`1tsKetwpgmNqsv+S=OVQ1uRaaM6zP`Sxsi}Q^edy`!b#--NVPQ>8O-oBll$4b9_4ROY zaMRP%*4EZ&Xn$ypjg7>_#KFPA@$vB_BqV%%e1Cs`r>Cd-`ueJ>s#H`|{r&yx>+9y` z=KK5m^YiwLi;MH~^I)lQUH||AT1iAfRCwC$+Qm`>K@>*epXr$(3GVLh?(XjH5F+sY zdkP4}F2yF(P&wZVoT|A!-J2@_0000000000ASGPaRewUjbp!H^F-8fFF-|&!vwp%^ zIX;EUN~Mi;BSpgnb$hp&ef|D~JK95%88VV`LppeLS_b?HU)>f-U4u=Mf`u1{k^R=T zO!**kc{4tdI9$QJ$MCaXhwEx{b!_xI98&h3PvO%uX=FI|6>ezMTT)7$*Q6Ls3dB86Ryf933iq^F3iozf3RjhD zXZPIBjIS>)?Sxm(x2=S;mlv1nZG;t5ERFu-fa`L71OG~fC46}R000000000000000 zEbp_t&+@*trQhPp@`__w;N;XQEDK7iYcxMMV=!mN3tGFj65dkq#!C3;`QgP%_~l6~ d?*sU&-T}FpQgx0KH}C)e002ovPDHLkV1f*Tbz}el delta 655 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 diff --git a/tests/ref/footnote-ref-call.png b/tests/ref/footnote-ref-call.png index afc103211207345dd73cd3bc982520aad1cb698f..635865e41bd5e3af9e649263f12b7d2f20e62464 100644 GIT binary patch delta 523 zcmV+m0`&cZ1fv9yBYyz}P)t-s|NsB>_4W7n`Q6{=-{9xw=k53R_~z&A`T6?O)!oj} z*vZS&*xKUU-sacX-*Itq*x1;NjEqiBPOPk~s;soY!phOp+r-Aset?K%W^SIKs9<7h z&(Yb?(%MT+Reynr%FNV4L`-mUdPquA+}-85yTesiUyqTQsDG-m)YjfmQd*gtql}K0 z+S}t=Tx587c$}P^y1KfSmX^}e(&*^u?Ck8mz{ru4o3609sjIVyijtF+og^eA%F4>I zv9ZO)#a&%pu&}U|m6a|oE}EK}{{H^Cxw+QX*6!}^`1trjLPDjbrSkIf)6>)G>FL+k z*WuyeNJvQc_kZ{M`~3a={qFDcii(Ph3rrCJ007xZL_t(|+U?cHZUQk7fZ>Vl6xdBj zPw2h(-g~c0Ti*XskSxwz5QBjEH1}VY#u^~xmNDr`&K3}HNyuUM5&n7Qf2vRn!e~Sd zyWFbXp}YnBKI=E`i#;lLDkA6?7TmDS8wy9F@kC6R|9=^6HH9gBibv^L&I{d4LQb(e z3eM-@D7axv*Wn^`&My=$78dOR)EmRm7&lxlL8}StZeLsAhAUMty#uVEH^dG98o=~1 zolc-Lz+SGyi&JUr@cQ!$mbH8`gB_mDHCWDbS?xal@rE(Fn1o!DJr!9YNSsOdIsgCw N07*qoL{Q3F$`1ttu_xbnt_x1Jl^Yioa^78NR z^X~8S?(XjG?d|OB?CI(0=;-L;;^N`q;osor-QVZj-R0fg-P+sZ*xKUP*x%UL*w@$B z)YjhC*4ES2-O(?(%RC}($CS^%+1lt%+$%t)5^-q#ec=cxx2%WHj zNJ2zRLPA0=E`Kg0BqSZS1abfX0K!Q`K~#9!?bOAt0#Oh}(F5-_?%inP?(XjH{Qtj@ zbQm*&3Xoc<`A+5L78qmf6Yyzdk*%LV=z1xg)RT4l*ntP;4QtW5MI&C<1 ztonB?_huk2r?eX@L}fVqRz`(E88sgl#@JUFKZWgp&cD4m_1?(Xj2-`~Z> z#i*#LNJvO|d3hfnAM^9}=I8C@<>k=O(9+V<)6>(;%*@zbnf`WqY?|<(&I5?%JuS7;p<>u=4_VywoA~`uZ`1tsCc6P3?xTB@5la-yCoTQtb zrI?tQgM)*YmzSofus%RYqNJ>3W^UZx<}xxe^z`)L;NZc-%g4#lB_$=>+~7q;MeOYC z{r&xAXK$XMsLRdOT3T8xEG&kGhH7kdgoK1qQBh1xOn<_}%-h}NOG`@~9UXmredgxo zoSdA1fPm@g>Fez9?d|QAm6e&9nU9Z;xw*OA+}x?DslUI!-QC^I&CS%*)I&op<_xJhT-{QKtUsny_*4DXfh_|C5zAA^u@)CK zfPe8F!(Ro5hljp9Ve2+0>kIR%yR(yWa>@KqBlded){06EAT~VOB2xtVb#g^TM@LDw zhH&t(ancL?%%`wSe(Ca+5ddiK={-^a0O`}5fZ∈HOV~3bP0-%Txg9$2rWePQxka z7V_2z0xHVczuWSqSsk40Fzf3ES|`u!(SM!{4()LeUR3C^YBDNp?yJ`h_#ED~)3{H! zQZ+P1vd;D1G1Y;76>^BzSv9PfAacj{1ozD<->=wiS(~WU;(-%lkrQg-r3c3IH1UHt z19Ae_`TA75A|Ym}Dm;mA3|fhgbG@Mb4f-+;;&~c3pq!wK(h^_8N<*e1EtMsj&wtJZ z1AQNp5}nVR+FEoSYS*DL$6zALzJL+4X>(cf77r{T1P>EAL_Nm&sD7t`Yli z{a*2>ak1gY1`8ow-JJ@t;YamWVkPSv%wb}~{+e9U7nPLMf7MSS5Zq^u>qY7S0000< KMNUMnLSTYPeU+vF delta 1171 zcmV;E1Z?}u39<>0FMsau^WER)_xJh!{{FYOx7ym;*x1-{adG?m{K&}2#KgqAySu)= zzIl0hSXfxVz`*tO^;cI{A0Hozii*0rx`>E~`uh6k=jYPW($mw^%*@PQUS4HoWss1N zXJ=<-W@e0xjDLTBZ*OnT&dy-QC^I&CR*Fxt5leP=8QRU|?Xdu&{o9exRVB z{r&x)pP!nVn&{~0M@L8F+~B6Brk9tOgM))|a&oS&uHN3>q@<*ai;Lyv z>O@9PrKhjw=zr{IO( z)z#J3*4CJqn39r`fq{WqT3ReDEQExF($dvYQBl{|*MCe*Oq`sY=H}*ARaNuz_U7m9 zb8~Z*m6hq~>Fw?99v&V$J3I37@=s4sQ&UqTBP03w`D$uvy}i9jNlE+r`<|YjkB^Vs z+}x?DsjsiEva+&6LqpWm)RU8w@$vCOLPBF>W22*^+1c4^Yir@*;faZfX=!QD(9q@O z-|p`2ot>R1C@6eE-b z>Y}2eH#aw>rKOaVlw^EENB{r=FiAu~RCwC$)m2YpQ4~Po#Vu_qb&3|JxI5$S?(XjH zu8bQD?q1wWmESg`nasmXhNs?!ko&!zrU9cPx;rV|5!h z$z%dzYH08->(0;Q6U+*}hr(q0u&xs?=HP zdQw8JU$b7G80%CrQdB6{=<6%g!toRPe19H(_{g};{K?)oMXFi8Mmd#?WhrimHEZ-N z3nTRYy*T0MG2h=*wYQm*ZSq?Y%74Uo z9Np8AI>Y+1)Mw@8Z>51Fkx1r*=~*G3fLTcoGoJ*Xzc4;J#Ea2`A$I&pICr+{jRNQ7 zA#1OH!)H#T3W8zkYW*8NbrMyQ$J0tprhmgS+3y>_3io%oz@%k-aG>9XKLrN{E>6=B lotCZ}mHxOyBAJ7J0T9}t2>3RBF$@3z002ovPDHLkV1m=Kf*k+= diff --git a/tests/ref/footnote-ref-in-footnote.png b/tests/ref/footnote-ref-in-footnote.png index 94498598360ade600e1c4f7797ec1b57d7c45edc..73901b479b0d7275941f12b304dded257172dc0e 100644 GIT binary patch delta 2571 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 diff --git a/tests/ref/footnote-ref-multiple.png b/tests/ref/footnote-ref-multiple.png index 899afca16e4d3445d6c4e1592a3353f2b0603720..59e9fecef63f228a9255dd9b70d60d77976c7c83 100644 GIT binary patch literal 4425 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 delta 1448 zcmV;Z1y}mn3%U!CB!7QUOjJex|NoSfls7jw?Ck9R{{H3V<^25o_4W1c?(UtPo#5c$ z`}_N!pP$gs(9_e?%F4>c#l^n9zM7hv$jHb@NJzuO!=Rv`KtMn!C@5K3S*E6@e0+Ru zZEe=p*6;7{o12@5hljkpyjxpabaZqgA|lSt&cedNq@<*nn17h*>FGj3Ld(m`=H}*H zTwGdOTDP~iwzjrwYiqN!vn?$x-QC?{VqzsFC8DCDqobpPgM*NekaBWzm6esKsHiF` zD!;$KMn*<|e}Bix(Wj@UcXxMJS641BE-^7NQ&UqsJUmfRQPR@V9v&Wxi;Ey2Anon# zva+(~=k4?J_J7;m<=)=jd3kwXUtcLHDQs+P+JBgwY6hoV@60& zo1LZK-`{6vXFx(qYHW1a*xIbDuA`-`uCTb8oTQVLopyG1U0q#5L`=-i*0Hm@l9Zg- z+Tw6>dNwvTK0ZDpBO^UMJ*%s$O-)VQ+}x6qlC-q6b$@krb8~Y~PftTbLvC(v_V)Ik zo}O4(SYcsdoSd9eQc|j_s=B(mxw*L~Cnvzbz{bYL)z#H|dwcWq^V!+i+S=NHfPk;B zudc4HRaI5z=jXk>y<}u$Gcz->v9X4RhB-Mo_xJgPgoOM1{IIaFFE1}?X=(BC@xj5t zg@uLD(SOlpWo4O}nP_NeIyyR~rKQBg#Dao?K|w*gySvxd*Kcoc*x1-{adG72~gevXcg>gwwE_xE66V5zC8kB^UxjEwT~^5Nm(h=_=jlaui9@QR9x)YR1a z`udHHjnB`|`T6;3YHH2R&Ghv2mzS5@+uQy9{eRux=kD+G>+9Gz52AKQ%0?)kj&N&B#LZR@j))#Gf)`3q&Q~_XqUE2y>+E5`6h(Z;dgW?)#pkek zFHD?0Ld)>9&agR`-^Sd3fcp|E^nYpIJAZu|)|J7BKMg=l2>Wd0Yh#8mF)_)d3*LgU zsp?^K>lyN$_MyXg@6&KVoJ@Fp6ljjLxP>OyJ9Ji8FhOq}uFXPnMfIyDR|5q_PDj~m zrzAbpGbFd(fJE_auT|PM^cTnW0!F^sIC73D-}B^~)F=6=pC7-e}FdJa`ZdmdZDb8&wEcif7mfXynxG$@J}8=(R~jOy(KMFa zxn=qfaI}*aM_oOT^eZZ)S+gX(V1Id(o%0x=XC(hmGTGdn;Y&7K5Gu$|Gj*T>6(iJ; zYcx&y98OLmI0hK`DJ+WKgxgBN`A`kSmyq%|S%!-J9J`D_4;Y+|z)P)qDAA!bmd`-| zY!B1}V@S6GNtHZlTj*W5?yXFCY9MIZCmGzMuia@raqaqz+(S3BPrpEuXMeTzN=*qJ zU|YGWMb?dd77Bc*sU@~0pbhN5_jJ1A7R;D2Q`tcZg~Fc#JmTAtJyJgAXYzdAT75_` zZ$g4}TKq{`zrk>KAXPn_CXIi?n>JGV4kD;M=-=?7$&}U?6!fL;qJP7p*!AMG7yKP| z|51W|e09!9{OS4Ar^19n%pntog@!0Iu23kxLmvT?KNXh!kY&990000y(s~{QUg-`uf}3+sMes^z`&NI5_q7_2}s6 z<>lqSzrW+-PSdPOiWB^X=$ROqNu2-K0ZG2@$tdI!Dwh`)z#I;#(&0hb8|U4IfaFVhK7dV z;NZ8nw_spk)6>(Aj*jf?>_bCCSy@@7rKRTP=AWORVPRqK@9&zLnp|95T3TA#+S)cY zHhzA7mX?;Tu(+e8t&^3Vnw+Hg`1md^F3il#o12^2+1Y`CfwQx-y}iAFfPg_kLHYUl zWo2d3($Z~hZGWJkp!@s#kdm5yfQYH9v#_$dGcz+=TU(u-o&Ns*!Nbddf{K5EiKVBn z+}`Hd+vA_1soUMVc<+dl1taePg2>!xd@rPmD8!Go!%l+zx%% z@~pwJwSTRYlFdaUl}gE{rlDdlAU8Y+Twk1nTIa|Rpu(r1))njqZbN4vR?+MawBhFJ z62v@SUrQ^)RNKk{=iv!`xU@`zywYe}KEoEj1+cwrwSY)LSXS>`eaAnblO}4qd zl1Y=T%+K9D03}vXGYd^hT98NxYvx$suMndSx} e&qX1`k9-4mgH_dRsXc)J0000y3?#@$vD&!NJwl)$Hu-rKP1|VPT)2pXTP~ zSy@@Px3^$mU^zKC;NakfhK7ZOg^G%bj*gDg)6-g7TAG@g@9*ziTwG;kWzy2po12^Y z`T67HQx9XliV9Ktf7GLw`d)K0Y=!Hrm?Setv$I zmX`SV_{hk}^z`(iqN1p%sIjrJb8~a>@bHt9lj`c~NJvO&X=%&L%g4vZU0q$t$;r^r z(7(UG=;-MD{QTS7+c-ElE-o(n`}?4vplxk!e0+RyadB^NZ;y|UkCB<%-R0Ta~!(shXUm z$H~#c#LSbGouj3#uCTb+*xIbDuBoZ1`uh6i<>mMH`TP6)?(g&6-{S^x@cw2KVxk zWm&vZSbwH5bdam51}-noL9L>)3J`%^P%C$I0~^5|5E~t_JBzWowGCoEudTF1!6RX= z0-lp&`tbUi2KV8Sl#*8pV=P_75<==|3Gu~blE}lonhy!<_72eJ4f^oS74Y?aMjyTo z0iQpo^kE@TSN|KGQV=$48!$7igH|LQe)798>rS8J31><6^7;XwOb0dV?H$N3Cy(s~C@3f%9v=Pu{gacE3=;*e#wp&|U-QC?%Qc~2^)T5)LoSdBg{{D=NjQsrk&d$z{kB{o=>P}8ht*xzb zadAdQMqprIJ3Bj}p`kW5HX0flq@<)%Q&Xy{s##fC#Kgo`SASP@bac+o)7`ea-E3~% ziHxbKsh^*p+}+{T)zxHVWO{mfsHmtN9Ua}$+1-_y-Ewr#lbGGr+})jxhujl9IAt52n&CN|sO@xGm?&$37?Cf@ScDJ{;g@uKi zou!(ZnwgoIzrVkpo}S*`-feAd@9*!tyu5#ZfA#hCi+_uY`uh5%rKP5(rozO`h=_>A z#l_j#+2rKpu&}V*-R0)z?ep{Y`}_R&_xau5=kD+G7{ZqD0004FNklmj z3`RdF72HZG6nA%bcXxMfacQYjw|DP#}3)eL!Sn{sj@GlNQOKk#Bl zZ!tS_RqY*9)7iCkg24FZA~_HNzdyj+{MWcognuqFaY<>?qbU-)?7O|)3{)s^bsLG+ z4Xlq)EWw2i$V$|1tf^c2d+5XoIhQxcRu2y$Kacr*loji=sL%`yhQ)+attx}zRC;H{ z0}ADo;_{knoV^-9oGy2fjPV0ODdvAE4(F%De+zyY32!(MVhmz!OOeDc4w1x5%OZ(a zS2RQtJN85$E_!h)wzoFVMG(v3fzc8xtqw8%+Tw8j6_$dLU|?XRq@*1k9ZgM5&CSgrAtBwBnJFnLLPA2gxPQ1iJ3Hs+=T}!(udlD= z<>lew;YdhGjEs!ka&*?#*3i(<&y$$l)!f~ttK8k;+uPePFfeOtYt_}&WMpJ|dU~j+ zs53J&^z`)b@bIgvt2Z|{kdTm7RaMy7*gro%@$vD)!^2ouSd*2Vqou8yoTRR>xaQ{O zJv}|FtghJD+JC~t%*V;m-Ltsegopp;<^TQtQ&UrfgoM}E*ZKMRy}iB1$H(1?j9FP( z#KgqiY;N7QyVlm&baZr?nVE%!g}1l2pP!$qs;a4}se62Y;Nalw?Cf@ScAA=+Y;0^7 z7Z;qIoSvSZ)YQ~%ZEf%G@4URce}8}V_4QIxQr+F%TYp+9<(11MDh00Bx#L_t(|+U?ZE zQ$kS`$MK&^8X$@&wqSR6cXxMp15!#i@A5Z0Z^e$4-p4TavpcKroVjxk7=~f~p6LSn zd!RukmVXzhJ2E;w zX}Gy2B+AYTlQMi>lz^6^Liw8o;CWon-8BTXh=0*i4e$mPq=#ZS9j!V*Dbx24sQP+4 z=!G-d+Ztg|d25lMNBn-88|(BqPY diff --git a/tests/ref/issue-1433-footnote-in-list.png b/tests/ref/issue-1433-footnote-in-list.png index a012e2345fca796e807c0534e93196f8ccb201df..19934a709764a3ba1bba0f7ff8cd7fcacb0ecafe 100644 GIT binary patch delta 533 zcmV+w0_y#Y1g->-B!AUVOjJex|Nj600N2;osHmv(^Y-TF?OIw|xx2&o`1nXjNST?L z?CtUC>h9?1=-%Gmjg5`Y&dy$5UQA3($;;El$IqFYqh)7ri;b0SZ+Eh^ys4|Rq^7Q@ zs(lp{dr_+cY#Z=I86{?C`g@x8vjEIyyQ50Rfej zl}ATM?Ck8nzrXwY{P*|y-QVZ#@AH4#|64ntJpcd!6iGxuRCwC$*;kIjFc?MQI}8ld zd+)thrqdH+_J6-5-gx8zL9Phr+rW_=%aMNz047;C1B3|^CQO(xVZww76DCZUFk!-k z2@@tvm@r`g`IUx{${}_-amvE)6;t@*)3wyD@b}lUvea%hJ)0|O)IDKUYyB_>NWR0x zQmM$V5&!_?(u@f!55`Sg+D0jEmrdcF-9JybvKKw!w}1T~#!ZZy2s3U1000000001B zShE9Zdcyk!cXoa+9@5k5NB-K`^hcp%MrHkUEHFz*Q$786g{BE%@y zGwzffnM)6k(u%SdpPse0zM;(hzr1Qyo2GAke&z`$*B74f@71LzY+wwhA#lk6CP@#Q XDu-@(;|^~}00000NkvXXu0mjf>w*_e delta 498 zcmV)a>hA08@M2_aiHnns zkC&vTuBofDvb4OYsE-$9bH!y5RcgFG=7i&+{uRzJRTNY16aWBP@t(xa z18r6Bj14#PsWs(b{*%p(^+-`HuEp_D zMtEs8PEIqz%PSFQ8R4=_3~z6xeB(xKr(wKlzsZTwh@Omb8Xt?k-t(y3-26iH$3@Zw oa(|Z-Zrt4Fgx{{OOc}s8A3*+2QJ3M15&!@I07*qoM6N<$g4Qz)9{>OV diff --git a/tests/ref/issue-1597-cite-footnote.png b/tests/ref/issue-1597-cite-footnote.png index 6ec017c76814c55908cb99a44cbb0416eaf34389..e7c076b14e1d23dde9bbe9defd74fc430fdc0c80 100644 GIT binary patch delta 488 zcmVJ^p$f&BaNJ>(0a(Y5UOu)j*+S=Nbl$7=L^>A=- ze0+RU3#>gq^HNXyI1%FNWy(bFnbM&Vokl3l0F z%*@OT0k`|#Pk(1Re%`upDKKnIjIduLz&bcG%Wpw2dfsIotcgio{*I3^EGsMYO*P*ReuQ?PQ6ck eDdi^zy50|4lOUp9I;>Fu0000sj*gF$!o$a|u(+(Qw!y){)z#I;#>Qx9X#4y7pnssCHa0eJaBzHleD(G9 zl$4Y{K0cF^la7v#r>CcNb#?0M>YSXMmX?+Amh{)>x?^Yin<>SenC007fTL_t(|+U?X=ZbCr}M&Up94I#bv z-h1yM38X{V|9^gvc!#IU7{vKDa5Q7t0)QU(_N)-^<$HzrsN5++_3Zd^J>bs1cjHD7Zt+Y38!IZ zoD={60OYLRmOAIZEe3-eWcvr%_t&WX*vQOmrgZG+&`JtLpPrg@AGVa?yI2Y&}#6T$ua`-xmaqgz2& zm>$;zJqwzM6!hmWaadW;zIfRS?q6Hq&LuRu6?BE^aZS*(pa}&{K|5yQo&lz2_(_}l zK&OWg5J0zgL5~HZs~P)eeb8xu;qhaD4!uvideqMkJvjp%U6>dF^pj!e>IQExV}A&s z`yoab3+UqF;(v{e4L<0*dKesKlOCO9b(sRZa|F=mCCrA!6&M_W{vm)*>Bml*uYcF-!7%4jr}l$2l&e-D~nXg=c+5oj!Dx7!^Gg>*Wd zTrNK`(9sXjqQ*{*dUnu9tpgJ%EG#@R(DzO7!i|bpK=17A)YjG>>3e&7d3kxsWReUT z4`X>bAN1F6spujZbklY41RxNC2R&TS*d^j|mVXf*Cacwoip%A~?m-02bo|jNKInz{ z)ZrlkH1>3Z3EnPY{#?*_cEjQD{{gMlYGpDRW`MfKyS#N3WWmOw9RJ2fshOubHIw$w8PTJ4A8o^)anWW zw14?AhEA_e0~fU4@3&YiqP#~V=#dHX{a>L>gI-%t1$-HxtE$QSee+D~5E2&jl_s*Q z;c*7EV*0`(p8 zz=E%T(n;vimI0h)2m_0;7fr+qIv)Ry*+6GsypKeJ`?t1Zxr9cyg03(Gx)U2h|6VB!8SqL_t(|+U=KHPZL2D$NfY+^2h@T2`{`DO!R>_jf;^43=$KO z8dSVd&=ji`1*~LkMNMlef?O1^rC8b`PVK2F-CtEfE1{ds@UaUVZpA}J zI)K({wUJ09D=X`7pyhV<+yOuj1=zEx8R{GM0bN>JDvF}l>n$iKpg&p_G;39NiSNIH z9uBf^JQ4!oLK=ZySXdAQp{J)OGcz+33LO?`*8Jn_zYb0854x(Vs;Q|dFE5XB_lN$VeB^ zYin!8#l>-ZV`C#DBO?}zse)#k@zklkpqE!L6i^3!^ENm=;PFA@%S6z0N<^ayBS8?% zW-}RfyPZytRM5(a-`cqs^vHMI*+~V>&h!Q&jDP;1{1ZXb-u3(a#{;^quCBbioH8KW z>2%TsfOhfj+e!`3r9lt95^GkgbnlMT)-HE-4<&ynw!DltuO)5*>FO1Ez=K-p{#`c3 z-p{(zKc!nYqS0Rlg8=|`ZyjZ2Wg3k}6h*7my1l&}_s3$tC0{^#q?4DT9BQ=EvwCS^ ziGOunZmF?>Z6T*$f7c9eG6kn(pEHCO= zO_8cb1GZXI3l>&=IQk9{IugOgr>X@D`#Aig!TpV=z=?{f*q~(aM7LNb`002ovPDHLkV1lwo%0d7D diff --git a/tests/ref/issue-3481-cite-location.png b/tests/ref/issue-3481-cite-location.png index 01139e25f55ff67f2742e2b9a20076b9019059f4..110ee4a2493c67ad5bb2b0b8c9dc00c8c748886e 100644 GIT binary patch delta 482 zcmV<80UiGI1N;M!B!ALSOjJex|Nr;*_lSsyi;IhPc6R>${-vd*H#ax#?(Y2j{B3P* zm6et3?CfJ>V_REW#>U3vAYm*K6&tep`}m!Y23jhEB000000000wF%*QK2hbHy z4TZS5y;lfd2?{SQ-zkK<6VAiTI4J;t)_&Vk@BFpJU&bGL~p`)uxOjJ=*TT@kCl9ZgizsG57bGNy{LPSi($Iq~`x{i>T zNJ>&zT4JWCu#=UYKtf79K1O(Yf|Zw_DJwIHi<37wL9?~JjgOakdxN^X#5+AjD=jtU z<>l4Y)zj0{9e*Du`T6=}W^PGKQzj@dUSMdet+hHlM5U*%P*PenHa^PC)J;!UDl9Zd zNl~q>t?B9MV`F2*#l`#k`@zD<*xBErqM}(@SwTTTPft%nLqpKe(Ek4ZdwY9pYis)Y z`Y|yvmX?-radDKClyh@)sHmv$@bIjxtaWvDR#sNX$bZO~n3$iRpR22@nwpw_e}B)< z&*gwu(gp4vYJgTg;QdC^0sj+f&e0F$%aB_MvGdo2`Pq@0mnw+FR zK}o^G%P=xJq^7P;P*`(yeW=6zQD-N(Ad=0-j9))uCTb~=k53R`T0?5umAuA z8h=SdK~#9!?blUPoM#w^@#pHh%L*(k1Px9g2}FrO++FH!sibXEcXxMpZ{zN+5Q88= zgS)JI&&%u}GZ|*yJ=jh=9e!WnI=laKiva`v=l=4ytAuaxH_v}O{4sKdWYC{;yXarr zI^l!oKQjEZ2^d!mU`m~N3a=1)1&*YTLVrQGf(v*}7{3^%M?q&;Bv9w}to=^Ndhj_x z(`HCmS_eRU9A1tr`l#unokX{=K>!Si2~|spbM~v4rsxRZSV^nr!w-b4HO~rcCqhtO zJ%Q`>L7M2nQr!3;Ej1@yC{-yM^hHxp&(lr^aLrIQTuP1r&bC;4!jc)aVr+PsT7NSO zTZ`kUgh#yfrjYZ*DuGQksH`T>EoBiL5qt9Y1IFrIo@EaUd3Sw@zj>Z{TKM2A z{H^T^;gc);?e~F^=-)ErI408{djkDyTQ7XDV!2^#BoH?qz*L@d3NHn{+#~5TT%ud~ z1-vT4!eMd-w3gZdr^T~Y_+<{ys~w^?%0&{DRgu; zHsDU`^nCr8khT1Afo;dnD4Xnz*3Xu@Mxr1(@4R;;#;A%~nW2wDt*mzgB+fY2Jrao< zf_uWw0MwdWM>#K*Ey1WV{St~tJo<=`^TLY)Tj2&Nz0$2~#~Khln18}1=|HWyoxMM> zyF+tS7fh}_Fck<+nw0Au3Bs+;te!CJuVSf=xt5!VZ#LG?j%^dG*T|2=O9ETN#%M)~ z)pFpnkq9D0P1VhjJtI+2c>Fltu+FI5(Ho8|g82&C4LL*^x9w)?J>r3NLe9&thyW#< z2T^Qu{HaldMAcJeWPhl;b5CgC65YA-<9h>#>b@a63mBG)gQ1pCP#)3k<7_g4fMX)@ z0Wmi&Uc|4?g9)4RX}?$8FJ!&_j!*^R9um5}*_m;j(G=8ZIbY(aZ=q4onBhGf9qrpd}jj&{PX?>nq$vFq(+&000000NkvXXu0mjfZM(q; delta 2296 zcmVNR}$ zaP7_HjWxjP{V`FR7s8Ps=e*XN)n&;1-FI=Crv^0LpmoNXXPj~Ft zQMGE-S+i#4yMOQJ=lAK;r}pjJw`$c&j7msI7&ves>*RI{RZQB?a9O(D&-#2U4 zOvW8Mb_8EkWc=~t$8O!a5fqHyzI}V<%$c1#cc!GI{FNsdvvup%YuB!E6We&l$Hylk zBI5Gp%YTfq84C{`I>h^2GWXP}QH2?!KhuwX&i zvSoRhh&gubSZZpj!GV7I^r@w#rFJ=Tzwb93`1O_~t$OO`Cj$jAr@3Bj=X_3MuqF#>>Iym&Eb(j?*+XJ9A(7&U5Cn>KBj zNKEzZ+jq{KIpPu#(k^}?eUX>gG|ihgA247*ixw^9DgLWhuO12jP(fCp5BY>Zo-kno zn17-}ojP^cJtPg>#+Y|VYb-^PVEv;eZ_=AcB7V`h~;Bj2Sa|@??qb+O=x~ zC{u`B-6Fw823Z0h$aK{cfB@pbg9nL;iGPI{^7{2_lKbe339((A1l9%z+S=NhBF-Q*G?Wk^uaQ9f{r&kop+NvPZQAtn=g(15QK(1_ z0WU=KMr42`zALt@Z9XNL0ym|Zf?Q?VYK*i3>_cY-!uJEqnaUn>S<+_A(n8S5bvn4HQ_1rkSA{=WlF3u^>bqNAf-Me=iH*PGF zRN}Np8!erSzBoC%(qHJ4N`D-sIlrT@913X;I6Dhh!;zVj6$%U4AZKSfMq3+eA8#+t ztj6Fv1W>vRNF{nUVPTN4u&^gjo^Y_uM=EiLL$N-}SOxk;Hb;L#Ds9@dNh3P+g}zRo zR0+J&wrS99D>0ZK-2Zn zJ83}BN=TX?8(1x&u!T0p()A$2YjPG*! zo0a09B9C@va-@H;W_||4-+T6KwVU$QF8mRN=*srKc&wmN2K&!c7!v;*_K86r& z1_xTat;^nl3r#R7f-r$CqWGXXkLaOAV-bueMg(!HoZOV;C|MGg5(Aja&H!1=7E*~s znQsctChd}%*&q6;nJKvE;lqc*-xee=2e8;vr%q*zMG~a28P%#)lQF-zkW^0MhCXC~ z59DP&VQ`>{Q-6w~#2Tdx;tg#;pD`E#2e}*0n!u#iVXAOCqBkO?2BDmC$rqAVp(VT+hGXwV?;=*}g>=>F|MT9`O- zBL2V-{uqM&U`CLqJG%!5;=Em4C1S`sToa1An`?QUF)1SV6YM z!;Kp^CJSTI;>C+$Afa)|Fq}Xr9l#A)L*SEMX3w6@KNQf95RYrttRcop8jl`5BCbgS z<`~URtzh5$J;mf145A;xqn0AeJc-uyi~^pq$v_JS7)V#{h`T0IyB@ z1~s&D%5f910@uP;;~|WN3nLo#@88cJLQj;0V4!S}+2n6I9+5^^+|kiKCp&%h>eXZt zV~|RO3~=O@N}*Ix1B} S1!b}T0000R?b92PR#Imxo_xJa|zrmrQqi1Jl&Cb%Kq<^IL_V$N|hr7GGWo2c+ zz`!#zGv?;zfq{WyVq)Ij;PmwMhK7jb=>gw!XUS6uIs_X3V;^N|0S6A}# z@}i=m*4Eaksi}p9g+xR|(9qDVtgL!^dO<-!-{0T2x4EgQtKi_^?(XhvY;5iA?LIy} zHa0fT&(CUVYJczV@vEz?i7XvW6I+S=QVjgR>F`IMBD ze0+TB>gq^HNQsGwu&}VRv$M;~%bJ>+h=_=zqobgppl@$)WMpJ%X=#&_leV_DjEsyf zE-rq4ev6BX`}_NFaBz){jbLD4($dn?)6@O^{r>*`<$vYn-QC^$`~36s_U7m9_xJhT z-{dj=5fh0+iIMY*_5g8w+U1rU2L_(aSLttXk3BRgJ>y53Yxe1&; zG3n`X6@NRsd*BS|N%mJ~Nil>uyR`vC3OI8$fa?!p@Qb!!c5GZ-i8(MRL=eWFITr|< zY5=&@RFnl68C6$Cu0BmF`A;g!pHEH-Vi0mlnR0oYn}CpG#O`c4rQr$R);IKq=f=42 w3oA$XX>nnuKOEsVhXgwu~lasc#wuy;}X=!Prqoc5}uy1c~prD|d znwqAjretJfetv!~E-pw&NVBuE%gf88q@?!t_G)Tsa&mIm*nimC+S)cYHaXJ^F3#N6E6baZrcb90xMm(9)1h=_=tot^3F>G1II zk&%%sEG#Q4D>yhf@$vC}eSI`EG{M2aU|?X4jg4?{a6&>t*VotI-``3~N~ox)mX?-H zO-3_`bfr$jHds+uQ5w>-_xuihqiV;n{bs0004FNkl-k> zDI`*6czsPSi}BGw2>~yz>rhZ94?pO?4{vT{hONLPzu^Km5Nod@WVZr9^dgDV6E;;= z3c%yV_Lhc~8q7-y>Ip(LQE+i|oI)bfEJ=G#Fn{OzU?5#2(Y}5l;eKi`?ts;33W!Il zTO#9#urEm&!cK=B%Ig{%u}p%+1Zs&d!Q@Qek1CpP!$Li%V;3D}ffF z%*@P#2M?}YyLQKp9Z^wHqMp>>-!G9!Fl}{pwa$Ra$;qUor0(u+147 zuex>X7N%7ym5cgYT3Y0CIj*Z92vt>81_U}REDS{O=5>y0G@6;28P*;i9v&SX9T^$H zdYqn~{&ikFvZtqKYHI2qLFeb^=j7yASXf}Rv9Xariya*u5{U%;`}gnn_4P$6cW`jP zGTgRpTUl9IZEfwIJ$r0zZ6_uskO8Dp>4OIkkcGUxy_W&ag{cViA3Aa3gpG|28h7s8 zL4Zo7!qVQmckjlH8wUmkaPz!y;R4n*0&qPpU%rfnr>7?(D=I1o^xJ~2udhev*|TS5 zGMST;)77h2o0^)?|McloEb7qE(6qF)*RNlrr>(6GJ?7@-h;(*#zH;SCU0of4ek;&e zlF7-*=yY{;<()hcZ`!m8YZ*xc$r?TB>FJ0BRvtsCL$?9~0`Q*Q+}!vU7>tmh0Tg?K z;^N}o-rk6a2)-kBbaccLzJLEdzguqIy4Bd&I4>`+udgpUI=Z2u0sZ#&_Lvqg7j^04 z#fuol?=lq7*Z>?kas=<$)YMcaliAtXZP~H~pEWQP85xQD9)L>d_xJ%9cjJ+ZN|CMG86K_=Jk@dY-~&}mkaYcbLLETcD6x)#)`wT z0y{JycB)V)ewr5=$i@7WD>pZnw{=Eh5;iS1b0sAui-OU~M%hoTR{Q@4o%8_+dl-7C zL?X}xnn05XG=U}&XaY^3Nd%felRiMuC~M(?HG$S4Jhu0o*W<^J-_h3*92~rE-MV}C z?kycciC}zu+|XAv%6)intX8YVcNWTttbo9UhtVh$ynOi*60g49h?(X6Op%8&QqsOG1Cn0Z#V^Ih7w_DplHi? z9o$8B`t<3Tm>8ChP|`I}&~SDgIB)>S01k-)`4`C))D9m$jJ+CcF}MtDwL*su9l|x> z1dkp)ikOopPvUlm+hcruJmxum{CIP7GoK&k8$l3Y#KFv)H*dy8GEmUie!*Du^PfT+ z%ad&MkeHdISBD?Pm;b zbL%gGUxj@g8qIOr*f|hry@9s-*ok5NKDQW4_)Fga4jMxQTK}MNd=nN!pb0dACanmx zx~a>}!^_#_b2JFF{z2Q?J2I@5)vCng?{HL9HQ& zs|Yj<&I$91i;Ghz6x_-WrKF^&)oP7KbN%}Def##QR4P8CR4T2ltxZi$*(uD?@S2M6 zSup-uSXjUtzbZk4qlt-$k&#hNO^vs=x3RHtMn(o3g8%c?t5@t>K(L82&|LT?08F~9 zEtku!tgOWMFPMfU63J>!9a5=u_wLE`g_3CX?;lxs&~W+{nn^*}H!gHKH&I;4iQbu-k5Pa=9GJq8}jV>UO)yquP#boOzN` zsYKR@wG?KvSw2|gOto5#$stA4nF7sB&HKk&K`NA3-0Y};0%(zr!C~seWd{*WbrEC2 zmTDKQRx6^QR4S!rh53A5J+`0|$ALI>pRt2oZkWwx9Q9bj*?A3Fd=+)t2@gJ!FSe-a z)I;&8%K7lgDEgk4T(||hK-vhI2uIg9SZ_A$OPw`3~`?@9FNCuCK4diu(9LI4v1+RlR)c_$h)Tj>E{OUyoHB;naKlK-qG`nJ-w(tG*rm zL66@rAB-;2iVM&epzi|x_{r0kuioC(usnPI;`N(%e+_6e6FhFAZ?^Y;0{!6OqXgd% zA3xdf80VkAd?jxI^e?AVY?!PYE$a5mVv|4o!JvH5@Zr!@) z)UB?m`O!c9{EPI_iHYc5Akl8!0(Xo#YxdFQ@R5lHVG*L?_a57;LQYRlL-dzNIkH#% zN+SFy`xu;`pFcc23{K~j@lVMN!+y3Oycq0%Qj@Nh4zDMp#D-!J-V5(UD z@$oTssulfW-W}uu#RY>cO_(Hskp24B<>e(7O3V^0QfS>z>3<}u+#Wv6mItS612j>I zE?i#gfvq6Q6c5}QHAm1{Jw0y<&?(%AJr@YAJd&nD=*8FeJIcF*!Z}GI4!o9yS^!*X zwJS|B_RFsp6M)@;zm+tIQ;YIL__QXOFbjNKeKDx${PskVf0i$k?>#G|MR^-=e1SoZY3aNZd6vD zQ6D#sr5g%DI9b?va_*9Wljs7^A zUJjNogi}!FSR>6ej<){$&D-x~5w>}yur0dWz#ySt_)gf!x_IRXvG z@qd5+!?mqP5p497u%3I@%f|tO*oWr)?E?Ck6;UoCnm(Ej)>8r`76VATjKHABHl5M>%z{wfh5X*8&s$!zkNh%j2E z_msw^pol1s@0yA=Z;O&ViEKMbOZ_(&11-n$sWsW)GnQx??C$PPJ-$fJ zhOGKa)WNrq3FEud6&QX}v!xExv#3in851kqh)yFrP!C&I*YSZ<%eaeAE{-SwbRGPR@N> z5*Kf7ZjRR@{-zdyKP5Mt@V4-z4!3DT2p&4axM)dv&d_uh+wlz0rDk%6%@uKw5FC7Rkd>ertfq-h zL?OYO^eQaIPvG=P3nA}e@SKt#9a;BRj)@^=<&))%&VB2$_YkNM5svYkv^ws z4-O6@v-pYwMD)Gp={CyxbqXK%<(4$LuvN3U2i7B#VitoT`#m~3qS6(hWe<_=f;h1$ zt^=QrWXdp!KH4BFt2iH~U65^owvsOPNY+RfNwEkS1evMf+yNBf1^U^_OW$%jUdXEY zO-YQ(Qvh1S(``&F=y`_6R|cAhgRNvp%(~1xDh0>VLvH+K;YM>j4xJqeYtl)w*fs&(D-p5cTUT zloq#VeJaQ0K(}~hpxKdy$5#4LeDYNC;V48LoS}@<(1k6|7Im;|<_Q;`m-mnj1T`bR zV?r}wh-~D+>I5*^S!~vm8)^LtKcMD%!IqYw#Oe)drq8iQp@nIOXajA3*IK+yIn4r4 zKOKcH4lN%_4q+?Ih&PjwqjPk^@_f+@86?|`pUwW1PcICBj z=(h%res&fuI7MFFS(GE8aS{qfibRWWhqj>9V%u`Mx3_0*7g09By39>ti87|KnB>-= ze8ObGNo>U_PKYIznIn_>vjaM!j0ed4G}RP^$)7wM-hm$`y9J$P&MXURSrk?8w48-7 z(i5z9sa2jVKE!?eDd`y)id3c?gqNa}v@L<1_H$Xw_x6*gx8SST zR-ucS?Yw}d^~lh`14&Ea$GEry5A0zi0-#$U)(qBH;dqD^!;$nwu9J5G_qsORFPvMo z=^RPh@Ll|v$j8t$2F54{0W?I3ET!@lE$SYqOT?3G4&N5FX)25qlwtXr&;q2G!uIxd z48;vgV4D}vSU9Sk^_uQDZXv6BJhOvf0q$bFY|6XzTtBFaC0a>`RjYxC#f=a@9mvz* zKtN2raw_%7sU)WKeP5wT4n#g6W$0#x* zpxH7^GmSz)H>B{e#vD=uVV7m;p78*P85ztY5!d;2dO}?foiAW1oG$`IDubO1#qyb>(SzJt_W*!;#FF6{5yoc5SeA1BMnIk?C+ma+=5; za{=0|@N3M<`~^ETh5&+xWhQ2iEGA?-AO?ZnO7~%U#&>xy$a}rz?sI;jG{fwL3`!&s-}N2g^_}3yz+^^)d|a3F}`(X*qLE39q0~p lFCFL(bO*YZ4)mIoe*xsRgDId@BJuzL002ovPDHLkV1hOj(Y62p literal 5129 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 Qga7~l07*qoM6N<$g6Tx#a{vGU diff --git a/tests/ref/issue-5496-footnote-in-float-never-fits.png b/tests/ref/issue-5496-footnote-in-float-never-fits.png index 4ae5903d808d8fe616b9e3b7d484e59e508b0ca1..85851f5ad05c51eab1ee30bde1befbdfd46b90ed 100644 GIT binary patch delta 329 zcmV-P0k-~+1DOMmB!6H@L_t(|+GF@n0bm%_OjXU_zyE|Fax~w){~Uq_Li2$`r|#T) zMvmrFXRbK8_}sYlh)kOcimJ%eeE#B153isVt2R*-V56Gpp!wPJw-AI~GbLcO7#h_) zs(DoN5YaqZ0FG)V2*|IW-@X4#rsm@(FOsSG=<)NZ>G|Z_Tq9A{1k_As+q0pm>+zEp z6b0A_(Tr$^eg6Dyv;Z6}Nk%n~c#*p~e~y!UpFe*-gdG?d*w)qt(Y$u;TB4kN@7}%A z($c!RI%{j|f`S6*aKQQV`T6+-G(UX!P*PIz=FOWxc3N5*MDy|E$CZ_pf%X&7ykNls z1qB5lFGdrb#*n6P1xBlUcAuP*LQYy&dA89t*w=ikeD`Y+U3ia8-ao6 b=jR6iA!|pI4=Dx}00000NkvXXu0mjfn%%19 delta 319 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 b=jR6iA!|pI4=Dx}00000NkvXXu0mjfn%%19 delta 319 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 Y8*7pP0XFhPE&u=k07*qoM6N<$f`Gj<_W%F@ diff --git a/tests/ref/issue-5503-cite-in-align.png b/tests/ref/issue-5503-cite-in-align.png index aeb72aa0d169fee63a9e8db9bef41fbd4427cc4a..eabc4dc3d6f0e7d07a2d76aa0f45d2a56e1670a5 100644 GIT binary patch delta 369 zcmV-%0gnEO1B?TZB!8|@OjJex|NrLa?f3Wjz{1M;`TFeb@!HzjU0q!@H8p>Kf7H~} znVFf(&DCLJYfeyDs;spA{r$1AvFYjQ`1trsOH13^+kJg~si~>h*x2^=_M)Pqn3$OI z^73P2W6{yk#l^)oHa6<&>WPb!+}`GUeT6|oOGQUdWoK`Ce1C+zzQ(b$yOWije}Re8 z)ZB`Ul-k_nKS4>Gou$su*o20TOHEbS+TzK})6vu0Pf=Oj-{%Zkquu}j0GUZdK~#9! z?bSyP!T=0JQHKDb_uhN&Nl51Y7eiu`H3-=Y=3U^+8;=mt=hCT!XmXJFlknYl0U)~g z%HiMvgjL{rQ)&*!K&e~-5bKBWj2Tu6?>9E#aJN0^hl4ZTmTTqk@#F$Pv{>neGfsk& zNkrc$=EX+A6B}8t(XaDiT#nSBu?hD|p~* P00000NkvXXu0mjff7;Gb delta 366 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 NgGeh&RY@2H{09z(kemPj literal 9441 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 bZiL3@p4yiWe?Gqcu!x`}r!HG34GR4qADlT0 literal 10391 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 XU`Z&<$A(oL00000NkvXXu0mjf9{dqA literal 12210 zcmV;jFHO*iP) z8pCj*F2%8eLli|7 z3I+B>jYgw-z0SUBv)K&pjbRw}r3n$Id_JE}r?c5?AQ0$uI%4qGgi^X6iqP$bO99lYa;dW(--tSjYh+4Hp}I5$iIG%a=F|atsdRMJp%Vfg7tb0 zmnIYn(KWi=uF+@|iA0bIf*6m-R;yL1RIXO5R;va34u=Ds9FNC#yKS@CAVW`}uP&F1 z$K%0rzu!wF5(vB9P9~F~llgp}_?CYE;q&R?XWQ*oEEb_UtUA>nw@`IP=s)QwdQ~v{AHBL=m(-v=9*#(L>Sl(gsCQ z&;mgeQA8^=E!wowzC5()i#Uji;(~~vAPRzt3;LkqgRg?le3--hlE39JB=a!SefV>K z_jm8PXZfD*IY0en-MV#juB@z#%}<{`Ei5cFQ9oN-Tc=E!^6c3&kB=Tb%FN6}iyuCG zc>DHkB3xEXoH#KMKX{b+O5UVZZ9i56n# z&Yiirxtz<7$=mk!_8mKRAhwiKKy2H!YuC)HtE-WUvjB<`va+(U$b}0RK7amvpkw?y&E|u(j-F|o!$QF z)2G;0R#v8Ln*8?X0L0GD&J!n2K$vg{O-RAROi* zB6g%K4&{1jXa>04Bn67rl<>-FJGQCX%bkw9&;@$wzRaA z4r|x0O*y6Zoj!e92pw6vbm@#4Gt|B%OO~Jn%ABd13scj(tFIEJ6Bo*hmJ>5s`!-SO6!*{UD~&AUmWKM2$SNG49S>1 zdp0mp_gf52Y%*bR;v+)?hj^Gh^pxBEjvhVwTa`vZJbwK6(W6J9!7Kf` z?|gz~2d! z8g_XV-a~&r0Qf3wOop$8#P*Q{v=U6+zFlZ(6*|}t5X5fUw27asWB2$J;}e(`5$JJ+B54oO zB`s=>+wCXK6(T}y0=YEBb)i)fZt8_AoP|-fh54izgaq*)IdViOW%$B{3(+dNaVd_A z6mnL>T~Sftl^YS6PK;P;aK460eUOUe2UF0KG73K16&&mT2+%(b|IwpS<}wv|+qP}! zN9@_)3q#+dMdL)ZLl3R`_++6g?Ijj^fHqtb(0TaqAtum-MPnL6edKOkT5)l)Mn$c$ z7)_XRhTvVnq1vx^@7_rvH5VUj-n_Z0s*39`Uc9Kma)pu_2iw}((=&c`5q9Jb&UF^< z6;^+*prl|cGP;gar%nOcfddEl>-zQUys0ST5}G4vHrhs@3{2__9-^%vae@%)zBC?4 z4sM*nRYHY$*yuPk>vJ?J4Gj%iGSz@s7{#4#w@s(3p_JsDBY8wLNUNr%Mh`$?;orS` zHy7boHX?XtYPxLGlduI5_=-@?HJyo)FJHd2lOb$}JY9!y&_RD?!f%AYkOo?$bT5#{ z`AWRT53nG~2Ch>&wBT-+=U~bk>}7ax7R_aj1m;f%qmT*>TimDsQUfmOkAow`G8T$} z8?x~ucBg>g%5#Y!MTLMe8Xg{CJIDH-A({CirAf2x3wfvgJRG8b|DO%HG0a;|55_e> z@L$l!E#G_mzdmGL)?Wh`{P<7dmw|GSvELv6QA-(ruxmFd%>+Rh$7dzFb){>Ml4Il+ zfk2j7=L$JMa-5t%#FO}g2OCi-CK(wtcJuO*n4bQquCDH>|MQ!RQ|0x&cmeGW-AhN` zz4X!@x|d%5utUpIdGnoftkOx|ue}|Qd|UG3{DZwqSIM)=E&K(3&cBYn(d($@wjh_T7tCCENQ{Iz zNV?F0p)3X|Hb>aS+&BeTAd#7%BIgeQ0CF^_1Wb^P9KMT0EoU_;c7Sc8do)N9tJw#8 zB4q)$*6TGp08EnEgf&hQHW|u-`57UlfQSgkU_DZbzWV8SJd%Pq28+kd2&H2v{K%9U z(B7l0@{=71P@^R5pK3C=09MeIF|)uhqTr^Ub?6xnAi{e5o8+ri@E1xI)5nR_Au}pk zBM3u?(I||`$~HM3TUib%x$SmqvACJT&=g=VV4N}8oaM1L0EjbO?$F{KzYLn^C~&b5 zXnLVjDLj~*0MOd)c6c7c*}IPWEGMLgGGfTGiA6{F5J51C%Rum~LAwDHrpcxIyx;GI zTZ}3UxhPuDJ5K4d4F*Bc#Ic&MWE)VW%O8Ob467rcOE|A@P2e2X73oa#xgs<)pz4CrX7$S^pHyA4qu(-A+wfk15WI98`$yTTse zCp+L1^lUmTgs4Pxh?$FNJJl{TW zTs>L5R!3(p17>brb1`tojbA#pahgS}rjB37w`pBHn>Kl&%|mP;;8_V{My;)oh(aLX??M_@T(ss` z7pp`j!NS+pf}numn|@rUz8eTSyd=u(2twd)c8fc*|HqaE=8V~4qRz~P4x!_!|=dP zOuLYwo$jf=s{ZxwnR;LJQeH9O9l1?G%p^Yo*R-}urQ)-;5#lo_xMzuNWgfyW_z*zE zB^Ix-bQE9U#V-5E4=Fc>Pf-p%iCGcdu!3^(3a2Lu`cS1yjl*k;T8T38t15TWQw8L} zmG@igvT|45u{?p5Dxhn_5_)W{N3@LGnqt82+X=hE8(r)4DRu3(oJ~?%LX?Aqf!-6FR=xu zvV+0XQX_v$%Hz{ZSNC9hM{Q_$w7I2ib8D~h=bzrb5%-twe<>xCrA#4f98)> z<7e0cNK22I2Ema+U?ACr$6Gt>jat?NH30*VkaHuLYN>ttgQ{dH0UD(PLP%Z=#_HQe z|BCf^FouVrVk7ZZQT`B0?eLHus@^eO2MnhLfzD*HUiVQN8B{_!d`dhHBfR!OsF;*w zAazfbDKY`WXT^^vSi5yoh3^)=qy>XL1)SG-rn82T6a`&9FLd;mBhpZO|Vgn zk47bmV!%L5)GCF70b5CLp&)2srC2C-YD7V?Q2YZF8^ux!8^wP>q6h}TkVFzC#+MI# zaXjpO&&$2%-sg3OhlhR7nLV>+X00`Auix*?K1YR?yOI1qUuh%|%A9+% z5+f2CQ=#{elm7hlGuytBqIW(4+|uD!+#zdX+Ar*Lxoa+KfmaPLIA66YecJtkL zy>=Vg59A7@y!_?JrQD;?fB(lzTS4ZBANlV0zF&o7${+stry8kEWQIkX(#DWvY1H+- zki|Rhylb}%lDm@shsy)wa5M}c4P4hRPa!_P`R(uglh~HQm2myaSHC9Z42f0Yi(mRO zg!Z0$-v|Z=3dn6g^z8Gn64nmQ0p~(D)b!KZ+JE>V-zeX*C_*1Au@Oz>xGM z^RGV7O#lghGV77!XBcKgvx$mPL<}7{(#c&wuP99*S@=so!|nhyNIqgl6T+pPXfXz3#+_;)GC?kZa~i5TEnCN<_+`Ic+Y0%uKQc|N z5uGX+7!Jlb0LjSx`PgHR0THX#D&kJ)Q8n}ZO0WMx7Bb&=r6% zudfSqC2dk}bSuk0_&S3oszJS&M^rd}a?emV90{mCt+ncrOxmg41)ile0utjTwwBS4 zs$hAjds}(NAXvgI;2<=ZoXo)3jqyaCV?2~t%N{la7-Zu?5KD|}^9VK35l#>LWQTSe zA&~Zsx^HpCFl}})SUOw6j8RI)2_IzxA?GI+ED->wV>+;-RJPTiXYp%Jk1%}F3d69$ z{D0!@JeqwB+-N2ds&LfUB)~H-C;?nLhiA(khQ!C1#Hv+vnz@^=ym%)nv0ZIa zcR;j1;nKLKRwY)=h=W;?{~dzSIZxsQ3a5^13PAvx5piWvAi7v)rqzR2zocg{P+uA& zoUry>6a{L*XkiDvxy9({Hx=Z5jD^5RU8+^n8dDSlWB?0&GIlNsjdmePtFU?Dq$0!& z$~2CKK?33G%na{fu}nDY#R;LohTo0U!sj~(JnD480@)Zt+RgsezSU>OW(ff!YMSMy z;`Rrae5dd2E}XH&Hg_Hn}PnCuoQFikrd=Vv|}ho|2#7vGVR% z2R1(4C1>_4$*GC8$FJ}lHW@3*Vs$}j zm~m$jnFQBJ8dfNd2ZN!h45PE7ILo$(dXMtTDUPwPPgrd^w4j|av145fO*^DDOrnmV zHC`6$hgGh9C~(@LSjxC7+4^K#G5=Lt=mzJ4I+DHZC@nEdVCw9dDO#_hP2nH+KLk7W zK+NsR0 zaD7;|Jl&X@F4+ zO9MwJGEb~iH>x1$ce@p$pxhx}MYB100=Uo}zitxfJ@0PF_)J>H zqY{}g65`QI6TV=;37qTz`rLkAr&KZwj#(|QLDF|=WCB5vk7;XnA?Sp+*r0s;Gy^3zw{xz#~!-G z-t4Z1FT%9vn;ge{3igc4>Z6GHRhX^sDm^*L7|whz&&0r7T;IiWiy*RcEoSKXT@S$% zDV*j1%vGMg!uefW!rOA~z+#)J(6d}W$01BE$1Jlgo(# z%PM&Cy;9FN$A^QB-6$56Ec1Eb%ukg)ik!4Aeg-GO2?&&4*;Yro2QDR9BljeM75p)Z z#gl)PjsYp1s%wX-e*E!I079jcPb>C#2THGc85f5aIX($*iPf8qD70BpPvQwE7&atT zSE>~-vG<4!Sv=UPWaxTOi}$|CXX|V5tEBm*HKkumko!|=9LN$FF1J{!*zc=llo^;xWwHoeyM69Ru2bXc7yTeTSGvyirF=9>0YJ%9l$F>jha#4qK{}Gx8zK9OQvq zKnwegI<$zhkmRO$(&4o)0W1Tpz&lN^q%vm|j4ve&J#Z1phqbWJ&ISU4$|8fnz-n*+ z3ylGRN-2d0zLvKJC&@HJ;s6w8uV4EE1hABdr&C!SpdLNu{v_J92~4oc@bCxTb>C4h z4br{If&zqZ%iA}nJ4(Q*CtM3=kn9zCh4L9ZV5JIaLsTa=+B?M+Rd}Gro^zr)4N5O+)&&{D)noIx1yEvv zt4VdRc8qtt`i(c`CY^{d$P!#5g$5G&7I?|OuojpXK!@ZIB7Tl|F|)NvjGPWX4OdE= zqoxg)uR*?md&=Lti|;hC!RZikbIV^?bixB}RPN12CV_)*H-Pm}Xn?|@qP+uBsG#@UZGKU|(Yt8(YW`EyVF^2DGnh zR>x~}lx5|2`7!Mg{$&Rn#`aEi)aor{~Y;Qe$FrN*_vlRMrCQ>H!kZcb}2``&;X zc3sr0x#*%#`kxw6CVQgFBi=x1u*`$(Vo4g*X5%7G;fJrxvPL+h@!~TpZP%vgZJOOx z*y>(9mRjIl==ZpbsC{02#V5nzZ3>`RY!cFj-XCWZ5SaOJv!*tSH^z(2>#K`c+V&i? z%JtfX^0+?fwXj$N;?&;liD$xz#_Y>ZdZQQR77s8On;uOXT&3{>(uioR_9Zn{>*3X z7!lHF2p(e6JUtc&>Q3&$s@U9x5YddB zcnqi)(HDoGwW;$P;_Ouxwaa9<3~r_}gNOB~ll=r-v|6 zhyS(4CG*)lbffmCDdoBdY4NBajTBO2`IvAajcKG<0ydm)QhAOP7zh;_lp+Shg0e>1 zG1cW6X|RFxvSc1KE|*}P_7Js`NU0Rl@(B|~C!(yhS%i?jnhm=OJ{d=ACRsRXX_dzh z+DQ^K-!CNCupQ5HC#gs-R4_~i=@EtGF4XLeZ<@6<2FSViHHM`pwtMXrgfvAqVRP7< zYE6-8FM3$y76u64~1x5(BnfIv14ASkiUja(!pt|4L870Q!d;ggdj z@MYY}n~E>xgGx*)2_5L#Yng)~s!8;buPvD^TUQoY%3YE>^g-TU;{(+!X;qrN^lGe{ zYW4<$MVsrIGG$U}{#@52uy(p0oT0GUb(<2RKof{*=QCZ|0$K-eMGH%|vTs+UNKoP$ z5Xn-S`31Sk5+i87+hC@cZd+DGI-5uu=K4oV5a!1}`6&Tzx%E{yz2cQ?Ngn>-qkPBWb2It& z```J`m;b%^9Fis9Dr0@IuXO_lU~F*%bc@e}P8*L*ct{ncq{=}dkPE7VmPNx@XNO(z zYI5w+3oQWGp2pC7wktJez~C-mm1bSjb?#YrLO?56$a#nQO z@i8iMAHkWrAe(#PN*(-19vy{|X`fG3L0`Cd#zlC(U`W#ZK!{*5gtX{BSI52D0VhF= z&21iP2tz9fX|V8i)CSO%;a4G-Y#>%Mx-0?488_rKX?i3Bsq|%;bZXTM`9c(07{DA; zCPJfhaoDCW5+=19JGE-`G8zMkL0`s0ve<@(1l}8M^49Sp=m8uE_+-GeyhfsZStp8B z`xO%!j|cMXf*6qpnmhe2YMN*jXuMUopZFvrr2VE3Sk({)NiA9&gW1IJ8>1~n7t{q- zk5<9=Bcs}CEgoSkB!~;|MMoVm#ad7@kV4sHI>Eewd{l>=ojGP_qZz(p(muB}>B7v| zG=PZEIGkzC+Lw&N&a>uz#U%vPaREqCSVy$YMoKo$8}R0Tlj{y`$Le z4S_tu4-u@6cthS3n#dEG1eMQUp=qIwfI6Bi4%p;EDa{eY^2B$*r6NVxz+vvq;2EMx zp@=qmi|v>N5ep?S5yBFy+nWx>4DK3?%3$Y3p+GEzN^uw_UCU?*+F92WX!7pN!z@!W z6U0?;)tLmXqV(&=sgbp@W~_IdgQF2s--QvuV67`{roKi;RA{!byO-J6d1y9EAR%nr zaI6^`i3bK^&^#-LyTY|wY0lnCx9Mq<(!=T#aukNgbh$Bi^+|+5o7E7>d;*qvR!x~n znqjdxIcsMv)Zr;~=TU)$SfFks3_Ui|O-X!+%Etf0Q*C{Ph~|O8&~+14eG#Vp$D&Y)Dp!Ep+>M~z=jX4`3Yxa%6lF4HTZXM1 zcS2yWICe--O9S%d+!AXhI&Wu4R^3W-x>_(hT0<4E%ka(FH@if_a={H4VP@@7Y?Cll zRAbe3q)pRd`RR(jkPQ|!S)gwPWNMP%EtaR6dNEU$-&YH6kPj)VRk7-nkF~@!)T~m? zBAh;9QEl`EMdPqnE!YJ30^Er{A7>+td+C|onRlnJ2td`Ta>De#S>o+5>^vnmzyvUa zo(;OC0A}%+Wg%Eylp_!On#g)hVEA+P^wy%vUy652MVZpjl)>)P4xWeUeik z)THN_d18ADCnuyXJg_h}P$T6{ZB~}w0_)vCVt5K&r~-c`&=-F@uZS-Y2@u#`$363T z1}@gJQxQ_It4%FyU(q}56y1?QOvyvLP=_S^w3-Pv_HKKNjfF&~`!EQKmv) z=SAdT*H#WWUqMVI+7V(+=zEq~rb5p$%T(xDX1Q2$lCxccf7f5hE=kHjP-WK2XdYQ4 zh9q=4Q|B5gf$(xt62|4orD@kkFL-)0SKC^U^M+`YjJwgwP_VrPjQY?A9=!SHU1z87 zSLpt&zxw8r-$Cp7=*J#^_PH018ISz2bUmy~FP?I4*VoY{uAh{rqyI0%lH79ZYwmmN z{V%-u(zfZk{~Zt9e9No*{KWFy^DqAOndd`Ew-pA{Pqi;w3BjR8@fR+rj2YWoe)+3k zKluLlQRuS+E0W%lm+>OxJ^>t@O@{$U?@P{eDzFj>e_%n#78(O6g#QL8_=O*IO158u z$&KM^Au`hYHVF0Q=Vjvousn2$bUnHyC?9U(T3SkdO|2z#3o#-gX^^?c0NM1i zKQj*_I~FOlf9hzFuF1?rGU)u=K+(a+yI z4gzx^u8dU#n`sci$Q(hckUiF?IQYsK$pdI)kQ9g*{zMKTGlWw6VEk<;RR?JFx=EB{; z|1u2Yr5wx^zK#6Z#CHDRqQT6mW5)oNCJ>2$;zIIp8IYeW4Ff}IvYOi02;of{pqrj7 zTI1IMs20K^;(;U>I*?-+TyIk_WA`}>+@Rt!ksQVMScNwDC`YhR0V3ihE=S0-nZ*{M zf>xIr!Nw`5Ppmx&i0&5G2rjrBhzBO6x80z*n0@G|5L_cjEi})C`WB$m-%KLD2W_y5 zuJIyF7L8m>NGjne!E6G8RP)Xky2Xtox3o%@O_)xObfLj?-CnWaS3!wQH&O?wP*7BZ z5BnV2FVAnTX;{p@bukqo_PrX=?Nrcowe27R*t@m7xT`T5wc80lL^a8#iyk1EG~pUw z_Xw9Z`FI4K;zu3rEiO163c<@NYQmN&pJsVqi&CdplejL6Hl2*YH<@jtPz?t|@76+V zST1;!VdDx%h8fb!2x&iVLWfX}(q@js!<=|Ay;JVmF**GGHU%^11gPtA)@H*$mdi0n zGmB4R&ydU-_aSYrW?rOQx=^}M4fHBbAbKry@1VAr!5ZxRp7HJIaZzxXcyxTXwoxmD z7_ISfV@>tvq6MGRh7-8})!+P+d*HCGC2d%Xk?;vQ?%EWa-CBEIjWO>yiZY|^q$S}5 zFGacIjwa8#Y<8ptzKyP!1S%fm$_|5=5wn+1;VC@A)?h2O4xj88Mw+{={jt!CUm;TNrW;1h{4GYVMc7&p< zURNwcdaK^Sao0`3kQ3}_DcepYb zSau(t$+1RyS4|84w{O~f*C+GZ{`geZ?6*B+uxqOvftyc?#jjeFc0I;kA~W~7c>kpR z*8p{=P4n)$D7@EBQWVXen4b}2&;X&TA&cXUo%Y*^1-OM$riGVc4A{wwbMew(@>4ra zp;hr}!ewKp&~(n85CPhzzpzQ6ryljL7jvjE8vb5LJAw+JdKxc~->NQ59Z6D3n+M!U9l+RVm!ku$w?w60mzjz1l_ zwoSqKCbL9&Y`HFqH!dGJiJ6|^$CXxb{CUhzLHYZ|joYs4(HgU^)5@%{u?sq~gC9+{ zgiPA?_2JXoj!|*FSgbQo>A07<`n5;Z7NRbSEEY{=KOz&)R0qnyb8$xOger3Z{KEAK ztT5b7wu;f7;f)X+b5EZN*Sp!Etz+AbToHxtnp3QPg{wQ240eEVug7K2vi70_@8!KV zyH1CWES`5qgW}*rZsEoKjO|3d1-o@Dk`sF-?I-)q;PAY zi=J;v8lI{MS1w2-Va7E#jr7bkilYu8;)*K-IRdc5+wb6X2{C)=guZ8)T#i|0nF>A2 zEY}`6X?F!V7x<@4WQ?41FCl!MR6I}kHQ_j(M~+Y4&C|xVyIyzaZ8yzwgO&X!`2YT? zr+26PobBMae81F{jD#>BZVMGh*7j5 zX%y*t=mYFP-VrX8PcG0hZyo{W;Y$FVs3t-JKvEtTG}zm5 zKs$gskTu#0dH{4c$P^;oiB;8v--2G9#^76MlUI|-B1=!o&P1o`Jep1?sz?m66J8nY zsUsXCA%_#+vaX8suoO>>JY*6IEjW?>oJ)ISmOU{vRYS0Xe_|B57F~3T9Bj7ptfKk( zmev9+sD9uliVp}rrVAwle$rP^Dvjq#9eRR9f$UZVaHtQt`OeZ@oMWj+%`yguY1jce zl53nsB^q&l1@lk=wgmkK4!dP8?NsO!3Z*Pa7{}Prr^Rd3dK~oU=2=w>nofMtLB$ej z)LP++7BB%2^u}?6eMOI*=rf2u4nwg~AnwKQ=|e09pMazIeqCU4A+^kPogv4`#eAbg$;|`kkzI>3kkr1V;7bMT6y>83#2K2zGs5#u&Cl0>f{JQvbj!`vs;soUzQ&M}n!UfrWM*!9e1xc~vWbh6 zjgObg%+#;3xm8zRqNJ?U*4|oNWI#envb4OInxc!1m1t^mjEDV&o~D6=i%(HmbasAye}^+QJxfhhP*PfaeuiUZZGM1=R#;$8P*_w} zUU+(fg@=z}V{5m$!BbUTj*ysFSz$LhK}}CrdwqpxX>m$SR4*|(LPShRN>X`ygGNYD zI66WV7au)8M=mfpJU&JrAt^pUNGU5bC@L}+86h1XCpkMqBqlCNOH&dQ94sz2*xB70 z93&tjDk3B-8yzJrFE{7t?9|oS)6>;&Z*l(q{`K|uCn+)U@$~BJ@cH@r78oENASf9d zBO@g({r&xEYjgAS_WS$%OiorREHp<+Q6(oYLq$zLK}qE0>ohh#QBzwXBP%jAJSHeG zIy^)%Gdn~^PG4bZrKhiJZFR7+x~r|VQdC@6T4G&aXFEMba&&y1pQnO^jBRgsD=jr} za(Y}|W^r?SVq|Q{%F;zgPrJRvk&~OFrLA^&fVH>3HaI|DU}!-@OToj-mYATYsj-@z zq>_}JY;Ja-qN<^zt95sOb9H@}nW3|_z0%a&m6xApXmHTd+SS+Jo}j3vsIaZCw}*+5 zwz$AxVrsFoyKixMxVplBfr*cinUj^Bq^7QSd4ZdqrO(mXnVX}uw!Yil<&>75y1c~4 z$R>000cZNkl8^H!osoZ0qzAzALEH)AO^xWXCqu81$%-3 z4333|*R9RdO0LE)6fMs!ES)kS)ps;cOpbDhgewr8@P1NLi-)68?to^$%@M^{eAHqGIY~K_-@mBxiXZpY0RYO$$e3*op%az=m z#Lx6H{BTb)XLD_ICxpPC1N912Tk$j6!Qhxof&C^yLrWurW4!zKi^FMeGkSu$TX6?6 z3wm0poVKv0)sb6mMO*>LeI&59bs6V(j^$Bk;-^gJaaiatV#+0{Q1^ROvhxN~M%RTD z44Q)L!aUX4Wzun@w{2=DcTm#Pv^F36igKnMiV(g9Xx7C` z2NXj(rD|JeS+OyzZ%1*qG3VHd9!X$%z34KwkP!&p=)Ohg+HCRe&1wA=_o?kMz%n6m zN_ncGW!0vf2nNWF5gs)U^Mvad-`PBnVA1!VE@D(+-uGkRmCd2-d25Sb~Fz!g$k#=}{CWscPO%J_64cvYOU<*b|jzyUYLhI(7Cv0+C1D93)5LM<_bhr=(4z?QFv zA1{etPB;>X{Tl(Ve&&fKQIt4m8@N8;o}6@}7OX0OGV(`ruXS+-#o%Zhk0*HkKy+W09g1h5rU?rgtr2KA7CbS>0tRr`d7Xa#Ba-x zqA@QWtjMAywq+Im`Gzfc4lNyAR>HhuTRMKYiu3T&!I#%iC?C6tpV&%LnwAa@?qPOK zd7fKmxaJ{scIja3{XEX#Ttb*W9@Xsq;~JT-FNtp>!1EPX@M7@HIu(l$=q;^Am6eIAJhxj~ssP7iB=A8^x#8gp`t$>y zFBh_18i}%m_I}bb`L(>)m|WC6gKhfKes*o>KQxiQJaoibU2z}oKNqS?1N|kpeqslw z?gT&DS^i0IeGTK!aY3$`V&RuEgxQ(G-@hb2kj&Zat6l9$a5Mnh z$}#0Vx+Efln?J#{k+%Y*Yv>u_iuH{1y45QUv^=nDC4{ilufnL(p5ugy3LKSu1@8&n zcE@pW7O+l9*OKfh(J*?(SE{C6A5C~Ro`It&gDa$lz&f&j zaVeM=r^~lp7}Y7!?u{mFY(tgK?c+?&RxoO$`MuN_ZNqs|{fGe|^F{oqiXKHzAT-Mc9IMcEaX}08U&DaB+y; zj;Ah(Y&GHQe&I_(-Ww-9I9vU0hBjYnOG0e52SYN3Ee~v;29tewrGv=3NCU=(z zQl)ex#GH-!OH}}lwjtatRY*qNy=sjHG6D^h#Lg%6r#*Sm&yo#^{Tg3Hx=`;G#8 z4(<^$RJ$`88vCb$B|gh|T-}<8b8z5@xy!WIa}9y%R32B zlmz=rIeCt+9$XSCVu%2i+=q4Cn|}y*_of8i_6b6P<$?X_JfJ*hXaCBSO3bIJWug#2 z>>aLe^Q{NqEfb7Skv~W+Cr_c%q_su@Z}^Gyir;J2NiUz(_T=eripEsk{A_87mjor0 zzO{YQ)s*yer8$OtycYAbS%jX*7UN^gR<%t`P4K@8r-EN68 z>^NT~KtS|}^FsniWq5`{^Yh}AC(5vO3yURBAjr}6=3Hj5TRgG-)f$K&$d+AT0L;?}6Bxig*E zj@o6F4kXcPmpH6pKYJV068BzTf8Ibq5=?VK!a@iLDIkVxcN23EQUHdhLxgs!3CW$> zP(T7gIXbIv+X6iS{mNCO=x5KIibFTu7Vkr!rm%JsxHzDiN(u+pZYWIke%0mPGG24f zoLUseX}IC?C_piCdwiJW?)*L<;ox|$ns7?6nuwNi%6E>TI4NoS?4gt)Bk3LvWY%aK0G4_{|?rERjwX)!C*VIYL z`wOmGPo8e26pDRZY4gb9$@p03cQT<3GK0*s76D z+(yzCnC}FD9|Q2j?8Ox-YIQ3V*BTp4&zDa4NTCFA#^5m%e_&L zpV*?REd<#MevVsamgA4c50_{V%q`HHaARWB&^xdRKMAWRi-3;={9Fw{bl}~A*vl@` zlyq2=KUV3k5B+rKfzv``R!QJ|{hgor>X(ga-0vqw0gi>8Az~r93a@JTG$|%82tGH? zG4BFQPExJXz6kt!a(&wyk@efx|L^#^*Dc=v1Jc9yxpa7vGynhq07*qoM6N<$f|5bq Ae*gdg literal 6423 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 diff --git a/tests/ref/link-basic.png b/tests/ref/link-basic.png index 0d2bd75330140c7deb06e5ab9a4be67f6a339aea..f53223ffd87375d2f87802dd4c458b8f4636c21e 100644 GIT binary patch literal 5991 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 VtmX^~FIfNp002ovPDHLkV1o1PAzlCg literal 6240 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 KMNUMnLSTZ67n&~s diff --git a/tests/ref/link-bracket-balanced.png b/tests/ref/link-bracket-balanced.png index 8b7e02db23dd0f1a940c3f0c65d86d9158b2e397..01bfa87973c1cd1417cdb078784ce8ba3fddb5b6 100644 GIT binary patch literal 2506 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 omU1LM`}}U5%f2Vk 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 Y0MK@LJ)eXi0RR9107*qoM6N<$f`tGNd;kCd delta 951 zcmV;o14#V!2f_!C8Gix*001~<+;IQ^1BppQK~#9!?Uw6LQehm&`(v_t(WxJu{ zQmfFUvRYY*7rZbPMI9m_A}A@|1x+RL0D_l=rX!i+@B+hYq=I-UG_x7HIYY&*e(P*! zE|Zrn-1FUUXW!>{p66`OXW!@9IrJ0hpXd-mfIuVANC-3njekHRA8B?{=;u-!2nvhbdA*k1>LEWtNkAI_V#vQU?Ak0LZPVD>ebcNpr9a)Mq{_z;jKs{YH4X9R%x4bH*Zmh z6P=q^1c-I*25K}NI+ap+WK`nrMB`*rQnTJ*&0y|5h`BsQ-hZA{=w04n-lu_ zE>pnKlMw2I09q!KF&GRm9*-v=Ai(W*L$sHdmw&-XrBV)uqf)8htynC^Fl=LEgSew# zP!r(NgxtZl^F+nznLL1a;Sy=m{^?bnl99$Hpuk&Ueu-^j7H$K)|0E&?;!jL~AOl>u zSk9fl)ZWnzoxXt)_%e$vsI6B;$0k8Xtj09iIkTU;p7=U2nh|vpM|o3K`V$H#c-TI=gcC z!jjTTe2CZR*vD8}qOrFhd|A0Pzo1Cn*bJU1OsQd{X3Whm44Q{%arBpkC9`v`%Bq^` zntI3dTuf|&uC3Fx@D)sRi>}AmS6ip>dN6m;e>D1!Mna$wXe0z0fkvQ_5NHG%34#7! Z^a~*ZhhP0-rab@v002ovPDHLkV1h=6&wl^_ diff --git a/tests/ref/link-to-page.png b/tests/ref/link-to-page.png index 2dbf76778ac7a169abf2bef3b407ded9400b77a5..d618f066b91f8d8713610e51ed0d8aad06c439c2 100644 GIT binary patch delta 870 zcmV-s1DX8Q2mA(*BYy#{P)t-s|NsBy=k3hS*2c)tl$M^!%hTN6=8lkMm7TS>zi)AQK|@PnV{6&lfK zW@vC&T4H2oZnL$$z{1LSdV*6`T|h!gb9H?&Gds}I+RM$=#DB)lD=jslqpMO>T%@M1 zUtwvesDVnjg=Z4B#n=kjE8`Sa+<$2cleH-FHt0#7X~ZI_&%eejy!Rr_|5VilG45k*!DJ zf#C1Itb^uoWCq^U{rXw=+V=cKN)3op6(%6xzWVWfL;@UgiVYmM8D2&O!qoEaZAhZn zNMBo$vWk?pu(8BaSj7B-_mx^!9!xeiamx(+uWt})IywRR!cR&(4=UZRe#>28S9!!< we1ChZ&^q64q_i@jf3CB+o;!VMb-Igi2!BPFGKh_5>Hq)$07*qoM6N<$g2X+-g#Z8m delta 960 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 diff --git a/tests/ref/link-transformed.png b/tests/ref/link-transformed.png index 4efa32f3c3fa1d4b170f1d3e95c1fd853ff965ec..c391f080bea18465a3f7915397f091531e5818c5 100644 GIT binary patch delta 1220 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 B)F%J{ diff --git a/tests/ref/measure-citation-deeply-nested.png b/tests/ref/measure-citation-deeply-nested.png index 596c351ebef651b56d9a9d4e673de6476cd0db85..6711fc732eb40ad1c569f1f589486efed7039e5f 100644 GIT binary patch delta 687 zcmV;g0#NbB3w%F=Rld~9xZ?7Y+N!++P7n4o!kgUCOC(qx(Q z*5vTX+tzWRJAL}@^m~1U(`A{wzQ!XZEzQr@+}`Hhin8v)*X_a9&Q_1;uF31U(do0z znw+G^J%8YnxPRrN!pK2^(`TCR$lKIuoWm}4*?g$RID6riy8G|+%Swl4XmHBR)RL5( z?!?&1MTGa^>+|#W`T6?&{r&8{)BpYcet?L!xWM$-<(QhHsjIWbIehcf;{EpddVGYk zw7lKl=lA#dF%dOH0003aNklYH;gmvd)S%p^vqs%ArgsR@~meCW)GYY(ma5rJLhegB!Kv-|HFB3RYs&KMj8~QuBjyZL{J;pJ90Cz8-$5tlB(cEH(r^6iK2Sri59(uTuIs`ls0KQfV z@2$lFf1rlJ3yZ5;ChTN-6f6pN5VLNlVWajB+$h#=Js>?{7Yf*O3F}GxnIh4D=nF1k VQ!KH&%+~+_002ovPDHLkV1i^?n27)Y delta 687 zcmV;g0#N+=8p{`&3n z`t0)Z(&7F1`r(zi>b1`P{Qbo>dE%SB*m$Pbb)@#*>C$7E&Q_27^!Ud;f9>w_-;T8D zvCEpAr1RF~;g-7Hin6-9zrDZ5T3lq)WtsZ!^vzL>$wPzZtbfSrw$JRm)5t%7?ZMXW z!q?=Wz|v%y&{~r4%iV*7h-++cO;1;|wY|K)#v>&ywz$B}&)4SW=74~Jd3kxv%*=v= zhrhtYtE;P-nVHSa&4`GIkdc+KvbOf$>f3;;(O#9)W}5KH+r~J1$2)!0Xq)cC*Vb{N z$wGqZv(4zP$$#s)(f<4V=&#Dc!otD9!O_vtl9ZfgXmE~>j_K*?<)gyjleqWd?ECNZ z%SwmYe5lY{lgL4V(`TCR$lJp%b<}B``}_Rv#MsG2g!kd=_1);sRF3Su)BpYc^w{Ob zIeew3uZM||dVGYkw7lV$y6wQ#_xJgs<&BB}009(9M1Mh4c-rmM#d5-66o%m!{vg5H zQtCq8-QC^Ys8OSW)&h55CJfVDK+;X~EWYhKXLB%_Os2nc8L|Oz8ahKC$?mQsM4!k> zr_;6N*||YgD5J`JUkD+_#)UZlw4jSbU2v1sbA%j4SzmQ4O86L+uOJ`(5~5~wM2HW) z!oZdl-hatkTLJctV)yYcvKdQ*p00e0>G< zdXOIR_ZS(DzQvrf>@0G^;Q>%#1xF`Hk>I0%+6RW^@9VImgS`khLQqkO;3?CHz<#>tR-koOxgfASzIu`smb)e{QxAJ VGDAqH|Kk7v002ovPDHLkV1i#BqpJV_ diff --git a/tests/ref/measure-citation-in-flow.png b/tests/ref/measure-citation-in-flow.png index 18617beda04614c0e03cd01ce9fbfd20d2b0a127..83f92aac4e686fa740b4b7a0ff30760a6b4f5db8 100644 GIT binary patch delta 642 zcmV-|0)7421=aFMd{ z=;-F>?dRv`__ z(a_M)&(F`t$-kC2v$iHU}YkcWqdgociWhK7QKjDdrTgM)*Afr);8etdjQBh1zR!dD)lW+kW ze>potH#avjGBPnSF)Au5CnqN+CMF>#D2)IB0Ub$1K~#9!V;BXaU=)l3Du5USaHXro z2?*d%SBu3EK#;x`rv%D^Y}JYm!)r0Sj~taP&Ij6RWGyBpnFX>qfEh_n*_c5d&QCI> zvc(x<28ZeRxYVR$pNC3-ohFlQPK=S zwq-Q6m?4kXFpZywfq}JrtyI6)CRlXu)4r$#2fy_ASTdWcstOl`IP{!Lr z$q}4Q@9*#J z?d|OB?Cb06>FMj~>FMa`=;r6`=jZ3-SWoSd4PnwOcOnVFfGn3$KB zmzI{6m6es0l$Ml~l#-H?k&%&(j*yLxl8K3lhlhuThK7QKjD&=SgM)*Afr)^CfPa5~ zetv#@e0+I%d3bnuc6N4ead~lZac&xJaBgmHVPRoiU0qUCTvAh4Pf=M>QBh1zR+DW3 z8-F)9H!?CZF)=YJDk>)@CnhE){gx^I0003XNklv!OpUekW{9M%#d(4ZPUQ*=3>t+%jupsa)@mRt z(w#Oz9i9ZUxKu8X!_Az5fu%}ERm3@-Ay-?d*qmF*uuwERfw~s+W%BVU1_!I~@la)Q zF)*n5T5Gz2ldoie5u=w6zqSjbV4x!tgR#4|0RweGDL;f($Y!7~u(am|QL?tWqZQXE d7zINK000%rI%GYlM(qFq002ovPDHLkV1m*LUTpvX diff --git a/tests/ref/par-semantic-align.png b/tests/ref/par-semantic-align.png index eda496411e511b17f950f54a2d827e07af63cc4c..202236efe12c7fbf6612c8b8e879a9f1a62ac7db 100644 GIT binary patch delta 3099 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 delta 3076 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 SSXqSt0000AiPRQN)5^NxsR>+<7xN8h+4u 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 QMF0Q*07*qoM6N<$f=JsB_y7O^ diff --git a/tests/ref/quote-cite-format-label-or-numeric.png b/tests/ref/quote-cite-format-label-or-numeric.png index d1dadf0e298bb70091fcaf2e71012ac55d99d387..22654a0d71815c7a4a75eed58fdc80b187b4a8e7 100644 GIT binary patch delta 2131 zcmV-Z2(0(|5a1AyB!4GKL_t(|+U=R?PgO@0!2M^Mero*Gs7;HMMHB@Q1rbpeaba-* zMMN}M*~FbtD~ehLR44^m6cCVAc94AsaRo(jR|J>#m!4$uA_l_;&3oxg$mF~;bLZaq zoipd2bD8mvZr`NM&`ETH4#;~gCxpFMl_#hH&EKVG(M*=IqoSg~SwczE*ArAwDS zyVS_Y$is&ZpFVx+;NbA}K!=2cR8>_O3+Ue7-km#l;*b6P{cQf;y?bit{rmSF9UUJ& zd{7z>4-bIezkh#U__nvV%jk|BJ03iEfWQz}$>fQN3Awzpv-9Q4m)uCLvv1$N#Kgom zZ{DyV(C^&2Bl^2{@6xNrRegPZWo2bAUc49>7{JmfM@L6xK6e}%8sbN z&mN|oKYt!-bLPxJ#t-)H-CJ5(8W|bc)6?VQr;CYzg^XJlkJJ3FJ{n8MxP z-{0s!BY(KNyVLXNV`F17#JS7O&0W2Ewft0b)~s2BpSrrbBA-5eIxjCzK*K9PKcAmC zyPB_hHeXXygCJK`R8**;z`#JP0(34fEG(2cuV23g`h^P@WTreuQc@Cg5SA`o3Q$lQ z-bKt0VnUqU(TG4-R#vWEyOyJ8V`BrLl`B^&zkdTIrxJYfqQe34Py{C@Cr&Y&?CR>m z={Vr)*RMy0OG!xy?k1OG3{C(mLy#3>Vq&m?4B`6;2?-npZYq}J$P?)6>+8$Q%N6J& zM~<+pL?J6HD;C7%x$mP#kHW*lSsC1%q1m%%V`i2@LtF-~pPwH+GmjrXZY-d2+oGZ( z!ha||k4uk7$WUu*E60;3=J@jy7L0>*doxGg4ZK3Jhs+^@-@0{+Wf@`uE>c`vOtOK; z@Yt+BI5?>D=r1&4$iagLo0^)46}n*gx_^yVG^a#YhPpD;&1gY$NJ)>TbxBlI6r}`j zFy2U0Oe0lTw{D$eGLsuGD-z1ddS1USE-oY+l2DT!t0AhSckkYr9%w#OP=g>$7j#NW z3ONQWr1+kqg@%So!8y52abGVlFDWur60Nckk_rk-G9ps~9UB`P6cmKVxw*NWJ%4+4 z%a$!{gG{cVpn!6YWNXu=O=P1aRlNNvy;O~xnwkn|G$h5y2M!!y4plE{bxuwW)n`4>EaM(ti!aB-bn+9UaY?AiK}Z%;Xlx-B_0nN6-BpU`X8>1))2nZmh^?&vCWn*@BcIZi*QDO-w7=mtVYaAUx#C$CRx&$Sv8$jdN944w2F$@+3@%HW8306D|HI6BP=5@qp8XS$| zNqt2;C1P;Eal>=x&QV-Z8RI+bnJ*G-n@UeEq-=vMP}sRdoAmT_^ejTO!4m;T3_;kn zYnKes_!xBg@@0x+aPt$0+NK*hfVA6e+{~C;QnXjs+#7s^eeUX zZK-KF(^`oZl*;gN@awOrWNF`%{-8-JJZv)R0)^XY-x zb{TFSUO%l?Cc!%E6(4TqeTyQBZ^`l-^fynGR?=GlL;e`gmD_8)&T5uk?p z*7kn-g?3K#Ou&&Z=YJNfOQdgV>A|mWGe& z!^~ODt-aU@G=HV#bs?eQc=s=JZRpL+=V#><60eztj0X8)YF6@v4A!x*TEybaBm^nY z%IdR!vLMQvHt3O&$7jx_NYG^SvbUn*DjbdA(A$5H2%z-%GI|VyOgv?RVE_REqm|W7 zVv=hm)d++IluV{a$;u*E*S2sbu%Zg++=U+KvYM~|HcYY?wJf-iBccwQD(H-iO#Igf zljZ1`*vRN3pQLvjBQhUd8S2VVHw{{Y)}VFMpfzaSH0W>D{soAyv-~P>!!G~;002ov JPDHLkV1hvd3ZDP~ delta 2157 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 delta 2883 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 diff --git a/tests/ref/quote-inline.png b/tests/ref/quote-inline.png index c09faa3a86db6c3accef91c0b6bd06318a1675b3..9205d68392d88d7715fa6e517d0e6f31c07d7b1e 100644 GIT binary patch delta 1458 zcmV;j1x@;$3&abMB!5g$OjJex|Nr9R;$L51=I8C_=jZV7@JB~S`uh6l=;*`4!}b;82J?(XjP_V)Pr_}ACh(9qEG^77u^-ue0YNl8gcN=nw&*7Ni8($dnm zx3}Tp;b353d3kxLsHl;Vk%)+hqN1X$t*v-?cz=I?oSdA1fPa9Uot>+ztEHu-o12?f zR#wr`(W9fI%*@Pzfq|r?q`|?#zP`STi;HS(bXi$hMMXuHmX@-zvS(*!)YQ~bQBh-K zV~L50etv#^eSJbgLi_vsTwGjEPEJ@@Sgfq9%F4<(I5>QKd`L)0)z{xwS66#`d!eDB zRaI50th9=Zlz-UT;*5@#tF5)HuC_@_Q>Ur1Q&nA?ou#t0yg)!euCA_xgoK8MhR@O2 zRaakmdxLIpc-hZAnmzS4{ii$ZoIYdN6US3{nYimU24T3Vo>pr4Cdy?|<)baByyJZsg?TY;0_pn3%D#vADRn+}zw~XlUBn+T-KnP*70m>FM3w-SP49 znVFf#$H&;%*yiTu>gwv%)z$R$^!)t%<>lr5{r&#_{_O1Ryu7@Ridu3200T2gL_t(| z+U?d=ZyQ+vhT(S%v7JmE+u=CmFeini+@{OC+kY-IGc%WYi)oX#VRRg*A!dg8V{2Jf zs@)5#k}Fr0{77>(pY-b793ArV^76>u$_)emQCvbwd{oc>c&5mxq!>VXhP!ccL0bDk z$sLK2ba=ahls_OP=vs1;QT-gag2j8e8{tUUaYS!(tpJfw45(8?iSLQgq9z&vRVY?f z?SB9on{;(S0oDPkr?-~66HExG10Hw8uPg&as}J7k($%@vK|oiQU?xZgjw8XJZ#x06 z<1(;g*IpWX?aCJQme5TouFyTyc`E>y&E=KJnb&5A(spx~;);3YyHn#A&V?T>ee3O> z@S1G=YM8kZgv=uu`f1HM@@^`X|0Ka3>j<3p+kd= zi^Y}JNf@*Lu3?xyE7!xTsYRoIgf$-|KN-uV6;_F1ei^6{GK7!)6)A|H4ZytHsi(Yx z7ZR(6_UhdR`{`2<+YzbM@2`O^-O2*?m2E$vg|U+cR99oR)t?SWu7{pf7ewlP*MEGg zmtbMhQ-Dl(c1o+d8b59iWeU$pRgzlrgYkk-N12q696X^cmyG%v0Tfb5s@3*GxK~L2 zstV7lVSRu1Ew>b9=>$yQ=#NT)Z6$G^@I6!;Ym+NO^{mS+1vsPO-kSXMGb_1PSd{ef zCq+etpO;U@#wRBfQ!ghLO6nxum1CK0N z;{L@&-O%EA*kT?pe=2m~@B{$zO``bnOQN_b+wjCidd4QdX*5YUQzteUAAbXS;=kD} zG(SI7JG*FEGB=y1`u=7k{7Uv=ZTEx)Q)}ew25`pC&%x%}I^b6ZjPoi@!;o$~xTEKKbp;QX8IS@4DuEqZdp^uL&s5MxX2S?|i(xY2i=2UCq?@^$vI% zNQXc8@S{{Xr{rIKDJH%5ek;zypMN1H9eh)s^D8s0F#mAO%e#;M1-wcAMj#&85dZ)H M07*qoM6N<$f>`GzX#fBK delta 1419 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≪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%0D39lxc;L146rmSK^d?(F81; z=LPg5uQr>_D%|)_cO5)HAypxG0bN!=7tjTCSpi)@mu>7P8m>=cExjdq00000NkvXX Hu0mjfnwp}d diff --git a/tests/ref/ref-form-page-unambiguous.png b/tests/ref/ref-form-page-unambiguous.png index e7baa2f2ca7c99f8b6edfbe87486662b1942eda0..3b37f115b88c380a360f39b7d06417118501ade4 100644 GIT binary patch delta 2923 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 V(Ds{Z-01)S002ovPDHLkV1mQ$p!Wa( delta 2853 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 D37&G( diff --git a/tests/ref/ref-form-page.png b/tests/ref/ref-form-page.png index 52fde86d742c523fcf60dc5b6f6932236dac92b6..0cc29a4f1ebf1f6266cc0212249a0c8597dad4a8 100644 GIT binary patch delta 3559 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 delta 3590 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 M07*qoM6N<$f;ONju>b%7 diff --git a/tests/ref/ref-supplements.png b/tests/ref/ref-supplements.png index fd715339f7467d29b3e6275cf2c63ec74f9eec06..e400c1feb523adfd89e1f09f9e7b35712f631658 100644 GIT binary patch literal 8167 zcmVJ@ZNK~Z!oyuGn_tg`hDyCKF;@G{GxaWm<)=CfErDWelpPXd<>}3)M#on1vUD6 z^yusLiYH?D`3_n%{-@p@kHWqbvQ8o%F< ze8}Z;>2$i)YN=GJcDoJxg25mJoleK?cKdw3SS(g77L7)uSS-dtl}bgc)#59djua}F z%aKUL>2%V+;C}~VM5$C_i*ZRdn@xwq(d~A7y&jLpi$)oMkUxPxj4M$-VV*K06~#Ul3}k>O$l-(a;`@fwiMmh1z&a8VS({UfMor8)|EL&@?87kTiV|a zG?)qrZjp6aQC3&0L~AXAXrM({g4{$vpdeB}0vj%+P;MenE-eH@NHL^M%WeBhCpp`+waR}^PO|%%ON5=4G9e*ByN@m{EhWOL&7h#5j*brQ=kPag z-rU&ONManS@YbzcxdWl2;}Efeo={~)+3ef5kI+G|Cg#vv{x2TEsf^qzaPd?+C@cEo zkt#r0$M(epB1FVN%B z?|q$+cp}0aW{l7qBJ{^1aKcZTW3R>M7ZlYuw4FYERzS}eG^5h?L*B!~!@QconV6XT zWn}cHpC2_gJrvLj4BFlVw15`S0$M-|XaOyt1+;(`&;nXO3upl?pdC&|!r9sRiAy?| ziR$F+Booo+WuBg%yd>=F>zggXz(D`K;=*0tJ4M}KzSiB{os*Lj7Z;b2k&&C5d+#s9 z;T7%GtFc*GSv@^H^3HI@Y&rW#@E+(J_w15^9&;nXaKnrLA zE%xsMjYQ3vGiNSdyvU;B3l}c12hE=+o~6d;X0!PCc-ucQ7E~Jy2AsOOI+mW#mW+pZ z4-XFquF*a|KE=hwuseSIIPwH2q0Et(Np)UcUPwgDmVia%8#itQneC~Kkr0GeaClN@ zDfJvFS=#Qvpiz#gsHnhU88>POjJ>H5iKeC|hJl#xoSmI*KabyNG!_*Vsm+;OkY}Pz zAf_;Lc{=$pM;#@F^z?Kbb~3uX&dA6}=07UXG$wCvZ^{+K>({SGd4VDi4h|-n zUaxO&Z*OU7LFz`G#i9+4n$2d$tz2DQ6B82=CUTe>&BgG&Wy=<&FmdI|73#013^o(= zOk-%&WHQCX#Gsy4SXij-XXp+cA#D?s+qP|kY*JDZn+i0}(W6KCC)$6Y)X7H`8=KmG zcX#)cloS*!Xd-IUF+zvGzrX#W5>;!^1fw1ylf>BOwDbs5X)&5OV1FnyG*mf=G5PxX zYWusox(M9L2b`+l)a#Lv5&V*p64eb+OcDYpPye;R1|R8gAY4ovG6l9Gp$xDk%4%H4{M5H8p^MhK?CMGaV^E%5U+9i9C+?hJ~vll2D!LpoobJ^(w%AO%XlV)1ON_u+M?b z|HuPNU|=8-kb3}RH6gyRnNw8>^zvkA!&rwxAH}^F&Vpx-+9TNtXfXjTpary;fELhV z0$M-|XfcNYeYfs}rEvH4%=fbjK8VhEFZ$N@FH*NBWPOr)J0!bES5~F7)&*G~oJ#)o zqsv+ErdA%PXgycb99r6{t7toEeR#g4DY&fZd|5-(S9kp5a(2eu^2#prscQQyKJUYf zySsjPp!=>xS8(62vi69zAuy-X^IGOx*KfOreD3P|={j?ahbj8)*qgg;&)t?RdhYgU zuz%GnE0#OkoSas#S^Cn-73;>QMkgi)#>XBH{xLc@II(JKY{P5syuM-G<29>nFQUZz z{qS!;E?csE(WKMrm)~5nc+u};{VP|z`i7g!eZVfGCpJIQjtO zzG`$1-ukEzL~XNG#qCb}o9~{RivGvn`cD8IMm{MZt#e)H`D_tE%jFnvr$ukBi+;R* zbizJ*N}?arN>&6R*|)QmW^cqT82yIN4ZyE-5G zo=C0Llr_tLKga%H%1$j@Or#(E)89N9zI^iX?I-QlfE=GjmV6E>K^I#xr6N5LV7M0?lW_C^&{gx>g3;)H-IgYXCM{bnXs*}&|tpKz{o3=j+ zf`XRCvZ+5>+mCR}G16pdr#ZjhRB!%NJD<(whx9kU_~nf)eSVmYj?Z!+Hk$?m5?)!? z_^qFR{Z%Z-rV=4G#h8SVl@)!%K+Og!i<7Hi)a^PcxkMDGmiWEv)zkOkbQ$d3800DX z%@gZqf42hAv&gdvA$jOd`gSNuLM_fxmV-2+C8h!intO|8S=#!9J{eh0pP#zka+Ji2 z2t5p>jmG*H9ghf?Z$8>On@__yO9N7XETss|ro(wQngNkc^E^%S3=)Xb?5yh_pC&2J zM3p`76(Pm~m#su~upCW`2@lH}tU#8QM6-jJ3K~K|H0T*Ww)>)9HXF6x z`PtO-MYEHO9FpsuFCPBY=vN;*E?ev=A3gtJqgn1e_{ZG7{Oaq+KP@$smNaD*MU*H=3#`=Iyap+XhXAKOLkg0^B+5dZ z2Vt}bqad&$VB;dP7gvxHcG~PpKp*#J-#;4)N&pe>+JIv)qi6&$ie8lE=zZ^5McihH z;T1y3FuH?r4HsD!)mvMGX)ro?zOIW4Z0=P)|L<@AHKgXcCNO~0IhkV)Tf&i|3_g9RL_< zqN-?WYOz=Is4kc3!X8~rwoS(OmM5pLH~Gx$+`Vv<2TFU3*?-eLA`9x$Uyfv7m)QaY zM#a;4nC3ZyQi?fZAiB3LytBo}jwsTietB@S_#PbN_k61@E4!xex&I2xZ0&u?sRTk~jm zcv-3#&8m??-c9YYQCyx(4Tedg{-aMHI)0>Zv``XQUK6OWwqD)6_r;sTqwCkVL5?X3 zLx>ZAHIb8`ARyMvsrfw1roKRXzS>$7w}B`&Kb-Oe-R|5R+C$5>A=QjAT!}iNK$3lYvBF+yw?MWskiKi}LS%OuL6sg?>3x(Q4Xv(@Yz9K6=cdgod@ z3~}yFk}Qq=LX(Qafs;dsaYi#}VNI%gdyHOx+wZNHtDz7}oxFSXgI224^^AoDSD=-E z=0t)?o`FnNOInHVj_hTeumm_hDXW5?B$k(+q4)QVCLnl<#m&}ww?E#y{-BRDqgJXK z3I$NH9I?&`h0>WD5So)Dp*LP2R+?D0D%Yjv?cr!hD0sDSo7&pd=!eK(+RtBpuv1y9 zUJoN`C7>l1wd#zatA?yl6xD7LPKRWzU6BJ?X;4-L=$<@V~ZYoO$CmYw=d*=4; z2d|&~3Xt~=y0p1TaQ}j(va*Ci04^=B-e^tUyfbP$l6gJD&imd#pn=Ts6?*@xyaMqu_JqcdCnLl|wd-`V9?G8udg({;si!gB4n>e<;&6;+0 z4$c1al^x)zRcKW+oWJE{S#eqlzj$uVniE+fTOq67w+`W0;J9k`XVu$aKUiW+b zg%=)Qj2W2iqV>XI6Ij8Se0n~y7Wryi@7%x|Mgm_jY?iUCu2s+w_AYEFd4|FTZlLJQ zE>f+CeEi|X?pxRQ|M@3hTK2i27iHjj=ik#1Ix)mK$v`@oSa)vS`{c9w zv$rRCFuS?iW+5f>0Anfzc^FYGzVT{0;5pAvskT{NiA3v)DkFwVAWdRAPIK28O>su_ z=>T;5Y!uU11K25-Se7ri%8x(2_u})iIdPHJ26W2 zs#*QtZ(ojnsr2TMVv1m{6k~=^RFI%XP9e_KeYqAztuiG z?fv`9!_Rm1zy0{(@gzAq9H%8QhTwmCgPq#upX>A3`gN;5nr8i1o|MBvZZ})^za#_= zC*2XX`1G2}*(>Ei`l3Fy|ftP=wKj+%`v?w>P!3%Y{@C950!U9Yz+zC^@|A&C`>gjQ*D(;lU@vjf|_V1hb{{CQ4{QSEgM6M2Knj$6Nk5YP(#05a2(k`3V zsyIm_KXv_-Wf@)&DMW=qXlSJ<3d6`zbR$?yQ_3w+vJ%jc&_RHv?vN|z6c=b*2Kj`L zpea!84b?O>I}EKE@e{edwN-B^(^2;roxMHzH?vVu^{uNRwe;q)&P%)=d5PuOy2gVX zEbSS^$$CjqYgC3Qp65KzEm$xMmVx7PFfJ&Y1;~kG5JxKkO)(&+fFV^OLtNLT3s2)~ zoTN{!1(OwaGMix_OX}W!i+~h+vA|0cE97Nv90x2ttr(1=R)n%43ULytoe#+P97Iu^ zr9={QHz@>~B`EPCE6yk?%e}eBAwpA3N~9voK~9ozH4+V|*r-q$4if0!Xmlj$<0zWV z-OK$O>r}23X;uzGYq4;nkQIpSTv5m=@n#fwt+l#irJ^WFRaD#H!ziaH$26<{wWN2J zOs7CJOjb^**mpdkfF(@?qnR&CYL;=!g`-M5lBfK28!a(QX0y3uDpEsXdNcRI-Ft>&gFJFWT4ZZ00ZlVnn#xg{S1PK5 zGXO?T;;Y=|!d{w28AmbT=8{YlPNh)B}Ausb4PHfamMe1*u8&$Dd)_%v{ z`KzcAMR9mW$I)GaNes&tfh6FPLVW;B(MC`U3kw^uu+YNB-bN6?7Z8PrjkSdjBB_LI zD+>w>>+Z5T>deij1B20QNUB+~Zk3$Y(#kfBxiE<-5D5`>V@~k3R2z2xB3H^lxo?d%e&eolaRn z$nDV$$)PI_&w8_QDK1HDi$dCFRY`Ul0$=8}?n0J4Zih=zCE<{SzSbo}H|TE|Vxei^ zNzcz!UEi!$GUueI8<)dn=n8j*hAuxW9zAZiLt1W(mEu(0F7L%Vzm3ea|4nL}EwnAP zZMM*-Cp1Wg>2wO01R4YmMe!0gpU-Wfe=)c(5NN<(M59qaFVL!>#rzp20rCon2Vj`z zIzR;phr`G97NQ4683d14B(%l+LF;3=XImz)8?Nn$0HjaR4qM0szcFg&7P62a@%AJs2wx zYxe8GQ~;-8Mw!b{Ct;kNNyrNlXrqJzn8=@EF#{*Cgs}-&cD4t^y9+iB>^h>1@$fVy2ol2z; znvIt&0mL19g9&*gn5@pwl=%9oy@%yyXQodU<{|_KEjW3=V!>e0>d6vD7gbfsC(wc5 zLb5QeRx$%>ntn2w1Ue<{8L=`NjaaR>J0lcwxm+w3gQ!O<+j6JV;n2j8`7>s-84ihv zC*!B^tb_dUZD-^0m?HeOZnq0GjL&MdTB%gh>9n=)p$_#3N`JB{axg>Il_E{7P_xLD z2}P8(4g|T0MG6>?dNL2t!eX&VD$nQhgp>(uBV|+1n2b+1=$^yjkQqAV8y4c)%E*Ay z#7i5w1!SfPA#Al;;wO7&ud+rEM&T_K4E}UcZ^6pKD=>W;5v(NO1!y5+BQ}<)yo6wD z;}uxg1`A8!6LJIj0)}i7H*6jpvh0t3Q8ObBc@K-MJK)Y9XJ>cLnRnj*c4SB^V<}Bu z16Z)tHvn!LI&>O7ZI=E-hTt*;7YNZpv_Oa!qJ?OI5G_OtEF&5%$-f?+=lKSJLO^oR z*eTFn@O11ZnhPL-gntc?<t$V#FS@A33V<&n?l(UHSNwDH$bWV2bv zJcwC;xzs+Qxk<@D^X3TdR(z0H+j$iQcdio!(>YAEX!_#dt9L~3)pcFVrtM-%jYcEp z1*p_nJY=v9)${!l;N;@=;*DVp>b0h@bg5hL)`17l#1gzWWfqUeV;Qh$dkf+(JeL;{ z2U&>L>54I6Nlq)oq?aMM48etHfeo6VZdrf&dPS<2I5dNmH<}4v9xFkfE@ENnR2;zJfh4M^IBb7>p ztrFTYAvX9sL8*NUqpa8KK;iB8`zpdG06`cZqKGwlP^;B^-Jo*_CO)X!XuI8FwWuTQ zcH6S91G|VzBEnEvS>0}TI2>MUWwlx@?N%1?TSR)Xv)OFk{s94As&p_I$bd!LV*{>F z7V$O$`$DuftX3M;oxbh6`P+xvU;5E-V38VfBAabM!sSv1o!(-4&JfgZZ+gMxy<_>kc zE76!kXf(<$L#biNzgR4s^C08|3DHS_&?RI!uBXvxpnUQb-ig5B_h0yrkr5DyVsp^} zN6B_nk-IRix z;F`j*;ivQA-f1(b3ViwlzkaMjE)fm91vyp#465y zkByD_&3RT14-dDrvF}mn@kW>TJE3epy$B@$qpi$TwoNLPF^B;K}4?n=8yT z_zQf44~c)29Wz1c6efmUU0sDj>a9e}({gZoF)-wjWz}PTU^+WHCGundATkm{v-uml z>+kPhTwE-b(U>waGD4{RY-8?58eguiu8MJnt%SO=VXk=ny0_(Fu z*@9S6$J5i(`RdRO>>C&ukTz7JCW%!zH#cWrd92reMxXk(%pp$d9*LGqLj*LhZZm>qL2=b;AT@(6Wk1zL$Q*D>{da%Vzq*rv$d$& zNP)49RuI_P*;!Iapb!UO8;qr;rC68-r&u`_l5~G<5M!1e7Zw(16%BH*HPvW&W#TSv zaBz^LTy}ux=jX%y9D*S`02Q+=%R(qiP*Cmd?VO@@7uHV$UtT1~$GiLa&;18aC%%~q z>7Rf7W9QDJe3|F_uRs#SHvZG>%fip8pME>HdQsxqjk~`u{8?0n;O`l;+Hd7^>-NL1 z2EUf5@v+M^##LCK9F&IeY%FGHek?d8>c^J^S7FKU}(er3Rzl6w#C2XW24)d;7_#RFY9CnMsp9I#FVD zVsv73qLLY1W5?z(n_IPrA8~W0k;Lf4=)~v}sAO<98f6a43YW^9pkfHUpD@FNSVFGO zD6|M8;1(+yOJ-HjG{iCoD`0XfAi1A2CF@->lO{$dN{mj7PK-`0NR0lZ=q(vn@tFhB?nM9q N002ovPDHLkV1fo(kr@C0 literal 8266 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 diff --git a/tests/ref/show-text-in-citation.png b/tests/ref/show-text-in-citation.png index 392487bc8a53191a74ffe07174bfa3f61f27e2ae..6533a4f7c914d47cdbb5fa20e70400260418ba3c 100644 GIT binary patch delta 788 zcmV+v1MB>o2CD{;B!8PwOjJex|Nq_J=ilJx-Neh^+~YYrL)zTr+S}v4z{n;jFz@g4 z-rwcj)7#zD+uhF9-NMMh!^?Vngr%pi-G+&ckC)%xZ-Kws&w|~F8y~W+Iw%x$Sz{1LA zXmCG4Ny*F8tgg0NTx4!=c&@OxPf=NEYjeoT(w?BGxx2%Ff{NW`X5H1@-K($N(AaEj zY#|{b@$vDHkdW@-FMc-iHVMmj!sTa ze0+Q&A|j@yrhk!Yv$)-Se%;#Q z-Dhdtw!Gb@tKF8Fv$M0^pQEv{vE6}$-Obb~DJeNQIVUG4=H})kBqa0m^B^D~_xJZ$ zSXhmXjg*v>#l^+?`ue1#q}`5^r>Cd5xVYW2xZT6a-G9u}+}`H*_xSt!`}_O+-FJE2 zsjl7A+AS?Dxw*Ob_xbMc^BM<^K>z>&OG!jQRCwC$*Hv~xQ4mDYs!QD6-QC^Y-QC?? zNFW4waEF}_m_j-$$=ee+f7PpDGW}&Bp$90A>*i;?Hceb zF3itChJU?*L9wA!Ev*{Bnd4~|GD;dr22yck7=UqkJOddyss8Zj5CFq^KRp2vStgfvHJ|NUbA-LaA7#0d)6Z?HM96YsrM9WK~hA0Z&)&>my{arkW&GO1Aibl#*m{ z2!+GSCtRs8^GJMmIEo|#+~VPP3vC^ps*D=&oPR$&Sf#>FS`OOVRC%EWaN)SUfecGL zBO{ob$lQDY#?#{xWE2#Ubmb`y3<5BoSC>~HBEFd1+oxiGpYKR)u0zDp3vDmVNawBw zaCME%4T#v;=9|-*rcO^Z;JLehJ%kKyMUbR&xy1)0nj`}(KRB7@TV*o+CSO93Kupr0 StqrXJ0000DJCW+larH? zk&&jRrkg(_5>Fp~mH6bA(Y;0`t@$rz5 zklx?p$;!?X$!5)ljOr?qUo_a<1V9hS$bpQSO8?=mP5^pfXm1Zh)YkbAcXt5L zZRqQVh{}4>?V`bX4M0m9VuB%}u|d}--;T{k!}4L}c$%*}&{lMV7{jRq&Dex5{DAw+DS({awx5zcA=!b6Z%1Q9Dk!^amF z<6|>(uYe^YG$<271_xle_CVUR8Q+UqQbkdI+2L06!!M^?CtTuz`#*aQD9(TKtMo{kdPuG zBBG+A+uPgL*4EhA*s-y(%F4=ld3nUd#QXdEs;a84uC6XFE-fuBVq#*6iHVh!l|@BG zw6wG|G&FmAdv$en3JMCOq@?%v_gGk1($dn`*Vh*p7v|>Xlc20uSP zY;0`E$jB%tD6+D$b8~ZQYHB+>J5*FudVGXILrdJ==7))qwz$B|&elXmPPe(iZg6B%GaDOI;4`~Te_*> z(9$Zt6$=viioHvuuGT25v3!+;@3Ws8`MAC^4nI7r9)BOH-n|)zbE>B&ud14}59?4W;8^&TV!@7K)5rNkj43=eYm4NAq#VDyj&F- z-roIj7*OmJ-?nzX9R~XOD**tYa@7RF$pc{2WYlETWYlETWYlETWYlET+9 zsi_JI3UzgLdwY9DMMbf(vDnzy*4Ea<#Kd`ddE49DqN1WAA|hg9VvvxKKtMoXU|>;E zQI(aIz`(%v_J8)>-QDr=@mE(@sHmt!L`1v0y8r+HUH_Wr0003NNkl!aet9j(@zDpDE0W>KnUclz zmAE!FT}rgLyx?02007Zd4TOIl0HY@F$Bdecnv9x^nv9y Date: Tue, 20 May 2025 14:57:19 +0200 Subject: [PATCH 113/172] Underline file path of failed test (#6281) --- tests/src/collect.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/src/collect.rs b/tests/src/collect.rs index 33f4f7366..84af04d2d 100644 --- a/tests/src/collect.rs +++ b/tests/src/collect.rs @@ -30,7 +30,8 @@ pub struct Test { impl Display for Test { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{} ({})", self.name, self.pos) + // underline path + write!(f, "{} (\x1B[4m{}\x1B[0m)", self.name, self.pos) } } From 300a782451082e5d7bdf894f0cc756261076008b Mon Sep 17 00:00:00 2001 From: frozolotl <44589151+frozolotl@users.noreply.github.com> Date: Tue, 20 May 2025 15:54:49 +0200 Subject: [PATCH 114/172] Always run tests from workspace directory (#6307) --- tests/src/tests.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/src/tests.rs b/tests/src/tests.rs index 26eb63beb..0ed2fa469 100644 --- a/tests/src/tests.rs +++ b/tests/src/tests.rs @@ -59,7 +59,9 @@ fn main() { fn setup() { // Make all paths relative to the workspace. That's nicer for IDEs when // clicking on paths printed to the terminal. - std::env::set_current_dir("..").unwrap(); + let workspace_dir = + Path::new(env!("CARGO_MANIFEST_DIR")).join(std::path::Component::ParentDir); + std::env::set_current_dir(workspace_dir).unwrap(); // Create the storage. for ext in ["render", "html", "pdf", "svg"] { From e90c2f74ef63d92fc160ba5ba04b780c1a64fe75 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 20 May 2025 16:20:40 +0000 Subject: [PATCH 115/172] Fix text overhang example in docs (#6223) --- crates/typst-library/src/text/mod.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index 462d16060..23edc9e98 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -348,15 +348,17 @@ pub struct TextElem { /// This can make justification visually more pleasing. /// /// ```example + /// #set page(width: 220pt) + /// /// #set par(justify: true) /// This justified text has a hyphen in - /// the paragraph's first line. Hanging + /// the paragraph's second line. Hanging /// the hyphen slightly into the margin /// results in a clearer paragraph edge. /// /// #set text(overhang: false) /// This justified text has a hyphen in - /// the paragraph's first line. Hanging + /// the paragraph's second line. Hanging /// the hyphen slightly into the margin /// results in a clearer paragraph edge. /// ``` From d42d2ed200c8f3f167ee09be69fcf86f4b645971 Mon Sep 17 00:00:00 2001 From: frozolotl <44589151+frozolotl@users.noreply.github.com> Date: Tue, 20 May 2025 18:24:46 +0200 Subject: [PATCH 116/172] Error if an unexpected named argument was received (#6192) --- crates/typst-eval/src/call.rs | 14 ++++++++------ tests/ref/math-call-symbol.png | Bin 0 -> 703 bytes tests/suite/math/call.typ | 9 +++++++++ 3 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 tests/ref/math-call-symbol.png diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index 1ca7b4b8f..6a57c85e8 100644 --- a/crates/typst-eval/src/call.rs +++ b/crates/typst-eval/src/call.rs @@ -404,12 +404,14 @@ fn wrap_args_in_math( if trailing_comma { body += SymbolElem::packed(','); } - Ok(Value::Content( - callee.display().spanned(callee_span) - + LrElem::new(SymbolElem::packed('(') + body + SymbolElem::packed(')')) - .pack() - .spanned(args.span), - )) + + let formatted = callee.display().spanned(callee_span) + + LrElem::new(SymbolElem::packed('(') + body + SymbolElem::packed(')')) + .pack() + .spanned(args.span); + + args.finish()?; + Ok(Value::Content(formatted)) } /// Provide a hint if the callee is a shadowed standard library function. diff --git a/tests/ref/math-call-symbol.png b/tests/ref/math-call-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..8308bece1fba2d652a82e225c94b1212a90f1025 GIT binary patch literal 703 zcmV;w0zmzVP)71w8wCUzNE$5E!E}jP(cDv_^;`hG!e(qlIT>PFB{H0C^Sik}nFzUlUjndufRT-{{ zA@$j%D!l&!xp<*Ua5t-|8Vp7lZh2Crg&XR?W1HxL=5_5nj7H8JIpMB4a4V}zG@zXW zMhl38Mjd#*SnIADprajEb3xmrL$yzh<5bB41j;#Rnr<9~Xt7H0N)Z5WqeykGs|3$} z10dK109%9NtroC=1uS3z|1RFhvFu+}fm?hWuQsR#{FFahey1pGWH0TS*I5d)jgM>Y z17OPfvHcQmDhcmcsNdT(^|iqSXG2Z^#;^MgI?yNy4|&2P^sNsz1G_0i00>>=fgSEB z35T*Orrx*VdQP~krC_5Uy$`(Eq0V~SkIeGm}^lKj${B} zT0?5;5VBVvZh$_jD7@1vT?0}cVDVD`>~rF+7O;Q?EZ~0yjw=K=g~5wd&>8^b7JSmR z+7yA!P8^k2q`hBZIX8n`?!u!QY3X80!1=(M41dX>CI`7^hjqEq8tG0dY#@|=)gHK) z0CPaT9^{5H-O||2P9_{P(dv&iIJAi{&h$9Q?c_BgrAa2dG(kU8vy0`4u)i#}j8veK z$&*OkP9{vwYh!I4Aq`-=5NEOjj#4YN)s%+)TJT+>DFwX?@Z=>G4qq;_0!6S=|DhGP z4!-O4`gwHjqY}XO@l}9fGte`hRG2gXX2t*@B44n81v#4^s3xg!v|GC7FaUcI Date: Tue, 20 May 2025 18:25:26 +0200 Subject: [PATCH 117/172] Removing unused warnings in nightly (#6169) --- crates/typst-library/src/foundations/content.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/typst-library/src/foundations/content.rs b/crates/typst-library/src/foundations/content.rs index daf6c2dd9..1855bb70b 100644 --- a/crates/typst-library/src/foundations/content.rs +++ b/crates/typst-library/src/foundations/content.rs @@ -414,7 +414,7 @@ impl Content { /// Elements produced in `show` rules will not be included in the results. pub fn query(&self, selector: Selector) -> Vec { let mut results = Vec::new(); - self.traverse(&mut |element| -> ControlFlow<()> { + let _ = self.traverse(&mut |element| -> ControlFlow<()> { if selector.matches(&element, None) { results.push(element); } @@ -441,7 +441,7 @@ impl Content { /// Extracts the plain text of this content. pub fn plain_text(&self) -> EcoString { let mut text = EcoString::new(); - self.traverse(&mut |element| -> ControlFlow<()> { + let _ = self.traverse(&mut |element| -> ControlFlow<()> { if let Some(textable) = element.with::() { textable.plain_text(&mut text); } From df89a0e85b80844ef56a6fa98af01eaaf7553da8 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Tue, 20 May 2025 18:27:14 +0200 Subject: [PATCH 118/172] Use the right multiplication symbol in expression tooltip (#6163) --- crates/typst-ide/src/tooltip.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index cbfffe530..2638ce51b 100644 --- a/crates/typst-ide/src/tooltip.rs +++ b/crates/typst-ide/src/tooltip.rs @@ -86,7 +86,7 @@ fn expr_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option { *count += 1; continue; } else if *count > 1 { - write!(pieces.last_mut().unwrap(), " (x{count})").unwrap(); + write!(pieces.last_mut().unwrap(), " (×{count})").unwrap(); } } pieces.push(value.repr()); @@ -95,7 +95,7 @@ fn expr_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option { if let Some((_, count)) = last { if count > 1 { - write!(pieces.last_mut().unwrap(), " (x{count})").unwrap(); + write!(pieces.last_mut().unwrap(), " (×{count})").unwrap(); } } From 2a258a0c3849073c56a0559dbd67ee2effcd1031 Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Fri, 23 May 2025 09:31:26 +0200 Subject: [PATCH 119/172] Remove unused Marginal type (#6321) --- crates/typst-library/src/layout/page.rs | 43 ++----------------------- 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/crates/typst-library/src/layout/page.rs b/crates/typst-library/src/layout/page.rs index 62e25278a..98afbd06f 100644 --- a/crates/typst-library/src/layout/page.rs +++ b/crates/typst-library/src/layout/page.rs @@ -1,16 +1,14 @@ -use std::borrow::Cow; use std::num::NonZeroUsize; use std::ops::RangeInclusive; use std::str::FromStr; -use comemo::Track; use typst_utils::{singleton, NonZeroExt, Scalar}; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Args, AutoValue, Cast, Construct, Content, Context, Dict, Fold, Func, - NativeElement, Set, Smart, StyleChain, Value, + cast, elem, Args, AutoValue, Cast, Construct, Content, Dict, Fold, NativeElement, + Set, Smart, Value, }; use crate::introspection::Introspector; use crate::layout::{ @@ -649,43 +647,6 @@ cast! { }, } -/// A header, footer, foreground or background definition. -#[derive(Debug, Clone, Hash)] -pub enum Marginal { - /// Bare content. - Content(Content), - /// A closure mapping from a page number to content. - Func(Func), -} - -impl Marginal { - /// Resolve the marginal based on the page number. - pub fn resolve( - &self, - engine: &mut Engine, - styles: StyleChain, - page: usize, - ) -> SourceResult> { - Ok(match self { - Self::Content(content) => Cow::Borrowed(content), - Self::Func(func) => Cow::Owned( - func.call(engine, Context::new(None, Some(styles)).track(), [page])? - .display(), - ), - }) - } -} - -cast! { - Marginal, - self => match self { - Self::Content(v) => v.into_value(), - Self::Func(v) => v.into_value(), - }, - v: Content => Self::Content(v), - v: Func => Self::Func(v), -} - /// A list of page ranges to be exported. #[derive(Debug, Clone)] pub struct PageRanges(Vec); From 6e0f48e192ddbd934d3aadd056810c86bcc3defd Mon Sep 17 00:00:00 2001 From: Igor Khanin Date: Wed, 28 May 2025 16:05:10 +0300 Subject: [PATCH 120/172] More precise math font autocomplete suggestions (#6316) --- crates/typst-ide/src/complete.rs | 45 ++++++++++++++++++++-- crates/typst-ide/src/tooltip.rs | 2 +- crates/typst-ide/src/utils.rs | 11 ++---- crates/typst-library/src/text/font/book.rs | 3 ++ 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index 91fa53f9a..15b4296eb 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -15,7 +15,7 @@ use typst::syntax::{ ast, is_id_continue, is_id_start, is_ident, FileId, LinkedNode, Side, Source, SyntaxKind, }; -use typst::text::RawElem; +use typst::text::{FontFlags, RawElem}; use typst::visualize::Color; use unscanny::Scanner; @@ -1081,6 +1081,24 @@ fn code_completions(ctx: &mut CompletionContext, hash: bool) { } } +/// See if the AST node is somewhere within a show rule applying to equations +fn is_in_equation_show_rule(leaf: &LinkedNode<'_>) -> bool { + let mut node = leaf; + while let Some(parent) = node.parent() { + if_chain! { + if let Some(expr) = parent.get().cast::(); + if let ast::Expr::ShowRule(show) = expr; + if let Some(ast::Expr::FieldAccess(field)) = show.selector(); + if field.field().as_str() == "equation"; + then { + return true; + } + } + node = parent; + } + false +} + /// Context for autocompletion. struct CompletionContext<'a> { world: &'a (dyn IdeWorld + 'a), @@ -1152,10 +1170,12 @@ impl<'a> CompletionContext<'a> { /// Add completions for all font families. fn font_completions(&mut self) { - let equation = self.before_window(25).contains("equation"); + let equation = is_in_equation_show_rule(self.leaf); for (family, iter) in self.world.book().families() { - let detail = summarize_font_family(iter); - if !equation || family.contains("Math") { + let variants: Vec<_> = iter.collect(); + let is_math = variants.iter().any(|f| f.flags.contains(FontFlags::MATH)); + let detail = summarize_font_family(variants); + if !equation || is_math { self.str_completion( family, Some(CompletionKind::Font), @@ -1790,4 +1810,21 @@ mod tests { .must_include(["r", "dashed"]) .must_exclude(["cases"]); } + + #[test] + fn test_autocomplete_fonts() { + test("#text(font:)", -1) + .must_include(["\"Libertinus Serif\"", "\"New Computer Modern Math\""]); + + test("#show link: set text(font: )", -1) + .must_include(["\"Libertinus Serif\"", "\"New Computer Modern Math\""]); + + test("#show math.equation: set text(font: )", -1) + .must_include(["\"New Computer Modern Math\""]) + .must_exclude(["\"Libertinus Serif\""]); + + test("#show math.equation: it => { set text(font: )\nit }", -6) + .must_include(["\"New Computer Modern Math\""]) + .must_exclude(["\"Libertinus Serif\""]); + } } diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index 2638ce51b..e5e4cc19a 100644 --- a/crates/typst-ide/src/tooltip.rs +++ b/crates/typst-ide/src/tooltip.rs @@ -269,7 +269,7 @@ fn font_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option { .find(|&(family, _)| family.to_lowercase().as_str() == lower.as_str()); then { - let detail = summarize_font_family(iter); + let detail = summarize_font_family(iter.collect()); return Some(Tooltip::Text(detail)); } }; diff --git a/crates/typst-ide/src/utils.rs b/crates/typst-ide/src/utils.rs index d5d584e2b..887e851f9 100644 --- a/crates/typst-ide/src/utils.rs +++ b/crates/typst-ide/src/utils.rs @@ -77,23 +77,20 @@ pub fn plain_docs_sentence(docs: &str) -> EcoString { } /// Create a short description of a font family. -pub fn summarize_font_family<'a>( - variants: impl Iterator, -) -> EcoString { - let mut infos: Vec<_> = variants.collect(); - infos.sort_by_key(|info| info.variant); +pub fn summarize_font_family(mut variants: Vec<&FontInfo>) -> EcoString { + variants.sort_by_key(|info| info.variant); let mut has_italic = false; let mut min_weight = u16::MAX; let mut max_weight = 0; - for info in &infos { + for info in &variants { let weight = info.variant.weight.to_number(); has_italic |= info.variant.style == FontStyle::Italic; min_weight = min_weight.min(weight); max_weight = min_weight.max(weight); } - let count = infos.len(); + let count = variants.len(); let mut detail = eco_format!("{count} variant{}.", if count == 1 { "" } else { "s" }); if min_weight == max_weight { diff --git a/crates/typst-library/src/text/font/book.rs b/crates/typst-library/src/text/font/book.rs index 9f8acce87..cd90a08fe 100644 --- a/crates/typst-library/src/text/font/book.rs +++ b/crates/typst-library/src/text/font/book.rs @@ -194,6 +194,8 @@ bitflags::bitflags! { const MONOSPACE = 1 << 0; /// Glyphs have short strokes at their stems. const SERIF = 1 << 1; + /// Font face has a MATH table + const MATH = 1 << 2; } } @@ -272,6 +274,7 @@ impl FontInfo { let mut flags = FontFlags::empty(); flags.set(FontFlags::MONOSPACE, ttf.is_monospaced()); + flags.set(FontFlags::MATH, ttf.tables().math.is_some()); // Determine whether this is a serif or sans-serif font. if let Some(panose) = ttf From 9bbfa5ae0593333b1f0afffd71fec198d61742a6 Mon Sep 17 00:00:00 2001 From: Shunsuke KIMURA Date: Wed, 28 May 2025 22:29:45 +0900 Subject: [PATCH 121/172] Clarify localization of reference labels based on lang setting (#6249) Signed-off-by: Shunsuke Kimura --- crates/typst-library/src/model/reference.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/model/reference.rs b/crates/typst-library/src/model/reference.rs index 316617688..7d44cccc0 100644 --- a/crates/typst-library/src/model/reference.rs +++ b/crates/typst-library/src/model/reference.rs @@ -21,9 +21,10 @@ use crate::text::TextElem; /// /// The default, a `{"normal"}` reference, produces a textual reference to a /// label. For example, a reference to a heading will yield an appropriate -/// string such as "Section 1" for a reference to the first heading. The -/// references are also links to the respective element. Reference syntax can -/// also be used to [cite] from a bibliography. +/// string such as "Section 1" for a reference to the first heading. The word +/// "Section" depends on the [`lang`]($text.lang) setting and is localized +/// accordingly. The references are also links to the respective element. +/// Reference syntax can also be used to [cite] from a bibliography. /// /// As the default form requires a supplement and numbering, the label must be /// attached to a _referenceable element_. Referenceable elements include From 9ac21b8524632c70ab9e090488a70085eabe4189 Mon Sep 17 00:00:00 2001 From: Igor Khanin Date: Wed, 28 May 2025 16:41:35 +0300 Subject: [PATCH 122/172] Fix tracing of most field call expressions (#6234) --- crates/typst-eval/src/call.rs | 7 ++++++- crates/typst-ide/src/tooltip.rs | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index 6a57c85e8..fa9683416 100644 --- a/crates/typst-eval/src/call.rs +++ b/crates/typst-eval/src/call.rs @@ -37,7 +37,12 @@ impl Eval for ast::FuncCall<'_> { let target = access.target(); let field = access.field(); match eval_field_call(target, field, args, span, vm)? { - FieldCall::Normal(callee, args) => (callee, args), + FieldCall::Normal(callee, args) => { + if vm.inspected == Some(callee_span) { + vm.trace(callee.clone()); + } + (callee, args) + } FieldCall::Resolved(value) => return Ok(value), } } else { diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index e5e4cc19a..528f679cf 100644 --- a/crates/typst-ide/src/tooltip.rs +++ b/crates/typst-ide/src/tooltip.rs @@ -371,4 +371,11 @@ mod tests { test(&world, -2, Side::Before).must_be_none(); test(&world, -2, Side::After).must_be_text("This star imports `a`, `b`, and `c`"); } + + #[test] + fn test_tooltip_field_call() { + let world = TestWorld::new("#import \"other.typ\"\n#other.f()") + .with_source("other.typ", "#let f = (x) => 1"); + test(&world, -4, Side::After).must_be_code("(..) => .."); + } } From 9a95966302bae4d795cd2fba4b3beb6f41629221 Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski <133046678+wrzian@users.noreply.github.com> Date: Wed, 28 May 2025 09:44:44 -0400 Subject: [PATCH 123/172] Remove line break opportunity when math operator precededes a closing paren (#6216) --- crates/typst-layout/src/math/run.rs | 31 ++++++++++-------- ...ebreaking-after-relation-without-space.png | Bin 439 -> 2630 bytes tests/suite/math/multiline.typ | 3 ++ 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/crates/typst-layout/src/math/run.rs b/crates/typst-layout/src/math/run.rs index ae64368d6..4ec76c253 100644 --- a/crates/typst-layout/src/math/run.rs +++ b/crates/typst-layout/src/math/run.rs @@ -278,6 +278,9 @@ impl MathRun { frame } + /// Convert this run of math fragments into a vector of inline items for + /// paragraph layout. Creates multiple fragments when relation or binary + /// operators are present to allow for line-breaking opportunities later. pub fn into_par_items(self) -> Vec { let mut items = vec![]; @@ -295,21 +298,24 @@ impl MathRun { let mut space_is_visible = false; - let is_relation = |f: &MathFragment| matches!(f.class(), MathClass::Relation); let is_space = |f: &MathFragment| { matches!(f, MathFragment::Space(_) | MathFragment::Spacing(_, _)) }; + let is_line_break_opportunity = |class, next_fragment| match class { + // Don't split when two relations are in a row or when preceding a + // closing parenthesis. + MathClass::Binary => next_fragment != Some(MathClass::Closing), + MathClass::Relation => { + !matches!(next_fragment, Some(MathClass::Relation | MathClass::Closing)) + } + _ => false, + }; let mut iter = self.0.into_iter().peekable(); while let Some(fragment) = iter.next() { - if space_is_visible { - match fragment { - MathFragment::Space(width) | MathFragment::Spacing(width, _) => { - items.push(InlineItem::Space(width, true)); - continue; - } - _ => {} - } + if space_is_visible && is_space(&fragment) { + items.push(InlineItem::Space(fragment.width(), true)); + continue; } let class = fragment.class(); @@ -323,10 +329,9 @@ impl MathRun { frame.push_frame(pos, fragment.into_frame()); empty = false; - if class == MathClass::Binary - || (class == MathClass::Relation - && !iter.peek().map(is_relation).unwrap_or_default()) - { + // Split our current frame when we encounter a binary operator or + // relation so that there is a line-breaking opportunity. + if is_line_break_opportunity(class, iter.peek().map(|f| f.class())) { let mut frame_prev = std::mem::replace(&mut frame, Frame::soft(Size::zero())); diff --git a/tests/ref/math-linebreaking-after-relation-without-space.png b/tests/ref/math-linebreaking-after-relation-without-space.png index 7c569ad1fd2166800e30b88c2f5ca689be659341..fb14137680a55a42a8717f11fe555378489031c0 100644 GIT binary patch literal 2630 zcmb7G4^UJ09S>FfgJq7Q(iS0|=-7reb#CN3kVplsaT0hc{*c1LB%sLZ0GU zN)f2VmKN?{U5|2=K}xM4%bzEI0|i6`it8FYoR5OK^AAdfV;@xqI*X ze))bs|Gux}jpW0D0nZ087>vLpuO_?+zD3~k(S~*4f4Tf8(F{gF;E{y*-;4Z=H`n}q z=N~@l#c83btZ%U4Oj1U3j;3|>@6@H*PhOMN{(9dIlk=Unx6gf99FsMk(mIq%bAP6p zze+cLn3F$Fr}nkBFVep{sY$mYTffO^cA)|HqPZUayV*63liouQ2?w1QJJ25K{>$!V(8~qym0ptp}j2hGksV=yEF}z!@)W}Ed z`{_g8o0m#MSeDb6?X+Y*Q~Glm`JHSK4Tili4F;*n;Hcy{gL#I(@`@+K$_c%0!@X!$ zkx*1DynmIJaBLZz-ZdVZfR09OS!i#RY9`8ZRA4P<#LH#h_DCdK7#eL zy`h3xDPrX!Jp!^41Zy%+9j4I7s3Ix~mouE;nLnxwVpTEbD96Cq zdq2z}1Y>FX38`^Hq-RSr7}Dtw=>@j<8e8wX%RDX+RH!)=uDBDWdeL!)v zBO^hfp`l*wT%}u1%Z<@0yBMtsj!ZV6VA@Ws-pC859cm(iV~Lm)>B|(Lt;lQlE+K26 zrZ~MO)1Er~xw?)<)ncQi9Q1qTO^5IRbkY~(Sc+#0;;cgYLlqsN=4zRr0~D-fdG4^lK~BmgqpJCJb}&glrn`kX zfIjo65FeYCkeMeC&qO-nv8IcS*Fm%4kXN_cO_IZOETA6iOr@h>Ln~YP_*O|@v19|l z18DpyScrMF*pLvh3jv!xQQurIVd!i*y55JPj%TjKO9t8`n^r|VB37uyX2_?{D7=I^ z%P;FXLkdSid}1#M+K-o{%(iMy>jSm-18*!M{wFnL6uCT1?UqrCvRnO?ksv@~;l;D1hJc@@-FsA-$KN=Ns# zI#bLrH55Pq?CVFHU4o0iEEmV$?y-Y$@}@ie;~rGTm8?%|Ix3mtN_W+{zN!NnZUVxT zeT|~S!omq*3;2crCb>XBMKd@x2X3-XwVKmM- zdSej;ZU_SFmlNo)aAt#^P2H~p(wzl(3#1r+f# zxE8bB8_XMTLI{b^nXbnH@ zqgWHvK@h-v9TI|#?DPF>$jC4`IBYf`ltE!yQtgouzms+4XEou|Oln#($}S9EZLePE zYSPWHrFypLU9QMqyV#`tz`Y+vdM0AabatnZ31I*E0iX|ovyfZRdiY)@v6n&K-t76Q zm-4P~+e@oOe%S=nss#Gc7j-lPSvib!Cn?90T3TAb(t+Xn*w$BU(9XQ5Eo!91^8FiS@oF=oT`IZ3{26RJ} zSBXlh824*b4{Agp4ETS(_S{3F_|zPK-Y&)LqOu85pw>~Qd-lAJ_UH_?V$(F73Xr|_ zq(AFb5e37tUa#^7}^o0NhHpOeC7`8Rvs&^m3Jp&m?N`Fx_PqHN%>pa6Bz-*L?2?dcZqPE)DS$d#f0VGHijBy=Syq68$IcU}B1?`b`pr`|&TRJ@Yy=mPJD Oj3bH33C*t@FZws;1~!fW delta 426 zcmV;b0agCS6t@GA7k_F900000QW46Y0004gNkl2#n5REgAXKhDI)yq{y_C3#65@6e+Qmv7MUKF1EJWOj|y3?vLF3zMt6l{RCch zKIeRo2c$d-E3B}>3cL0ip}^16;Ti$Hv$j4MkGj-%$=W#KJAZYD2aE*(=M#r+@Sd^Y z^rQzwVTH}ZZzQN{Y`U?+9S67K6u7bG&!C=_ zhRch=0)U&4FkI?MlQyod1oF<#@*6idta~I!=ZZ<4Q^|m%a^qPJRMh5r=%1qx^C UMq-$2r~m)}07*qoM6N<$f)`iH{Qv*} diff --git a/tests/suite/math/multiline.typ b/tests/suite/math/multiline.typ index 34e66b99c..70838dd8c 100644 --- a/tests/suite/math/multiline.typ +++ b/tests/suite/math/multiline.typ @@ -99,6 +99,9 @@ Multiple trailing line breaks. #let hrule(x) = box(line(length: x)) #hrule(90pt)$<;$\ #hrule(95pt)$<;$\ +// We don't linebreak before a closing paren, but do before an opening paren. +#hrule(90pt)$<($\ +#hrule(95pt)$<($ #hrule(90pt)$<)$\ #hrule(95pt)$<)$ From 82e869023c7a7f31d716e7706a9a176b3d909279 Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Wed, 28 May 2025 17:14:29 +0300 Subject: [PATCH 124/172] Add remaining height example for layout (#6266) Co-authored-by: Laurenz --- crates/typst-library/src/layout/layout.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/typst-library/src/layout/layout.rs b/crates/typst-library/src/layout/layout.rs index 88252e5e3..04aaee944 100644 --- a/crates/typst-library/src/layout/layout.rs +++ b/crates/typst-library/src/layout/layout.rs @@ -41,8 +41,23 @@ use crate::layout::{BlockElem, Size}; /// receives the page's dimensions minus its margins. This is mostly useful in /// combination with [measurement]($measure). /// -/// You can also use this function to resolve [`ratio`] to fixed lengths. This -/// might come in handy if you're building your own layout abstractions. +/// To retrieve the _remaining_ size of the page rather than its full size, you +/// you can wrap your `layout` call in a `{block(height: 1fr)}`. This works +/// because the block automatically grows to fill the remaining space (see the +/// [fraction] documentation for more details). +/// +/// ```example +/// #set page(height: 150pt) +/// +/// #lorem(20) +/// +/// #block(height: 1fr, layout(size => [ +/// Remaining height: #size.height +/// ])) +/// ``` +/// +/// You can also use this function to resolve a [`ratio`] to a fixed length. +/// This might come in handy if you're building your own layout abstractions. /// /// ```example /// #layout(size => { From 3e7a39e968644ee925598f792fdc597b55a2529f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj?= <171210953+mgazeel@users.noreply.github.com> Date: Wed, 28 May 2025 19:29:40 +0200 Subject: [PATCH 125/172] Fix stroking of glyphs in math mode (#6243) --- crates/typst-layout/src/math/fragment.rs | 6 ++++-- tests/ref/issue-6170-equation-stroke.png | Bin 0 -> 1381 bytes tests/suite/math/equation.typ | 7 +++++++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 tests/ref/issue-6170-equation-stroke.png diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs index 1b508a349..59858a9cb 100644 --- a/crates/typst-layout/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -11,7 +11,7 @@ use typst_library::layout::{ }; use typst_library::math::{EquationElem, MathSize}; use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem}; -use typst_library::visualize::Paint; +use typst_library::visualize::{FixedStroke, Paint}; use typst_syntax::Span; use typst_utils::default_math_class; use unicode_math_class::MathClass; @@ -235,6 +235,7 @@ pub struct GlyphFragment { pub lang: Lang, pub region: Option, pub fill: Paint, + pub stroke: Option, pub shift: Abs, pub width: Abs, pub ascent: Abs, @@ -286,6 +287,7 @@ impl GlyphFragment { lang: TextElem::lang_in(styles), region: TextElem::region_in(styles), fill: TextElem::fill_in(styles).as_decoration(), + stroke: TextElem::stroke_in(styles).map(|s| s.unwrap_or_default()), shift: TextElem::baseline_in(styles), font_size: TextElem::size_in(styles), math_size: EquationElem::size_in(styles), @@ -368,10 +370,10 @@ impl GlyphFragment { font: self.font.clone(), size: self.font_size, fill: self.fill, + stroke: self.stroke, lang: self.lang, region: self.region, text: self.c.into(), - stroke: None, glyphs: vec![Glyph { id: self.id.0, x_advance: Em::from_length(self.width, self.font_size), diff --git a/tests/ref/issue-6170-equation-stroke.png b/tests/ref/issue-6170-equation-stroke.png new file mode 100644 index 0000000000000000000000000000000000000000..a375931b50514adcfc0800fc1d4ac51b08e1bf06 GIT binary patch literal 1381 zcmb7^ZA?>F7{`muxiJ}#Wunqiw>dj?SQRF?%B_l(p_A%-Y0R9}RmLWkRvFsDrPqnC zj2AZ}5J;)cOcsfB11&P}URcGVQtE=0nL;n6wFPV6q4e$ctYrJ(2fr+N&hvaZ=j6%% z|I7c}*6hsasQFPO5-B?C(+!`8r!ah0y!L9i$0hTQl1MW@%G$7QhcLq06?tjl?&`Zv zaLiWG+t+wxGq;(imH$`YmN!&yk~OSMe$VDR+IFz$>KS^Th9y_+1nhUUrfZ<-Jg-0l zcJ|8KpMfO@w0kCzw_AA4ek|(ADmKA6b*R26BwH_6A(z#GQkq#_O81_u2?!EJe*Ka7;dy=-RP1I+4-}ej)E()1+m_C{ljP~g$07A_Qv?3vQ zut0;%KcEdRC7a^x0R!c(8SBY)o$bjG=4ZK*kjXD$r3|K0o6{Bjo>j$}fuXKKc)UZr ztg6^=xOF4Vn1EQdz9&=u{iYR$Yapdp?qho!sG(TgtHk@8Bt;*fE){m(Y+n4J(Ot=z z$cNojI8t_tSQ&iUjk~Ve!JMG04a^wO_p*bYLDyxO@B2*txjl|eaxQ4OrT<|d7G1Kl|O_pilkQ#nHX0>MlG^>HWdror{H;H-8epczB`uM*%^nuL9l`at;i-B!`50@;naLu!>$@|u8MSR9V|`e>uu+|7p^11#bwOtQ z+>cvO!JUkJ;auGDHK#u^so-7*Y!A5RR|yLtn+7UhC{k0rYvq9?bo?E(t|_!RY&_F3 zq2a+^RtXvR9OWpZiNc^uf^|2FwYiP~X^=a#68kYg5PE}1Mgt_&#zo6&v_*@>z{W5s zu#qd?+8FPLkRwMI=41Ll&ToSr-xwpfW7!%5nd11wu1dhYe)vA8BoUb`K)RD;L@esj zV702hM^HUk>Q_pY!w5nh#GY4tvpknie^9S;orXv9&=@vRlOb3AF!FfPdgIX0@{USo zUps$|avS~UUj^>x8%d|hw|FJg%OsLw*RZk9mcl05*8XpvU%=lDx}><9-Pw1OmbFa} z;=*83Qj3hOi2DuEUA+|sUx?|TDvSj<$MRRGjhcK>j&3xLeLNNh7=0A*D)Z*i8 z@$YP74qr4+JYpB0bOQkm_r&47nS_z*d|V|e(AjfzrQphWo*Q|@f_!Fg8HHE^nM?P8<|FwxE*ii~qiPC{d($4w%uU{9qmb&qhp76<}0y z`YF74l(IGcBAf0jTjQ$BbRcu5*YYcFGYm<%<+`uOPORR0IFQqhucLpJn0ze?#7v8D ziGx~Fy^mzXZ1HF-QhTP4AF)poDfKi;l~nAxQt$`k9*CS4WNh4`6?V{y;vqM|sAxmS zx9m2RsewhdkDG27*!$GrLuQ2YBa_f?2WM$uSjM%O4Bxfh-+bqaZ;Tz-8ltR|tHA!L y*;9wbon7Z_X{i%~PB8mr>GXn|FAcLY@5Db-_9~SC literal 0 HcmV?d00001 diff --git a/tests/suite/math/equation.typ b/tests/suite/math/equation.typ index 148a49d02..189f6e6db 100644 --- a/tests/suite/math/equation.typ +++ b/tests/suite/math/equation.typ @@ -297,3 +297,10 @@ Looks at the @quadratic formula. #set page(width: 150pt) #set text(lang: "he") תהא סדרה $a_n$: $[a_n: 1, 1/2, 1/3, dots]$ + +--- issue-6170-equation-stroke --- +// In this bug stroke settings did not apply to math content. +// We expect all of these to have a green stroke. +#set text(stroke: green + 0.5pt) + +A $B^2$ $ grave(C)' $ From 61dee554ba9f8d30b983776ecdfefa4b12a985ea Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Mon, 2 Jun 2025 14:02:01 +0100 Subject: [PATCH 126/172] Add an example of show-set `place.clearance` for figures in the doc (#6208) --- crates/typst-library/src/model/figure.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/typst-library/src/model/figure.rs b/crates/typst-library/src/model/figure.rs index 5a137edbd..bec667d6e 100644 --- a/crates/typst-library/src/model/figure.rs +++ b/crates/typst-library/src/model/figure.rs @@ -125,6 +125,9 @@ pub struct FigureElem { /// /// ```example /// #set page(height: 200pt) + /// #show figure: set place( + /// clearance: 1em, + /// ) /// /// = Introduction /// #figure( From 83e249dd334442b09bbeebcc70cae83950c31311 Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Mon, 2 Jun 2025 16:03:03 +0300 Subject: [PATCH 127/172] Fix Greek numbering docs (#6360) --- crates/typst-library/src/model/numbering.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/typst-library/src/model/numbering.rs b/crates/typst-library/src/model/numbering.rs index d82c3e4cd..320ed7d17 100644 --- a/crates/typst-library/src/model/numbering.rs +++ b/crates/typst-library/src/model/numbering.rs @@ -261,9 +261,9 @@ pub enum NumberingKind { LowerRoman, /// Uppercase Roman numerals (I, II, III, etc.). UpperRoman, - /// Lowercase Greek numerals (Α, Β, Γ, etc.). + /// Lowercase Greek letters (α, β, γ, etc.). LowerGreek, - /// Uppercase Greek numerals (α, β, γ, etc.). + /// Uppercase Greek letters (Α, Β, Γ, etc.). UpperGreek, /// Paragraph/note-like symbols: *, †, ‡, §, ¶, and ‖. Further items use /// repeated symbols. From 4329a15a1cb44a849c9b6a8cd932867b4aa53ed0 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Mon, 2 Jun 2025 14:04:49 +0100 Subject: [PATCH 128/172] Improve `calc.round` documentation (#6345) --- crates/typst-library/src/foundations/calc.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/typst-library/src/foundations/calc.rs b/crates/typst-library/src/foundations/calc.rs index a8e0eaeb3..7f481a23b 100644 --- a/crates/typst-library/src/foundations/calc.rs +++ b/crates/typst-library/src/foundations/calc.rs @@ -708,12 +708,13 @@ pub fn fract( } } -/// Rounds a number to the nearest integer away from zero. +/// Rounds a number to the nearest integer. /// -/// Optionally, a number of decimal places can be specified. +/// Half-integers are rounded away from zero. /// -/// If the number of digits is negative, its absolute value will indicate the -/// amount of significant integer digits to remove before the decimal point. +/// Optionally, a number of decimal places can be specified. If negative, its +/// absolute value will indicate the amount of significant integer digits to +/// remove before the decimal point. /// /// Note that this function will return the same type as the operand. That is, /// applying `round` to a [`float`] will return a `float`, and to a [`decimal`], From fd08c4bb3f55400e0fb9f461f463da19169a04a0 Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Mon, 2 Jun 2025 16:12:42 +0300 Subject: [PATCH 129/172] Fix typo in layout docs, change "size" to "height" (#6344) --- crates/typst-library/src/layout/layout.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-library/src/layout/layout.rs b/crates/typst-library/src/layout/layout.rs index 04aaee944..46271ff22 100644 --- a/crates/typst-library/src/layout/layout.rs +++ b/crates/typst-library/src/layout/layout.rs @@ -41,7 +41,7 @@ use crate::layout::{BlockElem, Size}; /// receives the page's dimensions minus its margins. This is mostly useful in /// combination with [measurement]($measure). /// -/// To retrieve the _remaining_ size of the page rather than its full size, you +/// To retrieve the _remaining_ height of the page rather than its full size, /// you can wrap your `layout` call in a `{block(height: 1fr)}`. This works /// because the block automatically grows to fill the remaining space (see the /// [fraction] documentation for more details). From 6164ade9cecf1f7bf475d24e0123c3664b8490a8 Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Mon, 2 Jun 2025 16:15:04 +0200 Subject: [PATCH 130/172] Add `typst-html` to architecture crates list (#6364) --- docs/dev/architecture.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md index bbae06792..3620d4fda 100644 --- a/docs/dev/architecture.md +++ b/docs/dev/architecture.md @@ -12,6 +12,7 @@ Let's start with a broad overview of the directories in this repository: - `crates/typst-cli`: Typst's command line interface. This is a relatively small layer on top of the compiler and the exporters. - `crates/typst-eval`: The interpreter for the Typst language. +- `crates/typst-html`: The HTML exporter. - `crates/typst-ide`: Exposes IDE functionality. - `crates/typst-kit`: Contains various default implementation of functionality used in `typst-cli`. From e023db5f1dea8b0273eec0f528d6ae0fed118a65 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 2 Jun 2025 18:44:43 +0200 Subject: [PATCH 131/172] Bump Rust to 1.87 in CI (#6367) --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 2 +- crates/typst-kit/src/download.rs | 3 +-- crates/typst-library/src/text/deco.rs | 1 + crates/typst-macros/src/cast.rs | 1 + crates/typst/src/lib.rs | 1 - flake.lock | 6 +++--- flake.nix | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41f17d137..c5c81537b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: sudo dpkg --add-architecture i386 sudo apt update sudo apt install -y gcc-multilib libssl-dev:i386 pkg-config:i386 - - uses: dtolnay/rust-toolchain@1.85.0 + - uses: dtolnay/rust-toolchain@1.87.0 with: targets: ${{ matrix.bits == 32 && 'i686-unknown-linux-gnu' || '' }} - uses: Swatinem/rust-cache@v2 @@ -73,7 +73,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.85.0 + - uses: dtolnay/rust-toolchain@1.87.0 with: components: clippy, rustfmt - uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0d235aec5..ca317abd0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,7 +44,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.85.0 + - uses: dtolnay/rust-toolchain@1.87.0 with: target: ${{ matrix.target }} diff --git a/crates/typst-kit/src/download.rs b/crates/typst-kit/src/download.rs index 40084e51b..a4d49b4f3 100644 --- a/crates/typst-kit/src/download.rs +++ b/crates/typst-kit/src/download.rs @@ -128,8 +128,7 @@ impl Downloader { } // Configure native TLS. - let connector = - tls.build().map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; + let connector = tls.build().map_err(io::Error::other)?; builder = builder.tls_connector(Arc::new(connector)); builder.build().get(url).call() diff --git a/crates/typst-library/src/text/deco.rs b/crates/typst-library/src/text/deco.rs index 485d0edcf..7aa06e815 100644 --- a/crates/typst-library/src/text/deco.rs +++ b/crates/typst-library/src/text/deco.rs @@ -373,6 +373,7 @@ pub struct Decoration { /// A kind of decorative line. #[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[allow(clippy::large_enum_variant)] pub enum DecoLine { Underline { stroke: Stroke, diff --git a/crates/typst-macros/src/cast.rs b/crates/typst-macros/src/cast.rs index b90b78886..6f4b2b95c 100644 --- a/crates/typst-macros/src/cast.rs +++ b/crates/typst-macros/src/cast.rs @@ -185,6 +185,7 @@ struct Cast { } /// A pattern in a cast, e.g.`"ascender"` or `v: i64`. +#[allow(clippy::large_enum_variant)] enum Pattern { Str(syn::LitStr), Ty(syn::Pat, syn::Type), diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs index 580ba9e80..a6bb4fe38 100644 --- a/crates/typst/src/lib.rs +++ b/crates/typst/src/lib.rs @@ -27,7 +27,6 @@ //! [module]: crate::foundations::Module //! [content]: crate::foundations::Content //! [laid out]: typst_layout::layout_document -//! [document]: crate::model::Document //! [frame]: crate::layout::Frame pub extern crate comemo; diff --git a/flake.lock b/flake.lock index ad47d29cd..dedfbb4e0 100644 --- a/flake.lock +++ b/flake.lock @@ -112,13 +112,13 @@ "rust-manifest": { "flake": false, "locked": { - "narHash": "sha256-irgHsBXecwlFSdmP9MfGP06Cbpca2QALJdbN4cymcko=", + "narHash": "sha256-BwfxWd/E8gpnXoKsucFXhMbevMlVgw3l0becLkIcWCU=", "type": "file", - "url": "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml" + "url": "https://static.rust-lang.org/dist/channel-rust-1.87.0.toml" }, "original": { "type": "file", - "url": "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml" + "url": "https://static.rust-lang.org/dist/channel-rust-1.87.0.toml" } }, "systems": { diff --git a/flake.nix b/flake.nix index 6938f6e57..1b2b3abc8 100644 --- a/flake.nix +++ b/flake.nix @@ -10,7 +10,7 @@ inputs.nixpkgs.follows = "nixpkgs"; }; rust-manifest = { - url = "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml"; + url = "https://static.rust-lang.org/dist/channel-rust-1.87.0.toml"; flake = false; }; }; From 664d33a68178239a9b9799d5c1b9e08958dd8d5c Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 2 Jun 2025 18:53:35 +0200 Subject: [PATCH 132/172] Be a bit lazier in function call evaluation (#6368) --- crates/typst-eval/src/call.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index fa9683416..eaeabbab3 100644 --- a/crates/typst-eval/src/call.rs +++ b/crates/typst-eval/src/call.rs @@ -25,15 +25,13 @@ impl Eval for ast::FuncCall<'_> { fn eval(self, vm: &mut Vm) -> SourceResult { let span = self.span(); let callee = self.callee(); - let in_math = in_math(callee); let callee_span = callee.span(); let args = self.args(); - let trailing_comma = args.trailing_comma(); vm.engine.route.check_call_depth().at(span)?; // Try to evaluate as a call to an associated function or field. - let (callee, args) = if let ast::Expr::FieldAccess(access) = callee { + let (callee_value, args_value) = if let ast::Expr::FieldAccess(access) = callee { let target = access.target(); let field = access.field(); match eval_field_call(target, field, args, span, vm)? { @@ -50,9 +48,15 @@ impl Eval for ast::FuncCall<'_> { (callee.eval(vm)?, args.eval(vm)?.spanned(span)) }; - let func_result = callee.clone().cast::(); - if in_math && func_result.is_err() { - return wrap_args_in_math(callee, callee_span, args, trailing_comma); + let func_result = callee_value.clone().cast::(); + + if func_result.is_err() && in_math(callee) { + return wrap_args_in_math( + callee_value, + callee_span, + args_value, + args.trailing_comma(), + ); } let func = func_result @@ -61,8 +65,11 @@ impl Eval for ast::FuncCall<'_> { let point = || Tracepoint::Call(func.name().map(Into::into)); let f = || { - func.call(&mut vm.engine, vm.context, args) - .trace(vm.world(), point, span) + func.call(&mut vm.engine, vm.context, args_value).trace( + vm.world(), + point, + span, + ) }; // Stacker is broken on WASM. From ff0dc5ab6608504c802d6965587151caf2c757f6 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Tue, 3 Jun 2025 15:38:21 +0300 Subject: [PATCH 133/172] Add Latvian translations (#6348) --- crates/typst-library/src/text/lang.rs | 4 +++- crates/typst-library/translations/lv.txt | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 crates/typst-library/translations/lv.txt diff --git a/crates/typst-library/src/text/lang.rs b/crates/typst-library/src/text/lang.rs index 2cc66a261..f9f13c783 100644 --- a/crates/typst-library/src/text/lang.rs +++ b/crates/typst-library/src/text/lang.rs @@ -14,7 +14,7 @@ macro_rules! translation { }; } -const TRANSLATIONS: [(&str, &str); 39] = [ +const TRANSLATIONS: [(&str, &str); 40] = [ translation!("ar"), translation!("bg"), translation!("ca"), @@ -36,6 +36,7 @@ const TRANSLATIONS: [(&str, &str); 39] = [ translation!("it"), translation!("ja"), translation!("la"), + translation!("lv"), translation!("nb"), translation!("nl"), translation!("nn"), @@ -87,6 +88,7 @@ impl Lang { pub const ITALIAN: Self = Self(*b"it ", 2); pub const JAPANESE: Self = Self(*b"ja ", 2); pub const LATIN: Self = Self(*b"la ", 2); + pub const LATVIAN: Self = Self(*b"lv ", 2); pub const LOWER_SORBIAN: Self = Self(*b"dsb", 3); pub const NYNORSK: Self = Self(*b"nn ", 2); pub const POLISH: Self = Self(*b"pl ", 2); diff --git a/crates/typst-library/translations/lv.txt b/crates/typst-library/translations/lv.txt new file mode 100644 index 000000000..4c6b86841 --- /dev/null +++ b/crates/typst-library/translations/lv.txt @@ -0,0 +1,8 @@ +figure = Attēls +table = Tabula +equation = Vienādojums +bibliography = Literatūra +heading = Sadaļa +outline = Saturs +raw = Saraksts +page = lpp. From 1b399646c270d518af250db3afb7ba35992e8751 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 14:53:13 +0200 Subject: [PATCH 134/172] Bump crossbeam-channel from 0.5.14 to 0.5.15 (#6369) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b70e06bc..30a4db7a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -508,9 +508,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] From dd95f7d59474800a83a4d397dd13e34de35d56be Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 3 Jun 2025 14:08:18 +0000 Subject: [PATCH 135/172] Fix bottom accent positioning in math (#6187) --- crates/typst-layout/src/math/accent.rs | 59 +++++++++++++-------- crates/typst-layout/src/math/attach.rs | 6 ++- crates/typst-layout/src/math/fragment.rs | 33 ++++++++---- crates/typst-layout/src/math/stretch.rs | 2 +- crates/typst-library/src/math/accent.rs | 13 +++++ tests/ref/math-accent-bottom-high-base.png | Bin 0 -> 572 bytes tests/ref/math-accent-bottom-sized.png | Bin 0 -> 382 bytes tests/ref/math-accent-bottom-subscript.png | Bin 0 -> 417 bytes tests/ref/math-accent-bottom-wide-base.png | Bin 0 -> 359 bytes tests/ref/math-accent-bottom.png | Bin 0 -> 622 bytes tests/ref/math-accent-nested.png | Bin 0 -> 537 bytes tests/suite/math/accent.typ | 28 ++++++++++ 12 files changed, 108 insertions(+), 33 deletions(-) create mode 100644 tests/ref/math-accent-bottom-high-base.png create mode 100644 tests/ref/math-accent-bottom-sized.png create mode 100644 tests/ref/math-accent-bottom-subscript.png create mode 100644 tests/ref/math-accent-bottom-wide-base.png create mode 100644 tests/ref/math-accent-bottom.png create mode 100644 tests/ref/math-accent-nested.png diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs index 73d821019..53dfdf055 100644 --- a/crates/typst-layout/src/math/accent.rs +++ b/crates/typst-layout/src/math/accent.rs @@ -1,7 +1,7 @@ use typst_library::diag::SourceResult; use typst_library::foundations::{Packed, StyleChain}; use typst_library::layout::{Em, Frame, Point, Size}; -use typst_library::math::{Accent, AccentElem}; +use typst_library::math::AccentElem; use super::{style_cramped, FrameFragment, GlyphFragment, MathContext, MathFragment}; @@ -18,8 +18,11 @@ pub fn layout_accent( let cramped = style_cramped(); let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?; - // Try to replace a glyph with its dotless variant. - if elem.dotless(styles) { + let accent = elem.accent; + let top_accent = !accent.is_bottom(); + + // Try to replace base glyph with its dotless variant. + if top_accent && elem.dotless(styles) { if let MathFragment::Glyph(glyph) = &mut base { glyph.make_dotless_form(ctx); } @@ -29,41 +32,54 @@ pub fn layout_accent( let base_class = base.class(); let base_attach = base.accent_attach(); - let width = elem.size(styles).relative_to(base.width()); + let mut glyph = GlyphFragment::new(ctx, styles, accent.0, elem.span()); - let Accent(c) = elem.accent; - let mut glyph = GlyphFragment::new(ctx, styles, c, elem.span()); - - // Try to replace accent glyph with flattened variant. - let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); - if base.ascent() > flattened_base_height { - glyph.make_flattened_accent_form(ctx); + // Try to replace accent glyph with its flattened variant. + if top_accent { + let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); + if base.ascent() > flattened_base_height { + glyph.make_flattened_accent_form(ctx); + } } // Forcing the accent to be at least as large as the base makes it too // wide in many case. + let width = elem.size(styles).relative_to(base.width()); 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; + let accent_attach = variant.accent_attach.0; + + let (gap, accent_pos, base_pos) = if top_accent { + // 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.ascent().min(accent_base_height); + let accent_pos = Point::with_x(base_attach.0 - accent_attach); + let base_pos = Point::with_y(accent.height() + gap); + (gap, accent_pos, base_pos) + } else { + let gap = -accent.ascent(); + let accent_pos = Point::new(base_attach.1 - accent_attach, base.height() + gap); + let base_pos = Point::zero(); + (gap, accent_pos, base_pos) + }; - // 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.ascent().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 base_descent = match &base { + MathFragment::Frame(frame) => frame.base_descent, + _ => base.descent(), + }; let mut frame = Frame::soft(size); frame.set_baseline(baseline); @@ -73,6 +89,7 @@ pub fn layout_accent( FrameFragment::new(styles, frame) .with_class(base_class) .with_base_ascent(base_ascent) + .with_base_descent(base_descent) .with_italics_correction(base_italics_correction) .with_accent_attach(base_attach) .with_text_like(base_text_like), diff --git a/crates/typst-layout/src/math/attach.rs b/crates/typst-layout/src/math/attach.rs index e1d7d7c9d..90aad941e 100644 --- a/crates/typst-layout/src/math/attach.rs +++ b/crates/typst-layout/src/math/attach.rs @@ -434,9 +434,13 @@ fn compute_script_shifts( } if bl.is_some() || br.is_some() { + let descent = match &base { + MathFragment::Frame(frame) => frame.base_descent, + _ => base.descent(), + }; shift_down = shift_down .max(sub_shift_down) - .max(if is_text_like { Abs::zero() } else { base.descent() + sub_drop_min }) + .max(if is_text_like { Abs::zero() } else { descent + sub_drop_min }) .max(measure!(bl, ascent) - sub_top_max) .max(measure!(br, ascent) - sub_top_max); } diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs index 59858a9cb..85101c486 100644 --- a/crates/typst-layout/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -164,12 +164,12 @@ impl MathFragment { } } - pub fn accent_attach(&self) -> Abs { + pub fn accent_attach(&self) -> (Abs, Abs) { match self { Self::Glyph(glyph) => glyph.accent_attach, Self::Variant(variant) => variant.accent_attach, Self::Frame(fragment) => fragment.accent_attach, - _ => self.width() / 2.0, + _ => (self.width() / 2.0, self.width() / 2.0), } } @@ -241,7 +241,7 @@ pub struct GlyphFragment { pub ascent: Abs, pub descent: Abs, pub italics_correction: Abs, - pub accent_attach: Abs, + pub accent_attach: (Abs, Abs), pub font_size: Abs, pub class: MathClass, pub math_size: MathSize, @@ -296,7 +296,7 @@ impl GlyphFragment { descent: Abs::zero(), limits: Limits::for_char(c), italics_correction: Abs::zero(), - accent_attach: Abs::zero(), + accent_attach: (Abs::zero(), Abs::zero()), class, span, modifiers: FrameModifiers::get_in(styles), @@ -328,8 +328,14 @@ impl GlyphFragment { }); let mut width = advance.scaled(ctx, self.font_size); - let accent_attach = + + // The fallback for accents is half the width plus or minus the italics + // correction. This is similar to how top and bottom attachments are + // shifted. For bottom accents we do not use the accent attach of the + // base as it is meant for top acccents. + let top_accent_attach = accent_attach(ctx, id, self.font_size).unwrap_or((width + italics) / 2.0); + let bottom_accent_attach = (width - italics) / 2.0; let extended_shape = is_extended_shape(ctx, id); if !extended_shape { @@ -341,7 +347,7 @@ impl GlyphFragment { self.ascent = bbox.y_max.scaled(ctx, self.font_size); self.descent = -bbox.y_min.scaled(ctx, self.font_size); self.italics_correction = italics; - self.accent_attach = accent_attach; + self.accent_attach = (top_accent_attach, bottom_accent_attach); self.extended_shape = extended_shape; } @@ -459,7 +465,7 @@ impl Debug for GlyphFragment { pub struct VariantFragment { pub c: char, pub italics_correction: Abs, - pub accent_attach: Abs, + pub accent_attach: (Abs, Abs), pub frame: Frame, pub font_size: Abs, pub class: MathClass, @@ -501,8 +507,9 @@ pub struct FrameFragment { pub limits: Limits, pub spaced: bool, pub base_ascent: Abs, + pub base_descent: Abs, pub italics_correction: Abs, - pub accent_attach: Abs, + pub accent_attach: (Abs, Abs), pub text_like: bool, pub ignorant: bool, } @@ -510,6 +517,7 @@ pub struct FrameFragment { impl FrameFragment { pub fn new(styles: StyleChain, frame: Frame) -> Self { let base_ascent = frame.ascent(); + let base_descent = frame.descent(); let accent_attach = frame.width() / 2.0; Self { frame: frame.modified(&FrameModifiers::get_in(styles)), @@ -519,8 +527,9 @@ impl FrameFragment { limits: Limits::Never, spaced: false, base_ascent, + base_descent, italics_correction: Abs::zero(), - accent_attach, + accent_attach: (accent_attach, accent_attach), text_like: false, ignorant: false, } @@ -542,11 +551,15 @@ impl FrameFragment { Self { base_ascent, ..self } } + pub fn with_base_descent(self, base_descent: Abs) -> Self { + Self { base_descent, ..self } + } + pub fn with_italics_correction(self, italics_correction: Abs) -> Self { Self { italics_correction, ..self } } - pub fn with_accent_attach(self, accent_attach: Abs) -> Self { + pub fn with_accent_attach(self, accent_attach: (Abs, Abs)) -> Self { Self { accent_attach, ..self } } diff --git a/crates/typst-layout/src/math/stretch.rs b/crates/typst-layout/src/math/stretch.rs index f45035e27..6157d0c50 100644 --- a/crates/typst-layout/src/math/stretch.rs +++ b/crates/typst-layout/src/math/stretch.rs @@ -278,7 +278,7 @@ fn assemble( } let accent_attach = match axis { - Axis::X => frame.width() / 2.0, + Axis::X => (frame.width() / 2.0, frame.width() / 2.0), Axis::Y => base.accent_attach, }; diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs index e62b63872..f2c9168c2 100644 --- a/crates/typst-library/src/math/accent.rs +++ b/crates/typst-library/src/math/accent.rs @@ -80,6 +80,19 @@ impl Accent { pub fn new(c: char) -> Self { Self(Self::combine(c).unwrap_or(c)) } + + /// List of bottom accents. Currently just a list of ones included in the + /// Unicode math class document. + const BOTTOM: &[char] = &[ + '\u{0323}', '\u{032C}', '\u{032D}', '\u{032E}', '\u{032F}', '\u{0330}', + '\u{0331}', '\u{0332}', '\u{0333}', '\u{033A}', '\u{20E8}', '\u{20EC}', + '\u{20ED}', '\u{20EE}', '\u{20EF}', + ]; + + /// Whether this accent is a bottom accent or not. + pub fn is_bottom(&self) -> bool { + Self::BOTTOM.contains(&self.0) + } } /// This macro generates accent-related functions. diff --git a/tests/ref/math-accent-bottom-high-base.png b/tests/ref/math-accent-bottom-high-base.png new file mode 100644 index 0000000000000000000000000000000000000000..23b14467280d93fba150194ded116d6b15fea4da GIT binary patch literal 572 zcmV-C0>k}@P)v*+FZC^6au}I+YU#J7le%9ciTqo1 z?0@j>I;vZ|cvTD%?|V$|$+DltUy+2SZAku9`=WvB7TbSE=B?=e|9|(g#mGWdz)+sq z^orURzqUr<#-2o1aNnIasd`-@3Rm?Px`G9Dw6VBw5i;+A5r%@~^|Z0r_yS5wOTi-UTRc?ab% z6u8W$jm1iPk$Dq+F%*cNqKU;HnBRb?KTvLBErdo0Jz@P$6N?Y3fT;>-8ELcyp4zn_ z^cEx9)W;K{Y3nqY{LKOKKeEXGmTcNsoID*&o&5r)E~z7A4nU~TCA6{F45i#!idk-H z+@*=d|D;eF78P^Q6?~VaO`Geg9}3s)9=d`PF|@IG-eeU28)GKE1+=j^@(D8USQ&-_ z|97;oIPh@^a>`m!{1Vt8*>M~p6eS`jE?aOks3{>``;4F15zPWt)@};Hll(iDbEx)$y`0Dql%5ELnaGLr){`eY& zdwXzn+U{;yvvTF~m21~5U$Js&+nE9CEnXV!ACFo*YVoMWv;_b(vVqN&POabo0000< KMNUMnLSTaLKoLv; literal 0 HcmV?d00001 diff --git a/tests/ref/math-accent-bottom-sized.png b/tests/ref/math-accent-bottom-sized.png new file mode 100644 index 0000000000000000000000000000000000000000..5455b2f5b260a860704717196db8b988e54b12f6 GIT binary patch literal 382 zcmV-^0fGLBP)^*)K^trgXzr>?sZh||E-Dt|M$1iB#vkLpvcYkLgAJ!`u{)R7;O?)@B|cY zXdg1qw14hwq2IKz_^lNR_njpY=cCI1|L5Ill%4u-K|Nmz}sk#rsE&2cd3?zfmC?CJ~ z+PZ!F)`j&_)BfN1@c;h@2)99M>i?S`{@+?ulo%VG-n{!G^(}t71`0l+a)FN6I&J-} zzu**heD3TAG)P>J9)f}xS>)k&dQ=h%dJ3bb7e0aWccrDI(WQ0*)($_9BYE-OXnj0t c@ldoF0CeBVg-HF}4gdfE07*qoM6N<$f{N|D(*OVf literal 0 HcmV?d00001 diff --git a/tests/ref/math-accent-bottom-subscript.png b/tests/ref/math-accent-bottom-subscript.png new file mode 100644 index 0000000000000000000000000000000000000000..818544445587a485c7152e543f2ab8a8536a89eb GIT binary patch literal 417 zcmV;S0bc%zP)_^QKU;L<91?f&9SD7A;fbqA z!u=m1^o4~dA5h2QFTuYnw?ocO(txH$&){ga6&A6x7v+e!2Ze_}Hx(;^TySZ-W0LeC*N+ z@v-~&%Q;lC_}ZM#lK)3mY~2Cj)EFQAPIJ$!K0Gkh;;a*+4S`XM2dBjVM)yn!G36@500000 LNkvXXu0mjfK$6xw literal 0 HcmV?d00001 diff --git a/tests/ref/math-accent-bottom-wide-base.png b/tests/ref/math-accent-bottom-wide-base.png new file mode 100644 index 0000000000000000000000000000000000000000..0475b4856bd49b9a4f7663564747ca9635654028 GIT binary patch literal 359 zcmV-t0hs=YP)YZomn-m~=V zl5b$v%y(ew?VOkY4?^UAnQW(79ACM#xNh-NWQ&*1U;8l#DtlA;1+6Up_rLfoh`!X; z7Es&P0gTkYq3^f$!=$_B(#qnSKdt{B`;YA7|33dq&i#k@^nE4Gg1U2N#fqb-7I$t_ z{rBJc4UqM>E7q-Fxq8i-)hk!7Uo+d~H&puT z)UEU^tN(0-&~K-;pWV{3{2$$GCrE1hHd-H#T08_S1^~|x+^P)&N!S1Y002ovPDHLk FV1n_Aw{-vj literal 0 HcmV?d00001 diff --git a/tests/ref/math-accent-bottom.png b/tests/ref/math-accent-bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..bd1b921460ca17d139552bd63f73144da916bc91 GIT binary patch literal 622 zcmV-!0+IcRP)zM(@7Ur)P)swAt_M<6Ut1fP)c+<8ciD}Qd5Z3L@6vA ziem*X(`{^sdt+OBld~7U<#W!t5GI`3MM3*;E`IQE_})EF8GlJxDO?Jd!v8}!59;!8 zu*7=tVPmV|(BlV9O08oS#pSwfgwA8b1N9^9Goe$L{Y-uM&%#4QkAPY1RIP0VS+G$; z_N)zj)9f?fjH>B9zZKrL%y{Jjlb0YQwy6t6x~*;#+4(n;HIo9!kJ$OJ3-Cpc>aYYr zwBIqlt|6Q_mC?UO)?!AnR^1Xrc7F~<`P15u2-c;WREy>BMX^zZFgARLdJW;eJ{ls( zD!eEjrBLa@$UK#N&LZUREd%b`B^5r~<+{ zz;pwEogY9=er!;jr#(a^a5r+=0r50&lBwGo!pC!Vs8xn$ehJ{x>MkEpy$wUxEP;`N| zRA|MsNYUxc-?Z8?t?g+&=43MSpZmYb@1B#Jdu{^a6bTAg0RwiHWgq`l#@1!t@JtK7 zS6jZD!y^2MdpL$w3mpmcV!;o<8BYVc-uSUwGy!djb64Q^#Z$_@ADa^_O0 z!KzS}wz{GPO;C$HLJ~1pNEU!Azp~7>L*@Dy?Q8B&^wNU2bAYwIfUMf%Q^bI}n*`Dv zq~K>wwQWX)gGNcGTRRNUZ3B*i8$4u8lY;F_QrO@b1nvrrhrr1qVCGyn;yB{X^@1xW zd>qhG;s4#e^}l(Z7l}95R#|b$E4%`*yYsOQDa>kv!9)zc_@XP%LrRQocXEXV!!R51 zXp1hrUT=y^KNdw%x$G(6Uki4)aeFDZ_W?6KSk!6SBJ2Zg&D!5B^<~!yyC`?L`IS*8 zIe5H5hdk+PWdiX8c;?g2)|<(}LjI#SOYT~W(LF{F&cdozmNAA#pr$XiqXkVyx!ZJs z%$OV?xt>V>xI8A{$|!%%NDmgGT|s*2!RFS9`iN5Z4EIjPpw#XSU!-k0RobEgR=^7Q b$ARAgn*4* Date: Tue, 3 Jun 2025 17:42:22 +0300 Subject: [PATCH 136/172] Change Russian secondary smart quotes & remove alternatives (#6331) --- crates/typst-library/src/text/smartquote.rs | 5 +++-- tests/ref/smartquote-ru.png | Bin 1877 -> 1886 bytes 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/typst-library/src/text/smartquote.rs b/crates/typst-library/src/text/smartquote.rs index 4dda689df..270d8f0f3 100644 --- a/crates/typst-library/src/text/smartquote.rs +++ b/crates/typst-library/src/text/smartquote.rs @@ -237,7 +237,7 @@ impl<'s> SmartQuotes<'s> { "cs" | "da" | "de" | "sk" | "sl" if alternative => ("›", "‹", "»", "«"), "cs" | "de" | "et" | "is" | "lt" | "lv" | "sk" | "sl" => low_high, "da" => ("‘", "’", "“", "”"), - "fr" | "ru" if alternative => default, + "fr" if alternative => default, "fr" => ("“", "”", "«\u{202F}", "\u{202F}»"), "fi" | "sv" if alternative => ("’", "’", "»", "»"), "bs" | "fi" | "sv" => ("’", "’", "”", "”"), @@ -247,7 +247,8 @@ impl<'s> SmartQuotes<'s> { "es" if matches!(region, Some("ES") | None) => ("“", "”", "«", "»"), "hu" | "pl" | "ro" => ("’", "’", "„", "”"), "no" | "nb" | "nn" if alternative => low_high, - "ru" | "no" | "nb" | "nn" | "uk" => ("’", "’", "«", "»"), + "no" | "nb" | "nn" | "uk" => ("’", "’", "«", "»"), + "ru" => ("„", "“", "«", "»"), "el" => ("‘", "’", "«", "»"), "he" => ("’", "’", "”", "”"), "hr" => ("‘", "’", "„", "”"), diff --git a/tests/ref/smartquote-ru.png b/tests/ref/smartquote-ru.png index 05c79263f6797e3072ee9cb867431b4fb334c28b..867121d1e32d470b9b4c7f4daf8bb37b9a521a4f 100644 GIT binary patch delta 1871 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 A{{R30 From 4a8367e90add92a0a316bcc258e39d522b484c25 Mon Sep 17 00:00:00 2001 From: Nazar Serhiichuk <43041209+G1gg1L3s@users.noreply.github.com> Date: Wed, 4 Jun 2025 11:13:39 +0300 Subject: [PATCH 137/172] Fix Ukrainian secondary smart quotes (#6372) --- crates/typst-library/src/text/smartquote.rs | 3 ++- tests/ref/smartquote-uk.png | Bin 0 -> 1971 bytes tests/suite/text/smartquote.typ | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 tests/ref/smartquote-uk.png diff --git a/crates/typst-library/src/text/smartquote.rs b/crates/typst-library/src/text/smartquote.rs index 270d8f0f3..09cefd013 100644 --- a/crates/typst-library/src/text/smartquote.rs +++ b/crates/typst-library/src/text/smartquote.rs @@ -247,8 +247,9 @@ impl<'s> SmartQuotes<'s> { "es" if matches!(region, Some("ES") | None) => ("“", "”", "«", "»"), "hu" | "pl" | "ro" => ("’", "’", "„", "”"), "no" | "nb" | "nn" if alternative => low_high, - "no" | "nb" | "nn" | "uk" => ("’", "’", "«", "»"), + "no" | "nb" | "nn" => ("’", "’", "«", "»"), "ru" => ("„", "“", "«", "»"), + "uk" => ("“", "”", "«", "»"), "el" => ("‘", "’", "«", "»"), "he" => ("’", "’", "”", "”"), "hr" => ("‘", "’", "„", "”"), diff --git a/tests/ref/smartquote-uk.png b/tests/ref/smartquote-uk.png new file mode 100644 index 0000000000000000000000000000000000000000..7ac1c032e583b0c3a691097b9b6fadaeaaaa7e68 GIT binary patch literal 1971 zcmV;k2Tb^hP)4J*jj{4nWRBDBRDmr&odJm6K)-=ECtQfMYTp8?isUFAc89S>MeP4f6ru+b))l-!sjh*$XV? zOzmJ!W`oJ|CbuX)9`4*!bQbL6J7=qLl4B&5%>jeB!)?H(2-$3$z1vy=1Q_%K4JP1y z&dcwkVM=1d9&$bmp$9eztnOsdC=0k639M7HSe?HWr3RRbX&}#dyc3*by9FN)(?Lh$ zMTcIsTd|FEt);B`OlryL$beC(vrKqwk&M4xHjs7t{(zlx7s0}&0b7yyv3@}GupXWS z_R{hGs+F132CJE*4xf~%hQlPcDKd3GS#L7tE_L@*mzx!4`;r(W(N-S`^HZlCD$YMz zBRNtV^K%Q38SYDQqCaXI(P%n&VHbrL%0f`%s6|vzNDO&SNVx#6lk_cG$ zY8}v#^=o6l2aYz!C!Bu(hUyG!!o@5YUD#H#O_$=p@-d$v7KJu$q-||<)+wNPou>WG zHulu1?r@~+p(O@7Vy{7TzDwhW-dN%ls4Frq;L(A^jBf))I>o-i;!7mD`l_o;#BM#Q zyIX<})dcSwxptxj2)0b&LZqvoO>)mk_o36V7gM#imVGqonm^_p-*xy*17fzORX>?} zSP%dIaG6l-XH~2VOX*@|0iBxFYmGXU_n?eVVH1WBrCw(%8al?c=RExb1w%sk#MB`WyY5@tMZUB-1W zd#ac}s<3YVyA}%84_Az65|rD~|W`RWDWv$Pj?w>3som`{4J(3sDQW9Eb`-v|r;J z&Y`C)(@M`V0w~PMwtcV}&FFA)?*d|eW(4mD>{2iJ~9F1SpQm_hNFF{k57D z48TA#ws_JT=Y6M;Z>dz8xaIL>@cHUd^RuD*or1T>RmU9e0E*=yRwGyor5Y$RlB-W# zkqwhUzEY|qa7YW@jZxddfm|W z#;1_xR+Hpbk36;;h!chSc=V1%M08FD>t-@Ck0fcwifp5S$TMT}hgAXehrKZ3{vH6j zSFw7maQEO=M-o@QXj-2-zExFx0Q3A*C(6@30y#M76h5$+S8EALum1t4+fIfBhoASY zP)`CgUnUc<`$kMk{z(QFaMM3_nC61lJE-|CG8h|}V0y_VRX*wF9X$M)C zSlY^lH#t#7Gp)@`U zERmfPRdsB)am=~qr7=DBHe_$m%h+#fBG%Q{ZuQIu+`n(P+u2cjOnm!;1|%5b??H9Q zr(Ld5vnMbL@b=TfsPvz=(*sS=K^13FlcTlP!mp-62dh%uOlxb^@efh+_cs8~p+&1c^L}@Wc=dA>8;r z0J-bhspo|002ovPDHLk FV1f-SyG;N9 literal 0 HcmV?d00001 diff --git a/tests/suite/text/smartquote.typ b/tests/suite/text/smartquote.typ index f2af93ceb..6eab35076 100644 --- a/tests/suite/text/smartquote.typ +++ b/tests/suite/text/smartquote.typ @@ -46,6 +46,10 @@ #set text(lang: "ru") "Лошадь не ест салат из огурцов" - это была первая фраза, сказанная по 'телефону'. +--- smartquote-uk --- +#set text(lang: "uk") +"Кінь не їсть огірковий салат" — перше речення, коли-небудь вимовлене по 'телефону'. + --- smartquote-it --- #set text(lang: "it") "Il cavallo non mangia insalata di cetrioli" è stata la prima frase pronunciata al 'telefono'. From 128c40d839398374c69725a5b19c24e07fb23c3d Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 4 Jun 2025 08:20:54 +0000 Subject: [PATCH 138/172] Apply script-style to numbers consistently in math (#6320) --- crates/typst-layout/src/math/text.rs | 13 ++++--------- .../ref/issue-4828-math-number-multi-char.png | Bin 465 -> 461 bytes .../ref/issue-5489-matrix-stray-linebreak.png | Bin 644 -> 716 bytes tests/ref/math-attach-kerning-mixed.png | Bin 2418 -> 2419 bytes tests/ref/math-attach-limit-long.png | Bin 1941 -> 2060 bytes tests/ref/math-attach-prescripts.png | Bin 670 -> 687 bytes tests/ref/math-frac-precedence.png | Bin 3586 -> 3592 bytes tests/ref/math-root-frame-size-index.png | Bin 902 -> 897 bytes tests/ref/math-root-large-index.png | Bin 648 -> 640 bytes 9 files changed, 4 insertions(+), 9 deletions(-) diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs index 59ac5b089..7ecbcfbaf 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -65,18 +65,13 @@ fn layout_inline_text( // Small optimization for numbers. Note that this lays out slightly // differently to normal text and is worth re-evaluating in the future. let mut fragments = vec![]; - let is_single = text.chars().count() == 1; for unstyled_c in text.chars() { let c = styled_char(styles, unstyled_c, false); let mut glyph = GlyphFragment::new(ctx, styles, c, span); - if is_single { - // Duplicate what `layout_glyph` does exactly even if it's - // probably incorrect here. - match EquationElem::size_in(styles) { - MathSize::Script => glyph.make_script_size(ctx), - MathSize::ScriptScript => glyph.make_script_script_size(ctx), - _ => {} - } + match EquationElem::size_in(styles) { + MathSize::Script => glyph.make_script_size(ctx), + MathSize::ScriptScript => glyph.make_script_script_size(ctx), + _ => {} } fragments.push(glyph.into()); } diff --git a/tests/ref/issue-4828-math-number-multi-char.png b/tests/ref/issue-4828-math-number-multi-char.png index ff0a9bab97de1957f3ea99de261346c14f8ccd9d..b365645d33edd081c77563ba8b3cf23809a3d923 100644 GIT binary patch delta 435 zcmV;k0ZjhU1I+`FB!84iL_t(|+U?a(OOpWr$8ldyFQP-Iplf#MB%}#NflydP6hRd8 z;$gLD3!R(UA8>~P4-zh*(bdd^&g;H1zNcyzG1Z9X`iji({&# z2{+*;Oyl8=^thv&e$a4OZV>Y8z?AxRQo9%P4T394z}m>6@PEh}V4%_%I9>*b1c$&u z1fU!b3vWekuG4|HuP0xS8ppHVAd{|Uo~{B+w~j(C#pWKDz*RiU)0rO*7;~K23XAIFU&7bBVyYUs&mZGlf2dQ@3})r^G=Y^(+%C zb;9Mau&Ft;F@Ika=@R_j=7bn+*%gN2^Hf z0lCHc@b4?S%Pi)Ay_B%<%7xSr_BVl&I#teZ>vFed-z?k@4SyRQ{ppAnvhVGTm72HM dgq!gH{R0B4Q+)?#OgsPp002ovPDHLkV1jDm(rf?# delta 439 zcmV;o0Z9JM1JMJJB!8GmL_t(|+U?a%OVa@W$8q0JZ=pk{plfstp)9aNN(FU@APB?= z1gEA>=GttT;DQ1XJ1D4#k|-_F9x#V#)0sucYf`lp^vQWqM|=kr@!WGr_t z9x0mZz~X6-bI-fy7r^iQUO)9tl5%1-#^oR^9F0bMsV8vm3s&yKnOscn)a^gub#$+3 zkwt>F^Kh*g7JoLAK`n1eCrvRHhxFNuso=(7sSLYI%&jsor1Un?Sy-S}SUCN;t&c`- z#_pstM3!Oa5*_V#3l9fju2C}E`OEqPpBlQFVr3db!&^bJpRsv>78Sbgs?@CgacMX- zEE_2!u>(xJs|;W9XoioYKshcnymBFt!tOUPZDt)$8rC^)}K(W^&8G|wvij;GHxoR*=|j& zkr}%Am6P%dDTJgcXe6;CG<6O)tW(@6>Rjf4&C% zCk=X759{Ge6plD%G~i(C4m;ZA*AsMoHF{EB+lT88&&&Ws)_+Lc`JJ=!A zdf(P|x-^HPOna{U6%}DB2>=qi=#HQjUJ9U4Df1piMe@9 z!qsR|4#UK05H{|kC~r2ijL8nJ=Xj?#fm*MDH9Af9eWM29D`doGm2w~?7sD_+iCLv2 zd89VX8pzE0#(z@VuCOo?6T%-75|r{ET>@u26IPc~K{`|bU|>~;2W@0ZLEqdO?jYbn zA9_(qq9D=fY=7Lju$TdWfkD+_ixb4zvlF~o#<~ahK%oi+(CNbg&K|w60D#k?I^66d zGv8!ho4EA^FKyC%U}I(_kEjkGG@>NM^E~&3k2{xsEh&JIcs@9_#Rr+Ay#t>AL5__Fs>8Sn;48y0E(AOZ z>j07SD1YtBk5q@dLvrHb9zstMS@FR*B%^JYxx!dD0JdJ zhTM6`C}4Z4cs`o&lS}Kl24M`uD#KQ(=Yn-_REN9J6U|se0Ej(@7GZR8WEPzBU>(P& z!7#iFkR=@vEKKHhH{$2+bpYV3P!YBpY$g4att~E8zq}e=)E(vmVGRU1{ZB#<>tQ|o aul)p!nYUG3woT>$0000qHPo<(8zW_vLsD&q5(v17OUl+h!`3+F>=D@UUhHwf1S`*a5 z4t8)8e~1;o4s)E{ImWEi2U=^!qEi)ZkA=^HIL#WWNnY) z10Y(6QVZiD6#KzzJwmSZY8i?Q+twlM78Y~LF}<*fhwS1#7#MbAu{@w)#MQyECkqj8 zrWo!$>SU7PZS$AjOLchL7jCWtK=&~yhH?2Ek`&)ytNCp8paR7YTgOtoJZ%Op0ARUz z&+zN%4bsy3#+|G$sh-nY1BXkY1O>+>?I)pywXhZ*w%?lP&H~-n|Lp((002ovPDHLk FV1nTEEmQyi diff --git a/tests/ref/math-attach-kerning-mixed.png b/tests/ref/math-attach-kerning-mixed.png index 9d0bea27af9c7a50b79a0d8aed64dc01e3d45ae5..64a5468696799f0c560878e62a10d767948120cb 100644 GIT binary patch delta 2408 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 delta 1926 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 delta 645 zcmV;00($+g1)c?vB!6W|L_t(|+U?cPOOtT`$8j&6`bWCerGk>gOF`Hr42yVFeQN61czio(%9vkF2gw3aav6F}?Bw1#>~fOuNS(9t+c`j=O|ae*a5)?fKcX3< z>#4VMID8kB3V+CX!+DUD6Xni!?`PspiNbS5M%1m|DjBeJG@`hgEwD?TU%lM%mQ|$~ z_MMvIWP3wD5iNcZ+84x@okdQJ<^=Sjn!kGa0%x0O{Y$e)7 zw~z&li_Tvmp9DJThGqxlU~%04IdC^o{*j9jvrc z+)WSBQ$y!cKm1J?{&~D0OhoyvW&#AVHxXH1A`&tmM1-}1AI~ez0R10c%(FjCt(9=I zYI4u?yMDn4rFvzeaJ{Qg1Cl12fN0Bt$Y@gxP#+PweaB^Lee00000NkvXXu0mjfCBiTg diff --git a/tests/ref/math-frac-precedence.png b/tests/ref/math-frac-precedence.png index fd16f2e6bf3e043a81e4e6e3146a6b15c05fc559..bddcb43c33dbe87cd906241291093c7edb68abb7 100644 GIT binary patch delta 3590 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 A2LJ#7 delta 879 zcmV-#1CacI2ZjfbB!5jwL_t(|+U?fQPm^~Xz;XWpcbV5EJ8ZjC(|!pp7-thxxA8Zo+qF07G6+WG+2W*Sc5keK7Vh`HAX?Hw)`k07?zI* zaz!bqeL6o%3WghEY$$bPGp<@b%DAi90BSJ|^K?6ZniC95GF9%-YoO+Ta*)gpemg1z zzH#>4HSYGnS{;D#1a#Z13Z08!n*r88zas*c%d^rT<0UE1JOsQ4dode$m!(q&;Aa3^ z<$?&f*+4}cYJbhCdf<8{y)CwVZTSmO-i$jZAbE?d0nb=1)6Ci7kpyJh^kB1@(!^VULjcoX zApnk)Be|F?Numz{zQcBmCfbyPQq`1KWO2bPXbZr$B&>DO1{#&|nRIZrEq9RlHyE|P0i zJ=%O1FI{vuLv?;E3xw-eDQwEeh3g?bSN;)v)qmmRc$~Tus7*}XKYL?T;y`w#qi1hI z9Qy({-ZS8ZI`&n|DVVXxIZ#cU_6HUNsg4Xn1-Gyn++s8)cR6%-i` z??Jl2bjdn@gn^kO=VV3^sPiheeB%& zfN1(Bun^!|OOfFwPqb_ZmP-wOU}qrtl3b4QjQZx-GhhH}EPQ4vGA!F&E#pAtj{xJ2 zDkmQ2;7N5mdtOZdQTyrIjG(VN8048vYD+?cHCTf;t*3U+FG98!$i@Hw002ovPDHLk FV1fz?qpkn| diff --git a/tests/ref/math-root-large-index.png b/tests/ref/math-root-large-index.png index 85689823da386744b879c83bb6d72ccd8706d8db..29dd478fe568b02e112791b7bf41689967e0c209 100644 GIT binary patch delta 615 zcmV-t0+{`X1%L&RB!5RqL_t(|+U?feOH%S!K@7bKMTw*- zg-*!A9He4k=1ZeYA#FNSN2Qe8NHNPWmrae$y|HX=*><*bzUi)Zww(=jAiX%B+uy|( zo~x&beNv=_wXhb}!Us0|>smrxIBM(=DMWKzT^L6t`x4&twSTTtfNy=HVKrgFGh_@V z3JA~j42IN%BTK!;r6}1Y#{Zz?qg0KWFs_vrFACyat0;OqsxFMLT|mizVmNFnt?>eW z_bwq{Sc>gQ$h#M-+(05ZNyx!^D=?IfM<$CBh1mpHDK0iA0y4msd^wWvNkeL2JeyEA~5POTS+6GhyaWY4LWm$ z`>vp*deDVhAN$|&KRp$IRyX!002ovPDHLkV1gV2 BCT{=$ delta 623 zcmV-#0+9WH1&9TZB!5pyL_t(|+U?fOOB7)oz;XW|>C~Y^5FG;R5*{R~i-#hEZp!*@ zWW|D7SxYIBs2Sz1u5UzQCff!Pt0uT>3%=z`y0*2>y13)+G&}Q6y*=!XgAP=O`5d1g z4`2B49G*P>kvuzWhwZQ({$Im#S;T5sa<_5}FTPt1>$Bw?Lx0zr)$l^HdWR!h>Gme9 zg_Y(0zAc@j3U|JgtcABj?Y*-Za>|Uz7zn1ybZcRPziJe6Q=czlWTMY%n3Xmi18$?R z>@O~f=n&i3lQIu$%05jv`*yiR0aRrl0_C2^;8r5s?OD)SzT3HH-8}rTabHMWbs(?` z$-0A4N_g*qJ%3x!a4?z^Zv9wv0CkG_s#{$l<~qo>QyEm@@g`Ze>(c(-3|~BCORZO_ zthgYSG6|=9CaC;P4ufAozD*8oNM3O0WSe~Z1gd0)FZfKt107TI&p?B$$t*lt1r04% zc)ANTbVM4k(Fiz9!us$~v?9gKtD7xF2-Hy@>2erwUw>_C5|{Mh=lo|%Gfef|Xt^Sc zV#Qu?Cd|X5rLfryGn|v7Vz+t(+UFq{EmY$O25Moc4Q4o_MiDHXCeeU-c>81o=MIoA zASq0CLub)z0lY3`1k#B(^$HL#B#Ef#Q*#XSFkO}L2Y}3!aF#Y=4`ymOD z%NcnJYG_qY#gZRU;Jgzceci1c8dme`CBQfB9|O_HOSM39aIHTKB02w#42*@weSoMS z0x;g!8$L8lLperHO<<(vzU8lvvWpQ3L%WLo*|WoT*be_Se*k<$&z8G_6=VPa002ov JPDHLkV1nH`CQASS From 5f776c7372ffecbbe959fbfa968c8c91efaf0061 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 4 Jun 2025 09:41:08 +0000 Subject: [PATCH 139/172] Bump New CM fonts to version 7.0.2 (#6376) --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 30a4db7a7..347704b33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2863,7 +2863,7 @@ dependencies = [ [[package]] name = "typst-assets" version = "0.13.1" -source = "git+https://github.com/typst/typst-assets?rev=ab1295f#ab1295ff896444e51902e03c2669955e1d73604a" +source = "git+https://github.com/typst/typst-assets?rev=c74e539#c74e539b090070a0c66fd007c550f5b6d3b724bd" [[package]] name = "typst-cli" diff --git a/Cargo.toml b/Cargo.toml index bc563b980..0f871e211 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ typst-svg = { path = "crates/typst-svg", version = "0.13.1" } typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" } typst-timing = { path = "crates/typst-timing", version = "0.13.1" } typst-utils = { path = "crates/typst-utils", version = "0.13.1" } -typst-assets = { git = "https://github.com/typst/typst-assets", rev = "ab1295f" } +typst-assets = { git = "https://github.com/typst/typst-assets", rev = "c74e539" } typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "fddbf8b" } arrayvec = "0.7.4" az = "1.2" From 1de2095f67c9719a973868618c3548dd6083f534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Wed, 4 Jun 2025 11:54:03 +0200 Subject: [PATCH 140/172] Add support for WebP images (#6311) --- Cargo.lock | 1 + Cargo.toml | 2 +- crates/typst-ide/src/complete.rs | 4 +++- crates/typst-layout/src/image.rs | 1 + crates/typst-library/src/visualize/image/mod.rs | 4 ++-- crates/typst-library/src/visualize/image/raster.rs | 6 ++++++ crates/typst-svg/src/image.rs | 1 + docs/tutorial/1-writing.md | 2 +- tests/suite/visualize/image.typ | 2 +- 9 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 347704b33..a9b3756a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1215,6 +1215,7 @@ dependencies = [ "byteorder-lite", "color_quant", "gif", + "image-webp", "num-traits", "png", "zune-core", diff --git a/Cargo.toml b/Cargo.toml index 0f871e211..b4890e3c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,7 @@ icu_provider_adapters = "1.4" icu_provider_blob = "1.4" icu_segmenter = { version = "1.4", features = ["serde"] } if_chain = "1" -image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] } +image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif", "webp"] } indexmap = { version = "2", features = ["serde"] } infer = { version = "0.19.0", default-features = false } kamadak-exif = "0.6" diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index 15b4296eb..4a36045ae 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -841,7 +841,9 @@ fn param_value_completions<'a>( /// Returns which file extensions to complete for the given parameter if any. fn path_completion(func: &Func, param: &ParamInfo) -> Option<&'static [&'static str]> { Some(match (func.name(), param.name) { - (Some("image"), "source") => &["png", "jpg", "jpeg", "gif", "svg", "svgz"], + (Some("image"), "source") => { + &["png", "jpg", "jpeg", "gif", "svg", "svgz", "webp"] + } (Some("csv"), "source") => &["csv"], (Some("plugin"), "source") => &["wasm"], (Some("cbor"), "source") => &["cbor"], diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index 3e5b7d8bd..8136a25a3 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -147,6 +147,7 @@ fn determine_format(source: &DataSource, data: &Bytes) -> StrResult "jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()), "gif" => return Ok(ExchangeFormat::Gif.into()), "svg" | "svgz" => return Ok(VectorFormat::Svg.into()), + "webp" => return Ok(ExchangeFormat::Webp.into()), _ => {} } } diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 258eb96f3..f9e345e70 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -77,8 +77,8 @@ pub struct ImageElem { /// [`source`]($image.source) (even then, Typst will try to figure out the /// format automatically, but that's not always possible). /// - /// Supported formats are `{"png"}`, `{"jpg"}`, `{"gif"}`, `{"svg"}` as well - /// as raw pixel data. Embedding PDFs as images is + /// Supported formats are `{"png"}`, `{"jpg"}`, `{"gif"}`, `{"svg"}`, + /// `{"webp"}` as well as raw pixel data. Embedding PDFs as images is /// [not currently supported](https://github.com/typst/typst/issues/145). /// /// When providing raw pixel data as the `source`, you must specify a diff --git a/crates/typst-library/src/visualize/image/raster.rs b/crates/typst-library/src/visualize/image/raster.rs index 21d5b18fc..54f832bae 100644 --- a/crates/typst-library/src/visualize/image/raster.rs +++ b/crates/typst-library/src/visualize/image/raster.rs @@ -9,6 +9,7 @@ use ecow::{eco_format, EcoString}; use image::codecs::gif::GifDecoder; use image::codecs::jpeg::JpegDecoder; use image::codecs::png::PngDecoder; +use image::codecs::webp::WebPDecoder; use image::{ guess_format, DynamicImage, ImageBuffer, ImageDecoder, ImageResult, Limits, Pixel, }; @@ -77,6 +78,7 @@ impl RasterImage { ExchangeFormat::Jpg => decode(JpegDecoder::new(cursor), icc), ExchangeFormat::Png => decode(PngDecoder::new(cursor), icc), ExchangeFormat::Gif => decode(GifDecoder::new(cursor), icc), + ExchangeFormat::Webp => decode(WebPDecoder::new(cursor), icc), } .map_err(format_image_error)?; @@ -242,6 +244,8 @@ pub enum ExchangeFormat { /// Raster format that is typically used for short animated clips. Typst can /// load GIFs, but they will become static. Gif, + /// Raster format that supports both lossy and lossless compression. + Webp, } impl ExchangeFormat { @@ -257,6 +261,7 @@ impl From for image::ImageFormat { ExchangeFormat::Png => image::ImageFormat::Png, ExchangeFormat::Jpg => image::ImageFormat::Jpeg, ExchangeFormat::Gif => image::ImageFormat::Gif, + ExchangeFormat::Webp => image::ImageFormat::WebP, } } } @@ -269,6 +274,7 @@ impl TryFrom for ExchangeFormat { image::ImageFormat::Png => ExchangeFormat::Png, image::ImageFormat::Jpeg => ExchangeFormat::Jpg, image::ImageFormat::Gif => ExchangeFormat::Gif, + image::ImageFormat::WebP => ExchangeFormat::Webp, _ => bail!("format not yet supported"), }) } diff --git a/crates/typst-svg/src/image.rs b/crates/typst-svg/src/image.rs index d74432026..1868ca39b 100644 --- a/crates/typst-svg/src/image.rs +++ b/crates/typst-svg/src/image.rs @@ -45,6 +45,7 @@ pub fn convert_image_to_base64_url(image: &Image) -> EcoString { ExchangeFormat::Png => "png", ExchangeFormat::Jpg => "jpeg", ExchangeFormat::Gif => "gif", + ExchangeFormat::Webp => "webp", }, raster.data(), ), diff --git a/docs/tutorial/1-writing.md b/docs/tutorial/1-writing.md index acc257830..d505d2d03 100644 --- a/docs/tutorial/1-writing.md +++ b/docs/tutorial/1-writing.md @@ -69,7 +69,7 @@ the first item of the list above by indenting it. ## Adding a figure { #figure } You think that your report would benefit from a figure. Let's add one. Typst -supports images in the formats PNG, JPEG, GIF, and SVG. To add an image file to +supports images in the formats PNG, JPEG, GIF, SVG, and WebP. To add an image file to your project, first open the _file panel_ by clicking the box icon in the left sidebar. Here, you can see a list of all files in your project. Currently, there is only one: The main Typst file you are writing in. To upload another file, diff --git a/tests/suite/visualize/image.typ b/tests/suite/visualize/image.typ index 9a77870af..73c4feff8 100644 --- a/tests/suite/visualize/image.typ +++ b/tests/suite/visualize/image.typ @@ -243,7 +243,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B --- image-png-but-pixmap-format --- #image( read("/assets/images/tiger.jpg", encoding: none), - // Error: 11-18 expected "png", "jpg", "gif", dictionary, "svg", or auto + // Error: 11-18 expected "png", "jpg", "gif", "webp", dictionary, "svg", or auto format: "rgba8", ) From aee99408e1cb6e825992a43399597f5d1a937230 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 4 Jun 2025 10:14:24 +0000 Subject: [PATCH 141/172] Apply short fall consistently in math when stretching (#6377) --- crates/typst-layout/src/math/accent.rs | 2 +- crates/typst-layout/src/math/frac.rs | 4 ++-- crates/typst-layout/src/math/fragment.rs | 12 +++--------- crates/typst-layout/src/math/mat.rs | 4 ++-- crates/typst-layout/src/math/root.rs | 2 +- crates/typst-layout/src/math/stretch.rs | 11 ++++------- crates/typst-layout/src/math/text.rs | 2 +- crates/typst-layout/src/math/underover.rs | 2 +- tests/ref/gradient-math-conic.png | Bin 1721 -> 1642 bytes tests/ref/gradient-math-dir.png | Bin 2615 -> 2575 bytes tests/ref/gradient-math-mat.png | Bin 1560 -> 1557 bytes tests/ref/gradient-math-misc.png | Bin 2993 -> 3138 bytes tests/ref/gradient-math-radial.png | Bin 1641 -> 1606 bytes tests/ref/issue-1617-mat-align.png | Bin 3354 -> 3335 bytes .../issue-3774-math-call-empty-2d-args.png | Bin 1315 -> 1334 bytes tests/ref/math-accent-bottom-high-base.png | Bin 572 -> 567 bytes tests/ref/math-accent-bottom-wide-base.png | Bin 359 -> 351 bytes tests/ref/math-accent-wide-base.png | Bin 510 -> 506 bytes tests/ref/math-cases-gap.png | Bin 340 -> 354 bytes tests/ref/math-cases-linebreaks.png | Bin 506 -> 492 bytes tests/ref/math-cases.png | Bin 1281 -> 1228 bytes .../math-mat-align-explicit-alternating.png | Bin 1035 -> 927 bytes tests/ref/math-mat-align-explicit-left.png | Bin 989 -> 903 bytes tests/ref/math-mat-align-explicit-mixed.png | Bin 2523 -> 2454 bytes tests/ref/math-mat-align-explicit-right.png | Bin 976 -> 875 bytes tests/ref/math-mat-align-implicit.png | Bin 1046 -> 954 bytes tests/ref/math-mat-align-signed-numbers.png | Bin 2036 -> 2024 bytes tests/ref/math-mat-align.png | Bin 1564 -> 1531 bytes tests/ref/math-mat-augment-set.png | Bin 1810 -> 1714 bytes tests/ref/math-mat-augment.png | Bin 3631 -> 3563 bytes tests/ref/math-mat-baseline.png | Bin 818 -> 816 bytes tests/ref/math-mat-gap.png | Bin 496 -> 526 bytes tests/ref/math-mat-gaps.png | Bin 1309 -> 1311 bytes tests/ref/math-mat-linebreaks.png | Bin 651 -> 648 bytes tests/ref/math-mat-sparse.png | Bin 882 -> 956 bytes tests/ref/math-mat-spread.png | Bin 1814 -> 1796 bytes tests/ref/math-shorthands.png | Bin 1231 -> 1173 bytes .../math-vec-align-explicit-alternating.png | Bin 1035 -> 927 bytes tests/ref/math-vec-align.png | Bin 1098 -> 1126 bytes tests/ref/math-vec-gap.png | Bin 420 -> 436 bytes tests/ref/math-vec-linebreaks.png | Bin 651 -> 648 bytes tests/ref/math-vec-wide.png | Bin 620 -> 630 bytes 42 files changed, 15 insertions(+), 24 deletions(-) diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs index 53dfdf055..301606466 100644 --- a/crates/typst-layout/src/math/accent.rs +++ b/crates/typst-layout/src/math/accent.rs @@ -46,7 +46,7 @@ pub fn layout_accent( // wide in many case. let width = elem.size(styles).relative_to(base.width()); let short_fall = ACCENT_SHORT_FALL.at(glyph.font_size); - let variant = glyph.stretch_horizontal(ctx, width, short_fall); + let variant = glyph.stretch_horizontal(ctx, width - short_fall); let accent = variant.frame; let accent_attach = variant.accent_attach.0; diff --git a/crates/typst-layout/src/math/frac.rs b/crates/typst-layout/src/math/frac.rs index 6d3caac45..2567349d0 100644 --- a/crates/typst-layout/src/math/frac.rs +++ b/crates/typst-layout/src/math/frac.rs @@ -110,12 +110,12 @@ fn layout_frac_like( if binom { let mut left = GlyphFragment::new(ctx, styles, '(', span) - .stretch_vertical(ctx, height, short_fall); + .stretch_vertical(ctx, height - short_fall); left.center_on_axis(ctx); ctx.push(left); ctx.push(FrameFragment::new(styles, frame)); let mut right = GlyphFragment::new(ctx, styles, ')', span) - .stretch_vertical(ctx, height, short_fall); + .stretch_vertical(ctx, height - short_fall); right.center_on_axis(ctx); ctx.push(right); } else { diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs index 85101c486..01fa6be4b 100644 --- a/crates/typst-layout/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -435,13 +435,8 @@ impl GlyphFragment { } /// Try to stretch a glyph to a desired height. - pub fn stretch_vertical( - self, - ctx: &mut MathContext, - height: Abs, - short_fall: Abs, - ) -> VariantFragment { - stretch_glyph(ctx, self, height, short_fall, Axis::Y) + pub fn stretch_vertical(self, ctx: &mut MathContext, height: Abs) -> VariantFragment { + stretch_glyph(ctx, self, height, Axis::Y) } /// Try to stretch a glyph to a desired width. @@ -449,9 +444,8 @@ impl GlyphFragment { self, ctx: &mut MathContext, width: Abs, - short_fall: Abs, ) -> VariantFragment { - stretch_glyph(ctx, self, width, short_fall, Axis::X) + stretch_glyph(ctx, self, width, Axis::X) } } diff --git a/crates/typst-layout/src/math/mat.rs b/crates/typst-layout/src/math/mat.rs index d678f8658..e509cecc7 100644 --- a/crates/typst-layout/src/math/mat.rs +++ b/crates/typst-layout/src/math/mat.rs @@ -314,7 +314,7 @@ fn layout_delimiters( if let Some(left) = left { let mut left = GlyphFragment::new(ctx, styles, left, span) - .stretch_vertical(ctx, target, short_fall); + .stretch_vertical(ctx, target - short_fall); left.align_on_axis(ctx, delimiter_alignment(left.c)); ctx.push(left); } @@ -323,7 +323,7 @@ fn layout_delimiters( if let Some(right) = right { let mut right = GlyphFragment::new(ctx, styles, right, span) - .stretch_vertical(ctx, target, short_fall); + .stretch_vertical(ctx, target - short_fall); right.align_on_axis(ctx, delimiter_alignment(right.c)); ctx.push(right); } diff --git a/crates/typst-layout/src/math/root.rs b/crates/typst-layout/src/math/root.rs index c7f41488e..32f527198 100644 --- a/crates/typst-layout/src/math/root.rs +++ b/crates/typst-layout/src/math/root.rs @@ -50,7 +50,7 @@ pub fn layout_root( // Layout root symbol. let target = radicand.height() + thickness + gap; let sqrt = GlyphFragment::new(ctx, styles, '√', span) - .stretch_vertical(ctx, target, Abs::zero()) + .stretch_vertical(ctx, target) .frame; // Layout the index. diff --git a/crates/typst-layout/src/math/stretch.rs b/crates/typst-layout/src/math/stretch.rs index 6157d0c50..40f76da59 100644 --- a/crates/typst-layout/src/math/stretch.rs +++ b/crates/typst-layout/src/math/stretch.rs @@ -67,8 +67,7 @@ pub fn stretch_fragment( let mut variant = stretch_glyph( ctx, glyph, - stretch.relative_to(relative_to_size), - short_fall, + stretch.relative_to(relative_to_size) - short_fall, axis, ); @@ -120,7 +119,6 @@ pub fn stretch_glyph( ctx: &mut MathContext, mut base: GlyphFragment, target: Abs, - short_fall: Abs, axis: Axis, ) -> VariantFragment { // If the base glyph is good enough, use it. @@ -128,8 +126,7 @@ pub fn stretch_glyph( Axis::X => base.width, Axis::Y => base.height(), }; - let short_target = target - short_fall; - if short_target <= advance { + if target <= advance { return base.into_variant(); } @@ -153,13 +150,13 @@ pub fn stretch_glyph( for variant in construction.variants { best_id = variant.variant_glyph; best_advance = base.font.to_em(variant.advance_measurement).at(base.font_size); - if short_target <= best_advance { + if target <= best_advance { break; } } // This is either good or the best we've got. - if short_target <= best_advance || construction.assembly.is_none() { + if target <= best_advance || construction.assembly.is_none() { base.set_id(ctx, best_id); return base.into_variant(); } diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs index 7ecbcfbaf..e191ec170 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -159,7 +159,7 @@ fn layout_glyph( 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()) + glyph.stretch_vertical(ctx, height) } else { glyph.into_variant() }; diff --git a/crates/typst-layout/src/math/underover.rs b/crates/typst-layout/src/math/underover.rs index 5b6bd40eb..a24113c81 100644 --- a/crates/typst-layout/src/math/underover.rs +++ b/crates/typst-layout/src/math/underover.rs @@ -286,7 +286,7 @@ fn layout_underoverspreader( let body_class = body.class(); let body = body.into_fragment(styles); let glyph = GlyphFragment::new(ctx, styles, c, span); - let stretched = glyph.stretch_horizontal(ctx, body.width(), Abs::zero()); + let stretched = glyph.stretch_horizontal(ctx, body.width()); let mut rows = vec![]; let baseline = match position { diff --git a/tests/ref/gradient-math-conic.png b/tests/ref/gradient-math-conic.png index ffd3e8068f65a6c50e76af647605aa5b21005338..9bac6c3d4ced5297eabd91af61bcd7addfb2d4a8 100644 GIT binary patch delta 1638 zcmV-s2ATP}4eAV#7k@Yi0ssI2xn8U3000ItNkl&$@C`$=+QAxUFYZ{WKY11r?vo@~dI9}p?x8r>q$74HQ#vaf1 zBaw2L(U__gNu+3=j~-c;Y+t_f*W(#!<|PoP;w6%RgeIX05`UV6CZP!unuI1uXcGE= znqJRRSSz_o@@!tQj*lHw9pL z(&z9{P)ek#(|F_!9NI%NYRA6S`Dx<%>-Kccy$UZxP*; zE3LsHt0Wuu(P2xi*4BFH?*~JLGW+b%>f8gPPEnMg!Z4h{5ZCE~JTC%A0=~l1qF>XY z8!h3JJ}-dh56yOjxus}_6F5$U(!to)8!UjyNr!zW1AiP51gYA*>O3u+gownW1n(k!Rn&VY~0mu=ufW&t>GF@ zo_i-XGkP^@+PP@PTp>a8XM4SG&Ca9GWUJSIThwT1YM*<(*Mn10 zt1^Fl`{2xnmb`AfxLk#M^*bNXoghd!KK1=pU+?`h9=cXz-+%Am6)(u1Y`xOsM>iDR z9-IzCMUrswf^M+i@My{|Nl*)VGImpRecst(I)9KUq|3jbpE%p0`Najc=L;zr#|uhU z_?N#8qnn58>4Awr*YjxeX0%yn|7(Aky}Jb<>`5QIS57o@xp(pmhZds|F%o7k{VF)@ z%KSV!9mNtw)!Y`uciw=CB~VS|nl~qWP?1E*ub9yqyU*xzH;TOv9lVQH*InH4eqJF^;ci#!L%Y`sU+sI z@+DQPP!j$A+BUe=An}G zrKn^PfPQK#AEQ)$iIbH5!^P(UDI+$Ej#mnryXms5M6@laJGlsM)>YO)0DQxr^?yP7xbh3ZA=I}y> zwz+{r)V5P-vrwW#iHz9`An%IQvU^YpZ#bfxZUDX&Yax_|jALQjrL$-^G=Hr(TP%fty#|%o z)MIodNwF1^wSUTVd0gmlP^zRU_l#cPc;l@(?_-nfSD5{2R1!rEg;opxd_`bdiu!al zFffj8GDb(USOyS(VojUeP=BfA>8va2oi`?}0pJLoC~S@B0BAZiZQ7w$4UnmL!_o0s zbQ51PHu5C^_1s?CXSU7!|m$rku~eS?0=GNv`mBZcW^sHv4Td}{M4rkbNN5t8 kAfZWU5}F{PNoWGVzrc)+4>46(^b07*qoM6N<$f=GfJZvX%Q delta 1718 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 Z`7hW!#nL9z|C0a!002ovPDHLkV1jBn2Ydhk diff --git a/tests/ref/gradient-math-mat.png b/tests/ref/gradient-math-mat.png index ecf9530389ebe41ea2ccc67c00a20bce7c8c416d..d003d6d03a21e22a04c21308f168a31c09f9551e 100644 GIT binary patch delta 1553 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 literal 2993 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 diff --git a/tests/ref/gradient-math-radial.png b/tests/ref/gradient-math-radial.png index 8d0047bbe491b86029ba81088f498b5361008a15..97fb17e6fbb1778200ae2fc10226d5d4c74f190a 100644 GIT binary patch delta 1602 zcmV-I2EF;|48{zQ7k@Yi0ssI2xn8U3000IJNkl5RxiT#iA)y)?hFmuq!0CsYvZup#(Prp!&w^ix||+;rPh^a5rk1~?2c3mc0`1m z147-9P~BeAGHbXXt$C#AZU9vYg-*GF){R zy&(ncAX<5VM2h2*W%zqUWGCaL9<9DTDgNE?c!(L|x zJ>i3v8nBp(9tAr(;QH11h$uuj$&(both+Wc)|Dhf{iVQ2M*grutTf+u~jKLb{H@fK&F{ma(@|ip8r+4@#>6ogI+t-1o2had7*Lk-U**mgGM;FzaB&tc3yg^RC&bf(4e)P z$Q^tHyJ9UXo&UIZ&j-CGPs7$>hJ(*;!zM5w*gnOaeD>h0y&bfSOD^UR(u%NEp(K2M z0_-SCGRRiirH{Xfx1I@4+**w5U}MJ1kb~WeyMG~}sDc66$WQO@+1o(}kn1&njzdw{ zDwc)gN5M|4Q0p&Uw>?L979s_tf}KKECMRez%nmT5E8eLmI)S4}9PW~DWKaN%Fak{m zO#!s=qwAK&Vc^;UuwcdEq88~FZ`ua^l!;E{OVg|b7M`YP+wFyEPiAf~`%YB{25=(} z0)Mq#eK?Ey!JwG$EhQU-XuY!3fEF6`*+xi=S`uC0>)y9)*lZ5XmT>l&atrK)c@Y4v z4CPk^9n3naf}PdD@_Mr5Uj>6qnEUy|JzE_H|0GH07n`BsDI}F-3~0 zVhjl}U17mYDozFm-vcwBTsMvS`ceB$<$vO`z6@p|P6!nxGCcJ0T~iX6^*srfH}B#0 zgsXN543fd(=?}X1WY8>yJtG3x8GVuTy-azVV|$>ZcTEaM zPD)2$$LxWQ-W?3i9^=o#uE?)QXAZa=8Z@hOUuSA|Z3TC3bx6AQNx-ecZdeI(m%s2h zG-%?}?r>h#%tsN>F}xkt;VpCxgMY=sNUrLKQRfDIw0?CthrpoUFMV6Fa_W4l;+L4K zg9S6B=)UfM@5FeUqF|xQI@wufJdE{KEID73?cBLRS3W9?%}8J{K3rTMdNZMsB z6RtEFW(OJ424F_sf0I?Eytyn+tb#%JgGI8!I5+5l-h69a+O|?jrnb-QwSU82PgB&> z7}YHmLtYh-$8Oi#@cY`_@Ebpz7f&JCKlb8JN95Ev8~>G6fB%@)60QYg0s*#N$ZGB*@^2Mef42_5&g|wL*x)_pKO9BhdU6A=~pW({VR(a zY#-9j^vgo@TUithng&hLplQ%FXo?0+gQfuB9}DrB4(bZ{wEzGB07*qoM6N<$g8JnB AsQ>@~ delta 1637 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 RGY$X%002ovPDHLkV1glBKE(h4 literal 3354 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 diff --git a/tests/ref/issue-3774-math-call-empty-2d-args.png b/tests/ref/issue-3774-math-call-empty-2d-args.png index c1bf52d0040aee18b7b00eefb7ba55ab48dafe9d..52472d8dbeed2803c9dd5d075f7244d213abef66 100644 GIT binary patch delta 1328 zcmV-01<(4U3bqQ67k}&s00000n)p3i000F2Nkl z2j+U1%h~FpW*szh9dn`(P7F-cU|N{sW9mdvvj>NDqvnFrMQZqf&POoMkWMHuNfa5R z2!bFF;o-onHh;vz7Phd3(T8(9-k&1@ zU(A5R=k(#O_yDl1Qyt!S0pRRjb$D6`AT&)IzO-DLenlBxbyj*NT^V*)NpH?nh9`7M zedBcD?&m=^tXmbXj|N$Ey@id!4gbLns&HT^$c6`0VWK;wzKQ6fF zmfn6x86NjU>K~&E`=dZM+;2}2gA;C;0I~@Us&Hs1$bW`~RADBzOFLgc7oJ@%{VP@( zUY{?`-=GXvd?fv_Oc~}qKY-tjHvCf_z_Fw1a3CcF9dm=~Fy-Gs(hc-se)D=R+b4{L zEo@;64{+G$iwwNw+KsOx0q5+6%Q^b+qeTFpHmSqAyZ|Mc+Hmk~sVk%mAKf6`a#9&y zQ6#;Ps(%bmua(}3QHI9^q)$fc!oCq8i}Kl1#NdPTe^(1)wGLqawBFlFB2 z>-Gs_VGCQ>!UG&`X^9M+ezQ6=67b$!xb~es+<*8nz^XcRcuP4z*?Mib+a-+&D8q-g zN_YIC45wX@7A{qWU3aB5Zz;p0gVMlAUAR*`U__@qMGQ{3X&lJLH~mj;Ad{Mpimtq)dzL+(TDGSPGD|5`Y?C? zD1WK4PZ$eZ*uoYb*l@6}E*J?olu}#2A&5S_{RC9*)`#z{1Xy3K4zIZdaC4Rfd-pOD`{0hNsp^|9(Rm9vPAbqjcepQ6L-DVNVf*6Mi@rWMdyj=ms(z zU3hYnv}KAioZyvSU!V+skt6+Kmoofdj(_z1`^qr$n*mx9(S{4N0kTWfVLoYxs$}%x zhiijfu`TGs)L$>XZ=Wz0wy=dQJn-Ohho4Nr$F~!9-<~oD-(Cc?*U;O+{ z7JaWH&J>)#6Cf`f%$IqF;F!n#!!2)`fjzPG5A#8>5qRDAPVj*YBk<%JfYz*Kjemq$ zu+`QlV=W>MJU0fmw})V6xBC&kFWn3MTD7VJTNRA9L`N mEW&Q9-=3Q+Y+(xza`<2R*CZT0x74Zt0000*X)3Zn{;7k}*t00000QKatv000E)NklY`(#l!N@ahxN z+%?K@ak6w#p?@;W^j-k}bhP2U#{qJGQipwufbO}y>M(_SAh`&Am{ab8WA-FrVGCQ> z!u=c$2EzmIx^iuAIN+QuaPp`={C7OS(sp$?^D;pGPIcIYL>tasC0&=N45yrzo?WgC zzf&W<^|mrRrbp@LSUeSi4gM*yqp)ZtBK0Hy1-;a-;34QT=N;f&)@xlf3lm7L(GCVve4Mga|T_Zp?qRU=I3{1FXG{{D`gy{}s zD7x^ZW@+1GWjM|)Es0l#zsQpQxI-Ckntv^QuU;8uUMoP`e6-<M$!hp(+u5 zxMgjiJGu>hn1+(#dV7Siu!Svb;r<7gIs9Y_K9)hqeS6Xzd}{%HZ1qiJ@Qg0FzBP+S zuPeqBoUt!3o}%Y6PBC>jYO$GJgSg zJP`o6nv#@9s6EwT04|NdaW51bfRDY3<6OTPfHUXfxVTIMaQZ?Vw=mrRoHifF#itp7 z6I?tz_vU^4XRZVTaC#CCujl`7bW*woj1s(TrZYTqnE^OzqBA@(O6RZ62+uRVx;i;e z1KyB`;}SO*fa9m&xGC`}&lpsm@qe~N9G9@&0Gt(tfhTJXzzptif;*nZ1RQhP3C@o-0`L9I2~OQ_1n!y~8qNUVwfjb3KE1%BcXjQG zJm5+yX5hMb`rOQWhF~)D=wrFNFa@);o{+1YhdH=sDJOBUy07*qoM6N<$f@^djP5=M^ delta 71 zcmV-N0J#6R1iS>0BLV^gu_fLCIdF8^?rvGLa^>=sYu7Aav2tnKnE~o8UK;Hmk6Jux d@u6Fv002ovPDHLkV1j5o9?bv% diff --git a/tests/ref/math-accent-bottom-wide-base.png b/tests/ref/math-accent-bottom-wide-base.png index 0475b4856bd49b9a4f7663564747ca9635654028..fb4a1169b745905df726e50d90e56d03d9fe11bf 100644 GIT binary patch delta 102 zcmV-s0Ga>i0^b6VBmpLoCE!}00^~D{abOJu9HdRj^>EAT6)RUPTe)V%AgS%!Xnj0t@er^W0BVrlFPS4}qW}N^07*qo IM6N<$f`ErEX#fBK delta 110 zcmV-!0FnRS0_OsdBmpjwCE#MO0^}bggb%ENfP=J%xGu!1iRQl`Gt@JFb|7?WNZ>P1N-O{rBAKhywNNW2wS|5*EJOnHT0M8KIstp54 Q*Z=?k07*qoM6N<$g8n2lN&o-= diff --git a/tests/ref/math-accent-wide-base.png b/tests/ref/math-accent-wide-base.png index af716bf45f7e1f270278e5ecba92c4a1adf0bb4e..793ab30bd3b2318829cc63dc193a6f6b6399ba4a 100644 GIT binary patch delta 480 zcmV<60U!SU1NsAyB!9t4L_t(|+U=FyD??!%$Ndj36*nT~#w8`BCPnVZ+>w_eNm7y& zbCi;lw|RfLaAUccSs|2{=7ycaHnUC4OJQov#&+8I+Ih}ar(Hib7w2>FJl~$4`t4xBY%sS9D8Gkp)oAiwzY@o zh`rYwXEtA1SeA_h&Uz^Zmc6ucHy#_0JLiClnKC)BsSb3 zMkxp zOmKzYz;ZzT;=u#H^bBudsn$=l#QL&h>=*Sv4wlT1by-`0qgL&t339P>X1&dTp;a-j zoIu3$CoMA^3*5W>)C}NsiDYkY0(X0ehlFb*%8xRS6|o{ty7(6$ WXFuwI9Dtbs0000Q3X&G8t1#Sh&ok!r5uIL^?b zFFj)e*4coE&ZNoVIwvq*w>ee5`y(Gnxl!?{hY<_*^)mrq>Y7OPX8}e%RtprMmx3+6 zuS`C*o%AQw&3{b5^QVc53J#Smp=1QWDJ7a*G;pyn(g>>VN`}|T2;Bw&r>zWCdy!$K z%_#6w(W}7(s&MWiGqAs9s-~8X+Lj6ZVC0B?z{7DB%^Y_RjP|1<_?bQ_WCos)ahD*g z;JsuB+0VdiDEh=b8*=_0uETWJ=d8fyf^Dqlbe;9&OGQ*z>;U>C!NXqUEz11Cm_*U=Bz0nP(taZ%d`Ak#ovs=F!>WNeN(!Cr(^RXN&JBe;=UU;@pHk1Jq63 mojp1tFlzCr#iJIF1d9PQ^aSoH$y^8k00000 zpzKtY2V;$_vik0{%7=8e*wF+;rvAHZN@t7f3P5D}w;y73wm8xoLNBfvn8bCbYG4xA ztJ(jz(%ItVP(KjquchTmXNxB#3{XD4qx^kbP{gRkqZW@^JQ6Gh Y0KklU)%RWnK>z>%07*qoM6N<$g3u(OEdT%j diff --git a/tests/ref/math-cases-linebreaks.png b/tests/ref/math-cases-linebreaks.png index eb4971c46fb2d2a36a8b95324d3d1e08b7d99319..65b4e402575de10ef2513bb25a1d21443f3235ca 100644 GIT binary patch delta 479 zcmV<50U-YR1MCBk7k@Yi000006IWwm0005ANklj3#$+rU#$G;b@ow$+iiEDsTwfrU+i|&bQfPXCJn=_yV$c3vg{+E59 zA&f`8S7>Un>Uj`3qZP*gYV{7vdKO1Zi{DHC15u5LwCw_8dA-~Zx+zwQ4|U+~}Kuj{)#d;k9rxk@vO zU-;|<)2D0yCx0CItMUIt&HscKAC311fko>l(=Mq0cHnuJ zIM?w1#l`>M185n?H+8|(${BC||KG83-Ly|P)~xvdf8D;#v`$?AW$)pV`4}-UmBgFI zH8izYVkHiV@BfBrB_XI0gB`E zP5&QVq_f3FdqL#peLLyxO4Tp)o(#}X#O;7R12y>acyzdF)Z$T#M=c&$ivjQ3 V%;jb2;!OYm002ovPDHLkV1n{o0g(Uz delta 493 zcmVVP4Bt@3Ph~{FOY>~-6-Ym~#Fk@(%=gn7d8s##_ z|N9-h|Ka!jzuw_<_^dgPGQ4DaHy6D zPTURhgFj~rC+X$d*Zcb|6!U@Jf`%y^^#b7U_EF#qbX=t@;hh41RPS2=ydL}DDms+_ z!Qf*H+cWR{nSXlP#p9skEE^kT_Bu?A%yIA*vV%==kNz}Qp>q`(??MII3`luOhtmxV zHZX&+S@xjHmcBS8ICMZZCNQe*yTvW{Ia zo?K$2M~*ie0O?xdfm<&i8Ue;I67~?JUDnVX5>L}kre<*X_7pVUfpwTU6oFEhiU{JM6vkUXP!URf!8x6Ff_6g` zbvV!h9hAa&IY3*W?K4l>oV2AYB%IPL>*wzD=E>>#<(re7MSq0JqDVLrj)WuOg%0aK z8wpGJ>^qYLF@XSBjl&;mgkhbmVQoi#CzYq&*IR z8)`cjNd7B!8jq2~WZf0f10Kk3&yE|kL zA2>7`y`v2zx__$6h#nC00ort+w>J$Tcc=_?r6Logk@8pgk?w0g`-xo`cyRTjdJZ zyaKS{7R$OpGEGrE_=nTV*;pg*sqV1zw18w^`e<}a8(`O@mFynJ6CUX2y({wGG32NN zZT0oZeSh`WU7oOby_KNhaToMi@Ff_794>vuq!1ts>n1=7n^ahWgr_l7Cz$KO~kGacLmP@+&Ig5}2jRnVcu=9xpME02WmU!4}!B6}ykZ&@} zHNYn5a<1Yx>`q`UhqVyx0CID@?if{qjNybkKn!fjk1aQFOa-LqmXAOT!1OfU*60zr z1b_VKPZP+E8Uw;feSom_&Srr6c03Hws(-W{`YM1hrExc6b1 zfdoT<@ShtS5f!>T*ZuHoj%EZju74enV(sLUcyXes$48}7r5XmtH=a-m$1Oo7)Al#M z1R=tmFkm=A!sOTirC3)Ns=UBs4uX zi5MpN#v3w(zk34U>=4hi1>R~aqPReMKhGa?8%?Cu$r&)95wNp1+LwdN>X(0Kk2`z83 z-O-z$RZK%D!^R_7_mMa%DtmuFq&{ubowre2bw4EG=I6pRAk!;a!xQHAYEyW^^z0mo jd<=<%BjHH+{}%oSB0>?K?nN@?00000NkvXXu0mjf|ENs2 delta 1274 zcmVrYc>9Kdn^1CwRhJB?Y) zF5a>&ZrPTxON^VuOZ3Imd5g=y&27eLM1iP7MMRt(193zc-UUIFyL1d?C^8r#av3Tx zF4cmiEiKTN_O$)ZbCUI-bR7w&HjDk_MaCW&kcmQl}@0oH~Im24}LD++si+O zYyg#)x+fIQdmo_v%wmu=WK(#qL;5;Vd-nbG&&wVAb;t(*S8*!+-VTm0c^D{1;V}^ zYzHLQ)S@9J7D&`&{!40Ii62(xuIIqFjvYG+@XGI_hQNjY@>6&ueOR(|;>SIzx~ZM!J$RY(UfX`0u05Qdlzbu1?ue+VZC4uzZv#0Ta}!@-dqxEm z>3Q7n1s~xgb62OAu#FJz%Hi{OxaupMeEc|f%Xm^vEn}Tt0kG%QxzlZU#D3hfF02<= zbN18Ha!|I_$n!Y$Fs?MR6DE0=*x@I(6RY@TMmHMnd^cXemCLEv)iO zIAJP4y7l~iCmvJO>Yaq5ervn=y>oNwt$qoo&j!fK+_>s49&-lJ5=t?-KQ6DP*N>y7 zYwfaDJg)7|FRGti7+!MWR{PeXGyV#DGXmgtxdAJF3D?XB&;k)=-40Bc#t-iYCX8mY kDfkyA7!HPm;b$%UH_GKbHUIzs07*qoM6N<$g1Z$_L;wH) diff --git a/tests/ref/math-mat-align-explicit-alternating.png b/tests/ref/math-mat-align-explicit-alternating.png index 1ebcc7b6847d96c69e3e5787510299955d3c7003..52a51378be2516fb650de8c05e6d4e71b8ccac96 100644 GIT binary patch delta 918 zcmV;H18Mw=2%iU#7k@Yi000006IWwm000AKNkl^q*=mxlCou4 zip~<-_UZQp!9j=dQ+|*ad)>S*Kb&(ozl+~E{3qiQ!-?Uf>;_yG91_z zg(#VA{KMhRAqewZ5#=5>TOm;SHQwRKsxbcb#aZ|f#NqIQJd!R)sqN*LIuNVQqX&6= zJMSAycu9nF*r|pLM|H;*>K7%JN5JeD)K*h1b%C9TrGVN`)z`l~Kt1i|98P-y83C0o zl{#?FIxhs~W`F*E8};=ma~fh57w&RXU!FFkQ=8WE4Tq;jA!AK*d#a!H##tnJS-a*S zyYwM|EeObl}kKV1qrew!z|k+nnbDm8(*Dpj5N&RS!y zINyX}(aTb$x(7hjO|QBc%*uMM;UWbDJ9vd_I3ojsU4J;nHLTD>Fw<79;T3KO=2^it zoLm9HDw4T|C#WEpYQh+JV*cSeN&AQT$(|nou{vBJwNh>b@wDC2c8p|rk_NF3gbYq< z`TgJ~YVgF+S-pEOw-WJOf8MeWL^FwNczh;em1z$fsjV5dRBHdpp|jeYM`v{$4JQrh zAerMw@qeoOW+2bt1F-tl`xL4v3v(pa?{jg!xDIj54>oM|js6=KOTMg5*7~wL`Aw`} zjjr2PFlMPX>nGyyIqzv&!9P4j2f=hxxQ3H#5X_b|w*TX_5(sAAz%_hO2f@^bxQ2B~ z2(~kiYq)v?1WWO94fjq4F*z+DcRg7H)DI=Ot$!d!hYfO~2D=kP@@OK|7RSYQkmeEj*TXrCqc4DNi|f7xj8l4?)5Y58 zs48k^?XtS6%OK;c)#WV*Qz|{whFG86sx4?^QGs{3XFCMi{DXTKm1Phn=Pvk%VXlE7 scl6*MCY=zZGjZLN7)}f)hDG}ezuAM7E-%;Wc1a95W(nsUK|L$s|}skSC}!)2i!}m zFekIQy_!xO3VTgCH$Q6(5$<{z%WEMC&FcSdIIA{IR zGZY+7B)ve+xj_>8$2lITIY%X~s6dRXY9EzYRm)o3P(&qOX=W|Hv64!hZDB38WK)S3 zRR!m|i^n@Vyjzof5hBIAN@vOYIYT`oBiJ`9s+Nu*G0G2@I(NYk{aJSn5eB zK5y4vNq z7Avo^7V8SB#2V)JxN;|zxMnYF@s?9m;=6?eK6{vOs?9@)9FHZwy%#~U^|~!e7`FQf z`3?sm`ujGOIPxQGKNdyu$#7#((+4z~?o|PUo5E{kz1)A}1thGEZ+-_nbKiIgh<=Vx ziGR^DI8AKRFAu@!xoKAeaI97OR`YiKlDM3B&Zhr`gzlzN3vj6A;zpo13(<*B*Wryb zUr_cY5E7OQ8tZ-V52%eQfE|UxDwmx2zDmsUVn%-roo;=PLy5&-3}@8VP>Lh)+O_X+ zeiF?h5IXT+@Q6N@;^#0PErF#rGn07*qoM6N<$g0=VWJ^%m! diff --git a/tests/ref/math-mat-align-explicit-left.png b/tests/ref/math-mat-align-explicit-left.png index cb9819248275a76b4ae40dff2472c993c64ee49d..09c5cb3d40a77a2dff1e7180a98355f326a8c716 100644 GIT binary patch delta 893 zcmV-@1A_eB2Zslc7k@Yi000006IWwm0009{NklPaYw zE|eh^(FM9-J=hg{u!|H5QV-dpEGlMV>NcChT*GX0Zd06kDle%`QnM66(oBt*Q%C9) zFO^R4()R8AKsd)Za1M43^{~&w@AG^3!-t*2`9Ew0{t+ue;eSv#6b^-D4NqN%jNF=* z8V*EzAtMv<0g2&9jgXc18foF-Gmx2BucWZ*BRiTHeE`BZ9OWPy7(F!O2wxqs(g-YGJ-&8RKgg5YuAEFpZpksbccLH*OW5aO8{JIh&*??9@x9>Brg zo|ANx+YB0Pa)C#6jD+yoF`7oE>cKFC7<%Hg=F*bQR6L-wHTD+|+X3D-kW*~7g2#__ z;$h!PD8Twb(QxZVD8SAZ(QwHjD8MnNXgE6t3J{Ym8h_5Z00p>^B^r)a~MmpWwZe-!BeOmSa$eIIEZV0V-dNy+V{s(^4(dytK;XjudoDb zy(boegkKR2pAG|I%SbelyKfg9#S&)jF5cC7I=ZX*bR^MTEkxL9iH~}EGjv85eSlqk zN5~wxDSz0(upSG?LrWd_9O&yh;rh#WY++m}qpUyJUS_s~&xpxcVx}dJsZE&#p9V*{ zrgORBxCkgfM4V_?9R&r5Qj3NS%6!KL(Xj0V6yUf`G+e(M3b3_NG~62o1z6oD8eUj4 z2icigwICYCv1Xc8Wht}d{(NmW2$Q3L#BBn6wST3%$w8yN2n17>79e*-LOiU~(Ny`3 z4sxl>cb4o^1PA?(siKLmEt}>C^RraIgDV2GbAKWHL z>rKlk^7k8-?Lk_!`uI}vVYh@Z&U%39EzsoD0EqnJK1|L{#Wp9vZ9?q#eu|>QS$b@! zV0P`@k6ZcgfYEc15^hdnM-kJQKF+$$QIXThdfe=+v;o{EL`%_YLpSRYv$MPyELNsI z!w)33NeUxj2(mI53u)nr1jxx;^n}DPJ|{y)ZgxOw7^9Gp(a_&bp>QZ13d_rHWFl3R TCZHZ^00000NkvXXu0mjf-nOI{ delta 980 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 U)b8MD5dZ)H07*qoM6N<$f^EX9@Bjb+ literal 2523 zcmV<12_*K3P)ZylHX;L)+<x3`=5vXw#RKd20!d{eC6uou{ZfqP)Cfdes*dm!Y&DKwn(cgp%Lq!tkUMXefMwFgzk24c+*?F#K#G z8Y+BN7#>=LhKhy?!-JBMiw!ELn=$589U?i!*r4-{11=i?N%EipK5@uCi;EeMtjN)g zMj-m1L0G%%-7)hlr#{dNo>&~7a)TuafF!*x-5HU+w!)dq2wVa)><=%FVR8`N(#DtD z7Y06!8esV0Xn+cTNmJz)hsJ(Gk8j7I5GCCgr3@dJwzJ{#f02iy9~Op(9Y}P`Syp=HV+8W%E-k{!4{uDGu$QD!2deFT$Cjj9 zfW!dVF=hnnDk|!+VkjvoX`!;BysiK6W65YJ`LPG@%k_-5;b(;5S2NI1%B#ZgUm_9d zjQXYtO0FR-66s3CoeN3Gg^m-3=O0C6o8GvToIS2ci$uB-Gq`c&Ix@Q#2*cam5%~Zn z7)f;nX^}`*Lec-Ga1k=gw+X}P;}CfbrWV>2KD>*v@yJY07lzF*m=PEbQ%dX#AKit1 z3cWgiWf6v1n25k1ER5~6D|~GO87t&}&qo~2n~N>FC0q>GeL<=FhY3iFM7k1NSGBH$ zUamToFjMbgODELig0Z2o5owV~S1K|a=~dx^TKL;_XkZBhRKxp&=}~cPG!zD={|6xUM655NksH?1*NtV{L9PrSWK0PP=e^a!9ZnX- z^w#$xJ6ZuJm}Gvh!u-p2`Z}|HfYaOeJ5<5}2T_EpUs@gn@FqClt>)e<+;4c-QI+tR z@08(+U9(1;ftO)wKDec-hOb0M&X0^NvKg)#B@Wk*Kp9@wg=ZA-7JS@H@XczITKHR? z&P}JQwHf9&_k`iF1<09Rht+op%(;7~g z!dlC&#Bo`KysHN!c~1`TsAnefk6VD`Q2y=F_RXu~=5xsHz6Rwk$;=I-Tkf*!_JwI? zv(Es-k0t@sL`qsyHYL9sLO0{lRVbuM_oXSrSETJC(9bi=!%2?{!-H-DbXH0%)D_Ms z=y|9X%P(r^t5LeIQ5mk0w%cgf)>R@8-yG<#FoCBl7a%DMz^S?P1rq{a`(VxFvd8NH zmcM1&{f--t`52Hi-=yNa=XVIiA3K(cm2!W7{{q5}1?u|`KaquovYrrzhv?9dZpef8 z$$a4!8p?iA82)24LY21xOC{1G(@}$fC8G_w^Y016E2+i2b_7;d zZ7xUVz)WFy;|@eVf(c!U>8<39D4gQ17^|({APirbhR6tTF0(5fw3E`Q$jpioh7H3I zc?Fyc>InA485 z^yu*0pgANVojQxcTb1Tt6 zjjI9m@TzPy@%;+*@YS#BQFUxSG=+qP0F)dKP9)7|Q8?DFt%;vLULG0P_cJER@!>8)|BS~%uQ<=9yF+C@)*(J;C8g0bWI zF7fb1eNetUP9;1!QyDfde`zhi%kW7q>^q2BxWi=HYBE`DhVv$i!={%}hCOQArey(d zz{DC-{W~pc;e%dY6TG}qY=(KUT^NpY0l9bJa|0Kn40*V;;Kc2{r3X#6!Y8ec`VLl{ z za|IwtALtO(EsA;NfMj`M>F?~DS0`*__on+Aluu6K^Ye7G?DMrR+?dSXF9#SNoC$zQ z!jXX1caGC7ebqS>x}^KMl;I9(+X8Ll>*V3g!NTw(i2#+yCCywtH7f~_WLbN~=)a~LA|{vnsh*|`yE=_`8aS#0pT=-H0U;iQlUI&elYy;c8Y7Of$9 zCo)@>3Bw@^5g7$js_hD&-A>6YWEKVq!_~tP`87<#<^JX0aZ^DE+ zyTYNK6waep<=cqEKKrqy={lb_FcDIgAT0`ePGIfsQX>55b$Fjj*zh5?d>=?%XmC0O zo@*kI7UbeLZ%M^kT;xGBy(St}!g$7_k%%3rg}df-qKUTI9jamS+|k5m`KX7n7-;i@FjL zf{HSvA}XN++J$Asg6tq21nH()l$~dGFsJD-!_2nkw#21xrIjlsvx5;t+QA~$%vH8T z?O+C#ZQlrQ_P`78Q@k(U>~rF77c4edDM~*#lP+S)Zw^jnY*k6%` z;Jd{!Edt;u-hW&JaKJ7RUfY6-vleF739D}gN1e?;((4z?ZxPLw&*Vnn-5mD0TY-jj ziSYPdRCJ&6I?KgXqSPPk1$^#HhuetORa$L#0uh^X<b~FJCI-f6VM9tf| zWLMt%zkg?Z-*vgL4ft5*y=X^Jq1?#*1l;!2FJ+^6vUCS`QQkQ19c{T=^+M*eL9ftwNc4~%)#{EQh17n6X!=$thU)kIr0B_D) zI{+y2RHE*cE6*>k$Z2;K07~ugFhF}U+Tnr%0Dtq~X%9e6V`vpX$ijl(sPfzmtQS|; zapnS$Mb1rtf+C4bk8%SRd`)x@rKY(tKhNwm*^zlhuu`-e({e?$1I`KTZ9TVV>on rqn+)@hUq~^J+aSCv2ZLL3+w9-Nq<$6)q=x?00000NkvXXu0mjf6dj#3 delta 967 zcmV;&133Ka2G9qP7k@bj00000*bA`7000A*NklZzc& z2&#z%U8rH%1+z&vODj#XizG=2-4G(EEVT$KEpSUqTTU>iWLE0jNb`mxX*sXkbenTt zwxe^KzyANTr~mlJ`6uU)5Bolx&-c&H;j?r2opazUf&Y{x5r1(+91%yvqKc=kh^e?X z%_S(nd^u(Ho7TDTTsI;MS^NAzgMoyE0+cAtd z8*R=nA4R1mkwLuDNfWEHd$!yq+Qbga)eIdfhucf#6~xZU9OI@(h<3TB}7X-2V}hUmQZPJ`^jZbV_*zRqWhjx}4s%z~Sn1j|VU(%zifz2^a$ozVuGIf_>q`tk1^V{`t@_ zjd(pgy|#HT&|~GM! diff --git a/tests/ref/math-mat-align-implicit.png b/tests/ref/math-mat-align-implicit.png index b184d9140a7eff77e8b69b544ddb31dabbe3f794..cd683315593520675aaf57c37cb30f714926a9e7 100644 GIT binary patch delta 945 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 FV1m!*%4h%p delta 2035 zcmV?)IPfYNzuA8}O z=6>{RZ0^p+xuWa3o4N74ygRK-%#5Uslq@l^RGQ2mgjoot0Xhx3RwJZDESX6DTz`ZR z#ZN)lRarJ+VSxZ)mjzj1cVEpn#@+pXcbDPwd4IFnneX#2{(pdZ%;)$^mc92XkLo5TCzpW& z8wl{#5b)-hc$EMn%=6XlM9qsS{|*yj}J z7hLdUg`|-|Na_QCm%7MsOat82B*jCVR+R&BpFZ*(fR?3H*sx5zy$!Sxklr$%zeI#% ztNGxPmuc|eJpgd!tWYrvjKB1g;1e$bfTdBWi3xuG7ztjx2i_GegQV1tASqG@q1_22 z_M7uEN;Lt#5Kb7G7lLtQn!~`|T(V%i@S!{*of}?MN`goJ>>MX-vkDDghaGo2Nq;aB z55beO@Hsm~4JT$eAnMxOZ(IP*Yyb`ZJ`$cT=V?wtRH;T&1yO~Xye0q(yNED0RD+|^ zb)drb7#EluSZpW5&~E^1u9tM6!?o=brhSb-Jz#=_0*V65FF2Ye-F1dNTW zZlc2@&t3&{!zu?E-lB!Oy0m17JAeE75s1rsrX4_WG8MKw0Z&e!Tc*?JUhN#cWj@|R zgx@*FccK3*nFhP=9pZzBr#ne-<$SMSWoLmguZ#r0n+DN|SNg#Fec(n2r6iN!*$3g3 z{_W+`MfsX_(nWb}iV*9C&iw=jAFTsE07GRuN-WHwC&ci~M0 zcxb{9IO`+*{20G&fmb!}t$&9&?aI#~?$f!g5YpWN3jDVU-jR9yVG{i6hrICKcoKYg z$#`LFuh5Vijw>X=*6HJg3-X2ZZg^Th2}Vq%XjZ=hNok)z(jo%@=oW(p*Dix6eah?7 zS-mkyI;&T2Hv(8yM}!gG364xrpu(-Iz|ry+G7L=)nA@fS9fqa_jDMMOl|7dNE3g79 z@C|}(AZFu(PwoJ*%u_tDeksUps$+*8k#>k{de08uoceV5X|Oa0fIZ5|4DZ_uN#|yE z!?)h52jQnHhgATue-A6{`kw>R=J9!g0AuK%E=Ks^8pr`7;V>f{bIN`8kJUmt zKKM*DBYgL;JSQIbJ%1ZqcOQJpzJF^Ae6sHTgL())88-Y~1EB}&nPAPMkf}wvOz`_l zAXD+%nc&wFAyZpkWrCx3LZ)^`v%$L{Q@hyUh&TwDGV2kz;ffUasZYBPp5h{SPF$bx zB?4!?7y&=Eo>YlW%mZVy8)lgu1UNEPE;u!k3BGgT7;u;R$A7xU1uwjl364sa>%{3% zJn#6#{1w#*n05CwzhipSzciwE92DQj55+;I;vNc)6F-*$naWTx!E2KsQ|l9$;Iudh z8FthnaQ33N;HRE~J@BN;V1o0Xf>7g|Um|d}<_P$yqgVw`GY>Ms#_4hmkb4>!Z<)RF zzys632FD&{hHQ0ep^sf6xbS60_^ajeoH$O)2xF!Vz=@~h$DGwO{Lbpc*&2BMVJ9n` zmkH3@@dY!C7%RwG7K0sbUJGiDs8$Xbwdo+1dLBG5Tp;FB7HkTvzzY0R^aE_NnIYHk R4VnM|002ovPDHLkV1fsk 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 KMNUMnLSTZSpzF8* diff --git a/tests/ref/math-mat-augment-set.png b/tests/ref/math-mat-augment-set.png index c5881b13976e15a853483b70e227573be1a4f367..1a66761591388a4bcd58dafec8824598b6a0000c 100644 GIT binary patch delta 1711 zcmV;g22lBu4zdl97k_C80{{R3#xNJ&0006#P)t-s|Ns9k%s`2WiL9)w>+9?N{r&s< z`@OxroSdA?%ggxq_^Yd{@bK{W_xJ7X?WLuqt*x!#;NYjHr@XwpOiWDW<>gjZR`K!i z+}zwlp`n-MppA`T4)zw>DTRS^D^z`(vudk}As-&c(prD|!v9Y$cwm3LA?(XijwY6DU zS?K8KXMbmBSXfx;>FN6V`ebBePEJlhKtMh|KHA#aLPA39?Ci?Q%1=*EP*6}eH#he7 z_B=d1Qc_YnIyyZ)J!oiXhK7dk@9#A=H90vsHa0f?{{H;@{D>*M#sB~WFiAu~RCwC$ z*=J8w2^5F%$B~&~7*UX-QUrE!7nNdRbrpNx-hX@Vz4x`3wZO{iBCLpt$bd@kAk55X z+uV|zT!t}olarX3`-?CBId7gg6Rzfh$K&xlxZ00m*|m%0RxUtUp#B(^dGknaNntm- zDh8nZ)e<}W@S^mG01{VaHL8l=2AJ=&!$ZWzFeI+dUNpV-7DzZY4&xXSS7j%PN^1ZK zr+>}B^j%=C)su#vl1+|qAPP}{xmMLP=ot%P2>)t@8I1iqh$(TscmOr=k9PxvDvk!N z@X~P8B}|Dc^K(C1mVSgV!uRyWmuPwSLpR}xAX>K7xe3>|p~Y9|CVbk5mZ?cM;mIkq z965~PkwxhPR=B+4S&*A^9X_J{H@*!+Mt|6Tl*mn%{EZdE{&ARzNmy>Ol*5r0c^XSt zZqC&bLCb}U0P!e9k+9sH>*59N?|96gF=mIuRUkLV(*KUh&U&=h3WusU29cXA<#2P4 zo1f#JX0$X!+=L?yXqoB+R?JJm3ZQceEyp|Dgijnt%Z)xaANBQL&~oZ448g_etACBK zy-ejR9|Pz5`Wv*2yypkVh1+J>ULcPWS%E#fQB$)4h|k#1YI`LK_x7Nty4LZmwpNmG z?OF6(8Up;&FpLqlR+4btDHP@A0r`QEd?RcxOY0Pl)qVg?Ih%mIb6pnY?M1NUjRVv|)1E}jF1zq0DB9F)8xt~gyZTog+TZ6i% z$`DEao!U@=vT!=|&)?|hY!oG?*Asuu)X9){#`Wrt~vfk zIRO)9*j{qu>J*AHTL1~SUx(-tBWy3Z;g5EsC;XfvJTeUb#Er1MKvu&(^nVm?afIoC zI~ZYmfvlJJqG$g?fY9msAf{_q+b)}7dx5O9Cs3334mf3GVutMn$lCuBTHgPFM0PmQ z@M^T|d(%z0ZarFdta20HyF>AB5xv!bvMEH(8?{XgSx6;e34L z7N%Er`mC_rWHq1DewR546Mx|NotxZbDSp(Axd01iAiBUAmYb~HM&(~$I`F=AbJeQM%TYp5)!A>9}3e$dM%^f~} z5Jlr7Kp#8~V6VBu@tPQl(wl&?@S3%Ny{vGkU|A58y>@OxQ}>Ag5Q|~3mKjbED2k)% z&~K?dK>j?0vZj-%gece5qArKowr*v%wcfiTkH_->`yczQPidWJ{mTFV002ovPDHLk FV1jcOdR+hj delta 1808 zcmV+r2k-c@4U!I!7k_IA0{{R3i}<$H00075P)t-s|NsB~{{H>_{rvp=`}_O)`uh3# z`S|$w_xJbq_V)Gl_4M@g^Yioa^78TV@$m5Q@9*#K?(XgF?d+9?4>gws~>FDU_ z=jZ3<=H}()<>ch#(^($dk<(a+D%&d$!w&CSfr%*)Hm%F4>g$;rsb$j8UW#>U2% z<)D`3pT)(+#KgqI!^6VD!ok78zrVk|y}i7=yt})*y1Kf#xw*KwxVN{rwzjslwY9Ue zv$C?Xv9YnQuYa$uuCA@Et*or9tE;Q3s;a4}sbbh~sHmu>rlzH(rK6*xp`oFmprD_h zpPrtcot>SWoSd4PnwgoIn3$NCmzS27mXwr~l9G~_eSCa;dwY9&dU|#NEijSF z1TSe?wrLugCSnCGl!O_gQcA@oyU}J|l3cTLkzxh9(8cclTlJmu&AfAjZFXng8E2gH z{PfKG!#=b7&Axl)TYP+cd~RLxk6M{Ei^RO1i!$kFW?E}#ATc>TL3C*;2Z|TkaIk7k z0DoYtEo(-VIUfTR$HF$;QZ(lk3}dZp9-6#$55SW)%w_l*!&qC^g(5RzfP}N-Fq8?5 zXZ?lfQG37>&WXcF1~8sA?njTaWu7p@FaR)~J#r2;0)J-#%x?W~z8ijV!O}*6@hszy zG_;ue7lvu2rMJ0Z7B0s!)_$Ik7OTHUFn{6ykyTfSF)R{`S zvk5JFV;C~lZ3x=%-Ah1bUW@f=zvI?|;#b{pMOzqTCMNG#GaQ4l7waEwb3YA9FAZZf&nBfGp|JsYoBr5(g*9^@Z9!K79lf_>3^Rw zlrF{%=M@(hA~P}NaBqJq3#4E9dt9>8rJ-SEVoH=h>Mo$NAqgshF1_&H2vAb#^53f` zrUW>>7cDk^g&}L>=1Jy^t-0J+4ql5tiWZB0#BemX=#U9pbEs$ugV%mrh!&ZDrg?U? zHIP*zmVP}8HDZhH1MKi*6Lu{@cz<3$dNkMBNA{74ChS^*aNW!3vEVa+I9$PmT}u$| zTZAH?7X!OjRPQ%oYYz9meYVVF`RCB2`T)S}QcT#ILsnki6py{O3RT`MOm0^fM$l!( ziKN1T$aIuh)@Y@&lEfS;N1Y;)pvb?a^6~NUxtY|G1ka&P`znNlKah)CP=6-02bhqJ zB|p~Z7xa?RT+Tw(*$&63)&?LOU^F(ah@eGx6vGG%;(raxOf1^1ea0QR zh40|9>!1^snV9|=lesRGgF{(OO_d4DOiVM}aXys=a=rsCj+{y*eCmkeM}3o4<_jOB z68`9UwAd2G;jTMC%nb*p;UE@!94*Sf!%+Lc+MS3w;fjth4r0IDj}}vZ$M7Y5ftdF{ zH?iweQR9a@fk|-~3V$Ltet706dc3h6#_Z(;;irzF$J#C!vzHTu-(QO&-(~{s1=EiM z*4%KQwk?Q*wLokFitO19OssxlGGNUOFA0Sn#=+XNHE0q%ou1qRNsk6lW%*0Vg%2)A ynca_ZBipudBhS>KPBlre#=oTU@$tEZ`4`!{o0>hWUX}m=00{s|MNUMnLSTZElAMwN diff --git a/tests/ref/math-mat-augment.png b/tests/ref/math-mat-augment.png index 0e2a42a241c8195744b2442e32bbaaffa1920ffe..306c4b1995f82e367be5442de9253dd9ae69e78e 100644 GIT binary patch literal 3563 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 B_7MO8 diff --git a/tests/ref/math-mat-baseline.png b/tests/ref/math-mat-baseline.png index d2f26621306cf21c89c4b80a0892857274b9da7a..01928f724d2784e7e1f60f56d014fb76371596d3 100644 GIT binary patch delta 806 zcmV+>1KIqt2CxQ@7k@qo00000#SVaf0008_Nklv8auqR4}@0$s&ViDZ+Jh%-!GRuWM%NT@n19-Xk4q&xgEPS;9J=@bR4025+J}UCn zIImFJy=o<%@*#mJlA>3p7jeM8c_Yghw%7} zST3BoxWnykw`IiI38MB;QzXKyzYIXkXRUMIO4V z^Xh#7%*_%BUyngk9@B-&=B(<2CQDk<*+u{wb~yps>_ihbo>=i4C0Fksg`wt!h-|nHg_^UAZetpP)Kb zb?QJNfPZK>WfH2w#{js}(e$<|O`vhmPQOVhl8ft3c6WDoe7iTLh7ToLf_~=B4H-D2$1uckbaCuCV#x{IeOo+8Gt3{QilW8>ja^2S~7Y` z3Sd-}NU13c)#(hOaN}b1b})doMbu1hMm4`tC`>{PI%~cIM*ACiwy_HU#~P%<Dtwi=ZyLD!Pp* z=pv#>Dq4l2E{deps34<_ii{Ss!oU_=h$4j2v7Eu0Qo_ozaw;{8)KpwT%TO^bmvTYP z(e#^{Cpdw8>Zsqqs}j!d`oP2goSQR>VKREuF!6@HVQ+ZEgnzX_&EUZxhru$2?8?WW zia-o87LKvfzruS^XbUa^;;eFE`w9lo;tQ|aA4LGHZ4nD!f64%If&s_zCV>0bq{7-P zCpztyENAKAYVh40%~YQb&@tB`6i(fW-kvONIX(C_lBf$$RKHXOMn| z9##=|8bFCUA%7H}lZ%dP69*Dr(A_~cQ~@YOb7jJ=v32O>BxIZ0=`KIEm@dw-PLXic zI6O4zyGS^5Iv$#tArgMP1P?8JEE10Q#Y4XFBH`!-7*lu>lm*d4y!gCsv0QlH%CNAo z&_lDQJLt=~K?zq+Ed#)mh@|*Ld@GdONBc!BQ9HwU_1JSh2(c(B$0h1Yr9Q zP<)!2*Fkk$@!*8ZFQVkW-k;nF4}Z<(5sDw?D~VV_--?E<3sLOH)K2nRORHOegogA9J zj6EXzbmNeP^};^+GX!CCukia9k^P6wgS0s16$TdnzaIhHJUrxM6A2ehp@*~zjs8sj zED~-RPj@x9!oSgvjxntwVJ0=Ar(M8}&g23>DSw-YOnB2v^vosn_Iqc2RFVxPWus6y zG6cOq+&C3U{^qABp;1EN%4O*JV#SI5wcy^U5E07r7f!UX(8=gWgM`d00aT)5^CdJg*L+EF7ICiMmeg(M>x=A(uo mS|KJz!_vHAZ`d0iG2y>=uN^j*@}lej0000r? zk^Cr1TpUC+Bsqv2{5*(@+CfZTMaz%!v&blgG7d^3cCh)eS{v@sgDwP8OMyJSNCC^$n zo5|p^J=S_sN`D6D7+C9VHW{4hVy*5>GFV|{tyTpY%$ZoLi6etEJgn7|k$x~|WUWSy z3|81!t4%=$XSrCbGm8w)yeu*zU2CzRu2>a>)s5eogmDa~{;WqgQVf31T$l=_Uw0&#lb-;BWDB<_^ zUxujil@x|W delta 483 zcmV<90UZ901n>ip7k@Pf00000ugP${0005ENklkhtA}BDSI(WNFGPnp&K8<^TWhCk#-+VCR3gL;wHZ zPoky8kK@3U39{Jk<^PLk0_ngJHlcF_-Xe7NNz2mp?}3HKd@QcbOA*EQKX&4 z7rp;uv3TXJ|2sgm(*>GY++7Ox+H8fX?~vkHMoV=kh*8r^GmB#<;gek!MKg=-*W;7j zV@orOmCoRky{t$xi#ea*lYPobGmF`u)!|3vWJt z%tbScmGRa;r+-vvX0gLAFtuaV0ul}3 z(qC0+*x0LE10-77{%1BJ#7=CQZ+H?&Z>pf7#V>t;#9aA#>)nt`lHaW+yMa{1OIr3M za>4QV{2!*w>3o`YVLm3{koXt-p0>Tk-P5s)SL~&A|9I;$to`FnqeBs+7LQsyYVq*5 Z7ywe-VaC$1f7Ac~002ovPDHLkV1ig@`9%N# diff --git a/tests/ref/math-mat-gaps.png b/tests/ref/math-mat-gaps.png index 405358776c3c42f9e20a7ac48ae152506cf3dfa1..95cd6cf113bbbedcb2fda0ac1af1bd39509cc87e 100644 GIT binary patch delta 1305 zcmV+!1?Kvl3ZDv)7k_IA00000nN1lx000E$Nkl1Kqlgn}bE5TV+*)YqC|&Q%k_VSfTVhfsVx5s7g5BlY2$nHYp` zHZwIB2(DHhjyz-(hLfKFWGzr0_U|wZ*Thl>xCW^XmrO!8(v8UV=_%Xy08BWeI2^MO zz_s-|TQdU1(SOm=)d0(56^CcUBW2eIio@eGkn-zT#o@ulNI5%5akx({QtJ9B4*On2 z%0*wrVV_c@l=>(R_pV1ueee4v>{Ei264g`tRv@LqS8>>{7AZA;io-+BAmy~b;&4z7 z8u$5b5UWCy{)*~HA_bs!Wy#np2$fAnW1XcqykQZ*`F}XZmm=b}SOUQ+yNYsaKgmO3CGg?dtlbZm>mK?^6Fj)l{%Fp!v z2zbNZuzxq~4c~*|Zj8B23U|$K!O+%Y=7htG!PtsYGs0PQ);L{c{IE@tZvguRjcDS6v2=Y=7(We;}+0(0yw)40mpD{#6dR`niw~ z+dl>_&OM1o^-`=$p(+f}S+W6Yz8RN{NL1i({|_aZc_+vK z2!BsQQy74pU2jN->xZM+KV^xX+Zsj=AGRJ~=R7n8fymiykq+;D9Rue^pgH&?ayE{U z4#&*FK%1XwC;oIbM*JGh3I}h)$iOpZg@ckX;-3eQ=Z~DEAn9=52#oZy0c1XgoY<$N z!_UTJr2j#)rncYEJ*T|hdLvx`oJ-R@4$X_7YQvh7U_4t4it%XM_Y#_I%P?zd#+^fB3v0zAwObD}{Wmm4vQsBdmJeGu0<^O^lG{$Cmw%16 z1MUDPn!1OP6CEZWZW{+2wUBDzw!crE8dL-vS%uEss{`Q5&xv~wE8l`9e2sjVg@xd- z?S1EW@JN3l5qPT^bOS2^qLN(?btBUK;W{)^l8_I#%mi@qUq{yfx{&<Q`1e3*<>gsx3<0lLJ+Xv#x#5PzvWen1a90_aJYgj@u=X4i2`?`i3Y91zpv1Mbd?cEkr!fb89(2jcV@2xlN4SU1)PxxO4781yvCZZiu P00000NkvXXu0mjfPUvZF delta 1303 zcmV+y1?c*p3Y`j&7k_OC00000PZatl000E!NklcPa0;jeqW?GhtmxUHynDfG;a*KJ19c7$ zXhcfm0G-2@a-@`7`fSH$*gRuD!vXh@QXimmctjaeE(Pfvo^S?5{?UVoxxPn{IZ@~E z{1kxSqM1;FP^uM0!a|+HJ68g@nq6Z%5V=*LhFPAVbAPzr}&7Z|EFm z#A8(9c?cY-xihgHaEwIn@Y)O@ZWGb#5h%?*5ONX7UZs1uc?NLRmSt^3#JMrl4$N-W zJE8K6);;0#OeE)z$><@TtPi`+(~u7k~2M%1?pglS_TO@ukxX;cgV5vgV_> z9p*&j+}v$3e*#P@K|UN&3$UaVd?$Ny4zTDts0OPq^(krTh@7?A4xspaw|ux}9{&i_ z$&YcsrA4TEp7{$Y->#MquS^kyX`0;w@MbHD+UJpzxIjJ}@<0%FZ0xQ9h{-@v5{#U~ zQ-7tyjiXVx7v_VP`{ONJ;{j5mQ526tPP$b(d~6DeQ)#J{cs;b)Y-Ipduc9anL5^*T zbT~c|4IKf7?Re=rv;?#m6%J2AOJJE%VQV^CMim69&OzH609cUSx z+h?PG>cYViXKY9~bSGMdzLYgz;iF?{3sCqSD)+P907uiWh9dUECKNGo z@?m02fQqyf4?fNuKLNbo3aY?50Q>nzLp_MxUZaM2?-26gwgn*eOB|q@a}vePFY*vd zJ*9>jQ;B?-tTcok%yt2){a>QGG=CM5>~=LwS1|sB*=8rnZV_b}lO zdbRG10paW4(3`u_7!YPpKH54yYBMCvzBV)+Y&3rxn_)9-hM%7B-&bRxJ`iZnT1o%_ N002ovPDHLkV1kEeWrhF% diff --git a/tests/ref/math-mat-linebreaks.png b/tests/ref/math-mat-linebreaks.png index 52ff0a8bbc61bba3604655887b7aeb893fc9115d..6666749dabf1fb1a75f67347472ee189f775a5eb 100644 GIT binary patch delta 636 zcmV-?0)zdF1&9TZ7k@Yi000006IWwm0006{Nkl? zsKBDYB3961&CpaTy2!Paixvn$vlz`*I53(naujW`)#wCE(FNJeUnvw43h7vwrO6`5 zwSg_SJ@TISz~Q|=`+9Spi|@1N>ccq~gRKIalnNczVI9_C5`TF9E^qe|Eu60V2RV7m zDk*&S7nFRnQ^Ngj^|N!75H{ujyRX4`OlDhVL9mkLf*$`z9Q&%m0#(%f#61&+EzLtlhlnMj$QxNsCupZIGC zJTq6JK9Y3izjeDjmdz<@b*TzPGwgPfaP!%fQ!KBd33yk0e`V;&Tash_Lt{Xx8Tl8^;Neo zVBAyMKe^I^rex;`KK(|tuRQQ)NrDPbyhbVfDiFhPWDKj(#2DbtY7AGmio3p+-3Ne} zk`Efg$2{V8G_yND4lzQSci3ZGvN*K WN34eLR6y+j0000Fp_9-l zGDtAGC{c8XR6@&wB3QQ48A}g}S`?Zq2AQdpA-F(8@h}obL~bT61Y6V&wl$2HZaQ@> zHn6R^Z-?Juy|=>c@!;=!d7j&c_c^=;Y*9N3wkRFeVIAIPuz&x)Haiz(;Ydq^wlv=T zDGE3B@6wi?&Fux7j(={VBfGavv&sGgK9vAi`3|s9E(ben0mdGU8UQNh#9*Txh!^wP z0$^(vgZC!@w~=P|3J@rkf&+yB@f+TwwkW{fA42eW82~YVJP1IkQwV-|5v8*>A=uoC zlBq)ou6c~oZGV#xT-}e-o$COEQ48lBE~*D0czO*0s>=Z0`T=033N2?KUoSrP_lr|K zXvNNemNifCQq9Lq7Ph|u=I1lKw0s8Ud|+A34?KE#*_4HwBGCKdxrxSZ_Y=(1bwMV^ zat!aQ0ie#8F49hZ0H}pfDZ_&t&v-74mjm2QA)7pnmFYhv*5Er3HQegV`v#Nc$v24I<; z90F1&5QDMW0m4auz7at*X7QFz>CPW%z<5%=m~ltqx>imX#p_tLd-~>#z<3 Ze*pLZl?g+Rl0X0e002ovPDHLkV1l@1Bn|)o diff --git a/tests/ref/math-mat-sparse.png b/tests/ref/math-mat-sparse.png index e9f0d948c363f982089c2fc19e5e2c57f3ae5ef1..c255fe3e59f72eeb47e723c5008be892d96f64da 100644 GIT binary patch delta 947 zcmV;k15Et#2D}H57k@kv;Lm;zwPk+tI!dW;AXW_R6-`|E( zbpbs%v@(iP$v=Zy@aj>FdPuMPR0Q)9_kdnc5}vQofm=2MFmiAKX*G9V`7(-Gy|q?o zb%oXe^)3y#a1x_rI*hZ0rJb18Z-s&xtj}OPPaVGKzzl}CC6XEb49c$o<(HM=-%n+Dqe`ffrI3b6{|Oky{Ik;`cm7|i0%C0Fqcj!x z2cxM|dBYq%7yS)@xdV#uiSNPaMTh4iAiCpryOUxJ#OY(^Ax2L-JY8_>^f&+|#}wgR zWoY%sPDQx#KeXCZp$PxH8?73xig4*(wAx#$2+yw+Cx36#7=SdTVt&AXJ`OJiRAWc(vQH-C00M$!$A9d?udr?1NHvhiIid0_(Mv5a88*x1=oT|oEsIEx=QO6LO&41`3)6ewZF(ZW_k|&G;qQVhvc;R}q zTE9>cE^a`phT;$H%Cv7stF3lLc;h;Zx`PmI^MEv^UU&e{=?DPMYDM@!82~LM?ckg_ zdyLnnTyKLF4tt|c09G7QguUNkRMpP#YG(40rsXKPhrhf;MR|*!h3cI=}{>Y%K&-G@Zu>}hJc#g%J9pt z0eXt7hWYo|{&ql~_S<}ux@*H|bl-w2xSK*wfMpZPFdI66Cqsi{aP0wrJR>j;5sW4V zhlU~OO#m1EM;#9TigLwc8ZgaAP^|tWIxH~tmr$y03wkhql=5f)lgPqZI19hEUIFYz VPg@A2?~4Ec002ovPDHLkV1ide%=Z8Q delta 872 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 delta 1812 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 delta 1224 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 diff --git a/tests/ref/math-vec-align.png b/tests/ref/math-vec-align.png index 680d0936d936349a26c04c5dfe416595c6f9be56..07d58df722224d48cc19c4a5ef0fbed5e6d819c2 100644 GIT binary patch delta 1118 zcmV-k1fl!N2<8Zo7k@Yi000006IWwm000CnNklQDi|$ zVy}M+!-`OdNV0-n_ApZ_HMQk>kdGEUEF*er>tO{!!%4KttZ6Te9uQ&=3TazSvj`up zmBV~A-FDmW+}*o#<}W=$P=DAq zSaT6$yHzgQx_16z;^Rifo~Y*se_P(o?s#yHcF86-ZC29m>}57}DO#3h=7q)ve z`-5*!ahY$sYJ0gy4VQd-jN7SgO}0m^sl4EO2WS^(^N4+^?Co)MTBrkEX_I#>qsM1X zB9m?41TSgt1Ai7WYbAfEsxP?wpyXd>3w^+C^ZCHfQb2oqpt&XR+cnaaH67GRQl|uV z&2V_*6?GnQleAX?3ma;>&z}i!B_b){k8V~CvW$sNhU4D}TtVu-sn#06F zj1bTkCQihJ7igMM!~xF9L;JQ3aYLz|j)P}bkutOuP4{MVfRhUGLn~~cx;Q+I(}CI) zcv_Ui0e?=^;GILPpxQ-vx=0IZ4Z%}QA_q7;ft1RWYQJBaK$W5Lj^}%*Qw&vtn%Kh` zPpia@gz!O(INvbPh_4?RXvF3uX~h3Ccv>0`4z35y4adRZxuEsII5=$@2Y7ru4h}Mc zrjNwIBh$&E2I1iN@q?J^eYXdiYW?AXrrNk}@PBK3Kkwk{{=qNu)z$rjEys4~`N5Hy z0@{KbK-ClRbfOxxAsA0HBRRl}3;yurY6d+MgQqcPK%4*Yy$@DbY;y-+Hm z@k7t90PRt9py_@U2l%K8?OQj7l(!K$cx)Xhp&!vS{Sl%n<(1JFMAh~MLYUCmB}xApGS#yeJT0?nf#jPWiPZF z1LtbJ*Pa0bKU(XZpDYaQ9Ovc%uX+mF(gdwb21mAt>18!&8%f(_u=EBksGE&>AIh%S>--oda4+(tlbR9M$TLqhX_wq<7`_>bz1euxqxHwy{>w zvwF#=zM|8UWCbj#lD~e4PTwVhx?`Pu|D6hN&{uU%(HB8*#LBB=OsCSv`2{teve6=+ z^*m}hEhl!t`IesN{L|L6rq3B_%6;2A-}|TLExzrFV*k{+)WQ4zxb~?3Ew+U+9apI? zHtmS-q}b%707*qoM6N<$f-FQWvj6}9 delta 1090 zcmV-I1iky_2+9bM7k@bj00000*bA`7000CLNkl+L8T)LAxuVK z&8){V`#qTM%I5$dJdB_+k9WyZwFm|;sl+nD0H#N?IlxJI2rPw3?&?@LjaB1KRlsR} z5(hZ30DplMY?Qld6`Zb8$=w)?4X}M2;BY0;x9vXQCb{Q?dPkZ*d!Ivd@Q4rC^n$1| zoPWlT5;Y^txccDP5oWBtltjO4|J1V(p90wdjj0iG2PgM+kqw?bfWh!U@D5Dbo= z#Q|QO3xgG%a=#o6gQG9Yy;A{$b64{(^_RFq&42!vx-l!^oByRYpeE=^`d{U zS+DQ*54LpZJNdz3N(9z8a*pT1>0EM-$HA#GY|MVS{$+4RE`mx_49Zdwg}{oekh@)B z0n=ld9N=^8hwPyCoIKk-MIo~6;hccElC!1O}^iyuH9`}8t%fIl>h($07*qo IM6N<$g8pp?od5s; diff --git a/tests/ref/math-vec-gap.png b/tests/ref/math-vec-gap.png index e48b3e9022b8742f1a3c1557e809c8a1ddee127f..ccfb217111766e8c1f7ac89688022582a4b0cfcc 100644 GIT binary patch delta 423 zcmV;Y0a*T|1GEE>7k@Me00000WR;^C0004dNklUnzH-(~#KmFch!>#z<>!x@LRlP|LHS6>RqLDuvs3V+X>!`YY@h&d(U+hKq` z4}j%H5Vl{H;Fv`YerTlx)AA?=2j+gmi+(Y=V zZmUdoObhWtl6wuDCdw{I7jr6qmeiV?nINn|R@qPnTzU^K*S-j8}l^?u-P8N5jqVm%^>11(B8k)r|bh6k}7nQH;Nhgcj zGEn&$Z3AZUaF1gz^f>k!?m^v!mXA}q=w$KYK(wG=La(yg3QXOcTKNnT#|PeTQMJecg2JwEfL~ z1!3Ha{72vZ@%L5;-SBO6C}Py&QHw_{9x)aJ05|AziVz_-XVd@y002ovPDHLkV1ma0 B%pw2) diff --git a/tests/ref/math-vec-linebreaks.png b/tests/ref/math-vec-linebreaks.png index 52ff0a8bbc61bba3604655887b7aeb893fc9115d..6666749dabf1fb1a75f67347472ee189f775a5eb 100644 GIT binary patch delta 636 zcmV-?0)zdF1&9TZ7k@Yi000006IWwm0006{Nkl? zsKBDYB3961&CpaTy2!Paixvn$vlz`*I53(naujW`)#wCE(FNJeUnvw43h7vwrO6`5 zwSg_SJ@TISz~Q|=`+9Spi|@1N>ccq~gRKIalnNczVI9_C5`TF9E^qe|Eu60V2RV7m zDk*&S7nFRnQ^Ngj^|N!75H{ujyRX4`OlDhVL9mkLf*$`z9Q&%m0#(%f#61&+EzLtlhlnMj$QxNsCupZIGC zJTq6JK9Y3izjeDjmdz<@b*TzPGwgPfaP!%fQ!KBd33yk0e`V;&Tash_Lt{Xx8Tl8^;Neo zVBAyMKe^I^rex;`KK(|tuRQQ)NrDPbyhbVfDiFhPWDKj(#2DbtY7AGmio3p+-3Ne} zk`Efg$2{V8G_yND4lzQSci3ZGvN*K WN34eLR6y+j0000Fp_9-l zGDtAGC{c8XR6@&wB3QQ48A}g}S`?Zq2AQdpA-F(8@h}obL~bT61Y6V&wl$2HZaQ@> zHn6R^Z-?Juy|=>c@!;=!d7j&c_c^=;Y*9N3wkRFeVIAIPuz&x)Haiz(;Ydq^wlv=T zDGE3B@6wi?&Fux7j(={VBfGavv&sGgK9vAi`3|s9E(ben0mdGU8UQNh#9*Txh!^wP z0$^(vgZC!@w~=P|3J@rkf&+yB@f+TwwkW{fA42eW82~YVJP1IkQwV-|5v8*>A=uoC zlBq)ou6c~oZGV#xT-}e-o$COEQ48lBE~*D0czO*0s>=Z0`T=033N2?KUoSrP_lr|K zXvNNemNifCQq9Lq7Ph|u=I1lKw0s8Ud|+A34?KE#*_4HwBGCKdxrxSZ_Y=(1bwMV^ zat!aQ0ie#8F49hZ0H}pfDZ_&t&v-74mjm2QA)7pnmFYhv*5Er3HQegV`v#Nc$v24I<; z90F1&5QDMW0m4auz7at*X7QFz>CPW%z<5%=m~ltqx>imX#p_tLd-~>#z<3 Ze*pLZl?g+Rl0X0e002ovPDHLkV1l@1Bn|)o diff --git a/tests/ref/math-vec-wide.png b/tests/ref/math-vec-wide.png index 9dc887a8cd7640ae9f0ba0c7707c6bfca25d0c52..000e3cf2a6f608878d55cfe87a78114e69a49222 100644 GIT binary patch delta 618 zcmV-w0+s#j1oi}w7k@Yi000006IWwm0006#Nkl=?-`3T7r@lqvMXjke}0$Sz{GgsfIt(T}Y}GhJmd(-Ah( z<$k!mV>^AH;hu~8 z{t7f%FABj;X4D4j%x$PW-YEnhu1B}A)k1KI8{LwNV>0kYFZ!(v$iPKI=vUk$0~dZl zzYSg)c%c^{(tmK?G)1OqGTGaYegt6oRmq8M*EoIyfN?tj$t~2L%h$vsjlLhvz|3^I zxC)Q;V|J-9Oq6}i7yO{*ZlVTw(v;3krX{b)^nG_67!CRwg25O-+p*686guY&vu85e z0fYwAjgV=Dqjetu%bMp7mw(b31^n&ly46)DPrP{9s;=5w zM$6uOt(w~%!zd|mugPTct1$Q6RX;_4r=_pa6c3=S$Rz`>7(lNyoOv!F z2UGW!9F3NzBwVbKQid9@n>tlkpKVy07*qoM6N<$ Ef_r}@Z2$lO delta 608 zcmV-m0-yc%1ndNm7k@bj00000*bA`70006rNklygdj{E$oyo{@e?5QG%;J|3DBMfQ zDBKB4Xl8N6I(!ztv!d2HcsNxSdZ-rD%B1>rL+HyTu#3|JU_G zitc&Py7GU~N`Gouym|J5`$<50cQ**kM~h?Et0)%#pB+cFIDT4u;r~K(LB>N+d)5Mp zAMI^b!EJ32C)7cLnqwN}LGAwk<~4L5^IQj0`}h4nj_l)$>HpW;gIS6@DYrQC#zq{8 z>yoaI$0}rtpO?IeolUjG_5CicRQ>VZy?Z|(74T}P+JD{GXq%7OP7x~FmJG9!i?GE< zXk)R`c>)T=uhGV0d-UA$5Vbb?!Tg0b7Dq2a6Np%V!o4FzqZUYOBPxGeJF3O|Eofx% zaSzlc*6vxT7SFAwk;Q-HP$uq5R2?XUW8T3LJ{5j9bV zrM^Jo?pbc3U2oCvJxY20?k#eA*5?E5EWT2STW)7JEk^{}&*GAK7Wt2+V<2_cv5Wsm uensO^9~|cY{4hGsHEQvw#iJI}7ytl}=_J?)Agx#c0000 Date: Wed, 4 Jun 2025 14:31:06 +0100 Subject: [PATCH 142/172] Numbering implementation refactor (#6122) --- crates/typst-library/src/model/numbering.rs | 667 ++++++++++---------- tests/suite/model/numbering.typ | 42 +- 2 files changed, 354 insertions(+), 355 deletions(-) diff --git a/crates/typst-library/src/model/numbering.rs b/crates/typst-library/src/model/numbering.rs index 320ed7d17..236ced361 100644 --- a/crates/typst-library/src/model/numbering.rs +++ b/crates/typst-library/src/model/numbering.rs @@ -9,7 +9,6 @@ use ecow::{eco_format, EcoString, EcoVec}; use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{cast, func, Context, Func, Str, Value}; -use crate::text::Case; /// Applies a numbering to a sequence of numbers. /// @@ -381,40 +380,194 @@ impl NumberingKind { /// Apply the numbering to the given number. pub fn apply(self, n: u64) -> EcoString { match self { - Self::Arabic => eco_format!("{n}"), - Self::LowerRoman => roman_numeral(n, Case::Lower), - Self::UpperRoman => roman_numeral(n, Case::Upper), - Self::LowerGreek => greek_numeral(n, Case::Lower), - Self::UpperGreek => greek_numeral(n, Case::Upper), - Self::Symbol => { - if n == 0 { - return '-'.into(); - } - - const SYMBOLS: &[char] = &['*', '†', '‡', '§', '¶', '‖']; - let n_symbols = SYMBOLS.len() as u64; - let symbol = SYMBOLS[((n - 1) % n_symbols) as usize]; - let amount = ((n - 1) / n_symbols) + 1; - std::iter::repeat_n(symbol, amount.try_into().unwrap()).collect() + Self::Arabic => { + numeric(&['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], n) } - Self::Hebrew => hebrew_numeral(n), - - Self::LowerLatin => zeroless( - [ + Self::LowerRoman => additive( + &[ + ("m̅", 1000000), + ("d̅", 500000), + ("c̅", 100000), + ("l̅", 50000), + ("x̅", 10000), + ("v̅", 5000), + ("i̅v̅", 4000), + ("m", 1000), + ("cm", 900), + ("d", 500), + ("cd", 400), + ("c", 100), + ("xc", 90), + ("l", 50), + ("xl", 40), + ("x", 10), + ("ix", 9), + ("v", 5), + ("iv", 4), + ("i", 1), + ("n", 0), + ], + n, + ), + Self::UpperRoman => additive( + &[ + ("M̅", 1000000), + ("D̅", 500000), + ("C̅", 100000), + ("L̅", 50000), + ("X̅", 10000), + ("V̅", 5000), + ("I̅V̅", 4000), + ("M", 1000), + ("CM", 900), + ("D", 500), + ("CD", 400), + ("C", 100), + ("XC", 90), + ("L", 50), + ("XL", 40), + ("X", 10), + ("IX", 9), + ("V", 5), + ("IV", 4), + ("I", 1), + ("N", 0), + ], + n, + ), + Self::LowerGreek => additive( + &[ + ("͵θ", 9000), + ("͵η", 8000), + ("͵ζ", 7000), + ("͵ϛ", 6000), + ("͵ε", 5000), + ("͵δ", 4000), + ("͵γ", 3000), + ("͵β", 2000), + ("͵α", 1000), + ("ϡ", 900), + ("ω", 800), + ("ψ", 700), + ("χ", 600), + ("φ", 500), + ("υ", 400), + ("τ", 300), + ("σ", 200), + ("ρ", 100), + ("ϟ", 90), + ("π", 80), + ("ο", 70), + ("ξ", 60), + ("ν", 50), + ("μ", 40), + ("λ", 30), + ("κ", 20), + ("ι", 10), + ("θ", 9), + ("η", 8), + ("ζ", 7), + ("ϛ", 6), + ("ε", 5), + ("δ", 4), + ("γ", 3), + ("β", 2), + ("α", 1), + ("𐆊", 0), + ], + n, + ), + Self::UpperGreek => additive( + &[ + ("͵Θ", 9000), + ("͵Η", 8000), + ("͵Ζ", 7000), + ("͵Ϛ", 6000), + ("͵Ε", 5000), + ("͵Δ", 4000), + ("͵Γ", 3000), + ("͵Β", 2000), + ("͵Α", 1000), + ("Ϡ", 900), + ("Ω", 800), + ("Ψ", 700), + ("Χ", 600), + ("Φ", 500), + ("Υ", 400), + ("Τ", 300), + ("Σ", 200), + ("Ρ", 100), + ("Ϟ", 90), + ("Π", 80), + ("Ο", 70), + ("Ξ", 60), + ("Ν", 50), + ("Μ", 40), + ("Λ", 30), + ("Κ", 20), + ("Ι", 10), + ("Θ", 9), + ("Η", 8), + ("Ζ", 7), + ("Ϛ", 6), + ("Ε", 5), + ("Δ", 4), + ("Γ", 3), + ("Β", 2), + ("Α", 1), + ("𐆊", 0), + ], + n, + ), + Self::Hebrew => additive( + &[ + ("ת", 400), + ("ש", 300), + ("ר", 200), + ("ק", 100), + ("צ", 90), + ("פ", 80), + ("ע", 70), + ("ס", 60), + ("נ", 50), + ("מ", 40), + ("ל", 30), + ("כ", 20), + ("יט", 19), + ("יח", 18), + ("יז", 17), + ("טז", 16), + ("טו", 15), + ("י", 10), + ("ט", 9), + ("ח", 8), + ("ז", 7), + ("ו", 6), + ("ה", 5), + ("ד", 4), + ("ג", 3), + ("ב", 2), + ("א", 1), + ("-", 0), + ], + n, + ), + Self::LowerLatin => alphabetic( + &[ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ], n, ), - Self::UpperLatin => zeroless( - [ + Self::UpperLatin => alphabetic( + &[ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ], n, ), - Self::HiraganaAiueo => zeroless( - [ + Self::HiraganaAiueo => alphabetic( + &[ 'あ', 'い', 'う', 'え', 'お', 'か', 'き', 'く', 'け', 'こ', 'さ', 'し', 'す', 'せ', 'そ', 'た', 'ち', 'つ', 'て', 'と', 'な', 'に', 'ぬ', 'ね', 'の', 'は', 'ひ', 'ふ', 'へ', 'ほ', 'ま', 'み', 'む', @@ -423,8 +576,8 @@ impl NumberingKind { ], n, ), - Self::HiraganaIroha => zeroless( - [ + Self::HiraganaIroha => alphabetic( + &[ 'い', 'ろ', 'は', 'に', 'ほ', 'へ', 'と', 'ち', 'り', 'ぬ', 'る', 'を', 'わ', 'か', 'よ', 'た', 'れ', 'そ', 'つ', 'ね', 'な', 'ら', 'む', 'う', 'ゐ', 'の', 'お', 'く', 'や', 'ま', 'け', 'ふ', 'こ', @@ -433,8 +586,8 @@ impl NumberingKind { ], n, ), - Self::KatakanaAiueo => zeroless( - [ + Self::KatakanaAiueo => alphabetic( + &[ 'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ', 'コ', 'サ', 'シ', 'ス', 'セ', 'ソ', 'タ', 'チ', 'ツ', 'テ', 'ト', 'ナ', 'ニ', 'ヌ', 'ネ', 'ノ', 'ハ', 'ヒ', 'フ', 'ヘ', 'ホ', 'マ', 'ミ', 'ム', @@ -443,8 +596,8 @@ impl NumberingKind { ], n, ), - Self::KatakanaIroha => zeroless( - [ + Self::KatakanaIroha => alphabetic( + &[ 'イ', 'ロ', 'ハ', 'ニ', 'ホ', 'ヘ', 'ト', 'チ', 'リ', 'ヌ', 'ル', 'ヲ', 'ワ', 'カ', 'ヨ', 'タ', 'レ', 'ソ', 'ツ', 'ネ', 'ナ', 'ラ', 'ム', 'ウ', 'ヰ', 'ノ', 'オ', 'ク', 'ヤ', 'マ', 'ケ', 'フ', 'コ', @@ -453,40 +606,40 @@ impl NumberingKind { ], n, ), - Self::KoreanJamo => zeroless( - [ + Self::KoreanJamo => alphabetic( + &[ 'ㄱ', 'ㄴ', 'ㄷ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅅ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ', ], n, ), - Self::KoreanSyllable => zeroless( - [ + Self::KoreanSyllable => alphabetic( + &[ '가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카', '타', '파', '하', ], n, ), - Self::BengaliLetter => zeroless( - [ + Self::BengaliLetter => alphabetic( + &[ 'ক', 'খ', 'গ', 'ঘ', 'ঙ', 'চ', 'ছ', 'জ', 'ঝ', 'ঞ', 'ট', 'ঠ', 'ড', 'ঢ', 'ণ', 'ত', 'থ', 'দ', 'ধ', 'ন', 'প', 'ফ', 'ব', 'ভ', 'ম', 'য', 'র', 'ল', 'শ', 'ষ', 'স', 'হ', ], n, ), - Self::CircledNumber => zeroless( - [ - '①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩', '⑪', '⑫', '⑬', '⑭', - '⑮', '⑯', '⑰', '⑱', '⑲', '⑳', '㉑', '㉒', '㉓', '㉔', '㉕', '㉖', - '㉗', '㉘', '㉙', '㉚', '㉛', '㉜', '㉝', '㉞', '㉟', '㊱', '㊲', - '㊳', '㊴', '㊵', '㊶', '㊷', '㊸', '㊹', '㊺', '㊻', '㊼', '㊽', - '㊾', '㊿', + Self::CircledNumber => fixed( + &[ + '⓪', '①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩', '⑪', '⑫', '⑬', + '⑭', '⑮', '⑯', '⑰', '⑱', '⑲', '⑳', '㉑', '㉒', '㉓', '㉔', '㉕', + '㉖', '㉗', '㉘', '㉙', '㉚', '㉛', '㉜', '㉝', '㉞', '㉟', '㊱', + '㊲', '㊳', '㊴', '㊵', '㊶', '㊷', '㊸', '㊹', '㊺', '㊻', '㊼', + '㊽', '㊾', '㊿', ], n, ), Self::DoubleCircledNumber => { - zeroless(['⓵', '⓶', '⓷', '⓸', '⓹', '⓺', '⓻', '⓼', '⓽', '⓾'], n) + fixed(&['0', '⓵', '⓶', '⓷', '⓸', '⓹', '⓺', '⓻', '⓼', '⓽', '⓾'], n) } Self::LowerSimplifiedChinese => { @@ -502,306 +655,170 @@ impl NumberingKind { u64_to_chinese(ChineseVariant::Traditional, ChineseCase::Upper, n).into() } - Self::EasternArabic => decimal('\u{0660}', n), - Self::EasternArabicPersian => decimal('\u{06F0}', n), - Self::DevanagariNumber => decimal('\u{0966}', n), - Self::BengaliNumber => decimal('\u{09E6}', n), - } - } -} - -/// Stringify an integer to a Hebrew number. -fn hebrew_numeral(mut n: u64) -> EcoString { - if n == 0 { - return '-'.into(); - } - let mut fmt = EcoString::new(); - 'outer: for (name, value) in [ - ('ת', 400), - ('ש', 300), - ('ר', 200), - ('ק', 100), - ('צ', 90), - ('פ', 80), - ('ע', 70), - ('ס', 60), - ('נ', 50), - ('מ', 40), - ('ל', 30), - ('כ', 20), - ('י', 10), - ('ט', 9), - ('ח', 8), - ('ז', 7), - ('ו', 6), - ('ה', 5), - ('ד', 4), - ('ג', 3), - ('ב', 2), - ('א', 1), - ] { - while n >= value { - match n { - 15 => fmt.push_str("ט״ו"), - 16 => fmt.push_str("ט״ז"), - _ => { - let append_geresh = n == value && fmt.is_empty(); - if n == value && !fmt.is_empty() { - fmt.push('״'); - } - fmt.push(name); - if append_geresh { - fmt.push('׳'); - } - - n -= value; - continue; - } + Self::EasternArabic => { + numeric(&['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'], n) } - break 'outer; - } - } - fmt -} - -/// Stringify an integer to a Roman numeral. -fn roman_numeral(mut n: u64, case: Case) -> EcoString { - if n == 0 { - return match case { - Case::Lower => 'n'.into(), - Case::Upper => 'N'.into(), - }; - } - - // Adapted from Yann Villessuzanne's roman.rs under the - // Unlicense, at https://github.com/linfir/roman.rs/ - let mut fmt = EcoString::new(); - for &(name, value) in &[ - ("M̅", 1000000), - ("D̅", 500000), - ("C̅", 100000), - ("L̅", 50000), - ("X̅", 10000), - ("V̅", 5000), - ("I̅V̅", 4000), - ("M", 1000), - ("CM", 900), - ("D", 500), - ("CD", 400), - ("C", 100), - ("XC", 90), - ("L", 50), - ("XL", 40), - ("X", 10), - ("IX", 9), - ("V", 5), - ("IV", 4), - ("I", 1), - ] { - while n >= value { - n -= value; - for c in name.chars() { - match case { - Case::Lower => fmt.extend(c.to_lowercase()), - Case::Upper => fmt.push(c), - } + Self::EasternArabicPersian => { + numeric(&['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'], n) } + Self::DevanagariNumber => { + numeric(&['०', '१', '२', '३', '४', '५', '६', '७', '८', '९'], n) + } + Self::BengaliNumber => { + numeric(&['০', '১', '২', '৩', '৪', '৫', '৬', '৭', '৮', '৯'], n) + } + Self::Symbol => symbolic(&['*', '†', '‡', '§', '¶', '‖'], n), } } - - fmt } -/// Stringify an integer to Greek numbers. +/// Stringify a number using symbols representing values. The decimal +/// representation of the number is recovered by summing over the values of the +/// symbols present. /// -/// Greek numbers use the Greek Alphabet to represent numbers; it is based on 10 -/// (decimal). Here we implement the single digit M power representation from -/// [The Greek Number Converter][convert] and also described in -/// [Greek Numbers][numbers]. -/// -/// [converter]: https://www.russellcottrell.com/greek/utilities/GreekNumberConverter.htm -/// [numbers]: https://mathshistory.st-andrews.ac.uk/HistTopics/Greek_numbers/ -fn greek_numeral(n: u64, case: Case) -> EcoString { - let thousands = [ - ["͵α", "͵Α"], - ["͵β", "͵Β"], - ["͵γ", "͵Γ"], - ["͵δ", "͵Δ"], - ["͵ε", "͵Ε"], - ["͵ϛ", "͵Ϛ"], - ["͵ζ", "͵Ζ"], - ["͵η", "͵Η"], - ["͵θ", "͵Θ"], - ]; - let hundreds = [ - ["ρ", "Ρ"], - ["σ", "Σ"], - ["τ", "Τ"], - ["υ", "Υ"], - ["φ", "Φ"], - ["χ", "Χ"], - ["ψ", "Ψ"], - ["ω", "Ω"], - ["ϡ", "Ϡ"], - ]; - let tens = [ - ["ι", "Ι"], - ["κ", "Κ"], - ["λ", "Λ"], - ["μ", "Μ"], - ["ν", "Ν"], - ["ξ", "Ξ"], - ["ο", "Ο"], - ["π", "Π"], - ["ϙ", "Ϟ"], - ]; - let ones = [ - ["α", "Α"], - ["β", "Β"], - ["γ", "Γ"], - ["δ", "Δ"], - ["ε", "Ε"], - ["ϛ", "Ϛ"], - ["ζ", "Ζ"], - ["η", "Η"], - ["θ", "Θ"], - ]; - - if n == 0 { - // Greek Zero Sign - return '𐆊'.into(); - } - - let mut fmt = EcoString::new(); - let case = match case { - Case::Lower => 0, - Case::Upper => 1, - }; - - // Extract a list of decimal digits from the number - let mut decimal_digits: Vec = Vec::new(); - let mut n = n; - while n > 0 { - decimal_digits.push((n % 10) as usize); - n /= 10; - } - - // Pad the digits with leading zeros to ensure we can form groups of 4 - while decimal_digits.len() % 4 != 0 { - decimal_digits.push(0); - } - decimal_digits.reverse(); - - let mut m_power = decimal_digits.len() / 4; - - // M are used to represent 10000, M_power = 2 means 10000^2 = 10000 0000 - // The prefix of M is also made of Greek numerals but only be single digits, so it is 9 at max. This enables us - // to represent up to (10000)^(9 + 1) - 1 = 10^40 -1 (9,999,999,999,999,999,999,999,999,999,999,999,999,999) - let get_m_prefix = |m_power: usize| { - if m_power == 0 { - None - } else { - assert!(m_power <= 9); - // the prefix of M is a single digit lowercase - Some(ones[m_power - 1][0]) - } - }; - - let mut previous_has_number = false; - for chunk in decimal_digits.chunks_exact(4) { - // chunk must be exact 4 item - assert_eq!(chunk.len(), 4); - - m_power = m_power.saturating_sub(1); - - // `th`ousan, `h`undred, `t`en and `o`ne - let (th, h, t, o) = (chunk[0], chunk[1], chunk[2], chunk[3]); - if th + h + t + o == 0 { - continue; - } - - if previous_has_number { - fmt.push_str(", "); - } - - if let Some(m_prefix) = get_m_prefix(m_power) { - fmt.push_str(m_prefix); - fmt.push_str("Μ"); - } - if th != 0 { - let thousand_digit = thousands[th - 1][case]; - fmt.push_str(thousand_digit); - } - if h != 0 { - let hundred_digit = hundreds[h - 1][case]; - fmt.push_str(hundred_digit); - } - if t != 0 { - let ten_digit = tens[t - 1][case]; - fmt.push_str(ten_digit); - } - if o != 0 { - let one_digit = ones[o - 1][case]; - fmt.push_str(one_digit); - } - // if we do not have thousan, we need to append 'ʹ' at the end. - if th == 0 { - fmt.push_str("ʹ"); - } - previous_has_number = true; - } - fmt -} - -/// Stringify a number using a base-N counting system with no zero digit. -/// -/// This is best explained by example. Suppose our digits are 'A', 'B', and 'C'. -/// We would get the following: +/// Consider the situation where ['I': 1, 'IV': 4, 'V': 5], /// /// ```text -/// 1 => "A" -/// 2 => "B" -/// 3 => "C" -/// 4 => "AA" -/// 5 => "AB" -/// 6 => "AC" -/// 7 => "BA" -/// 8 => "BB" -/// 9 => "BC" -/// 10 => "CA" -/// 11 => "CB" -/// 12 => "CC" -/// 13 => "AAA" -/// etc. +/// 1 => 'I' +/// 2 => 'II' +/// 3 => 'III' +/// 4 => 'IV' +/// 5 => 'V' +/// 6 => 'VI' +/// 7 => 'VII' +/// 8 => 'VIII' /// ``` /// -/// You might be familiar with this scheme from the way spreadsheet software -/// tends to label its columns. -fn zeroless(alphabet: [char; N_DIGITS], mut n: u64) -> EcoString { +/// where this is the start of the familiar Roman numeral system. +fn additive(symbols: &[(&str, u64)], mut n: u64) -> EcoString { + if n == 0 { + if let Some(&(symbol, 0)) = symbols.last() { + return symbol.into(); + } + return '0'.into(); + } + + let mut s = EcoString::new(); + for (symbol, weight) in symbols { + if *weight == 0 || *weight > n { + continue; + } + let reps = n / weight; + for _ in 0..reps { + s.push_str(symbol); + } + + n -= weight * reps; + if n == 0 { + return s; + } + } + s +} + +/// Stringify a number using a base-n (where n is the number of provided +/// symbols) system without a zero symbol. +/// +/// Consider the situation where ['A', 'B', 'C'] are the provided symbols, +/// +/// ```text +/// 1 => 'A' +/// 2 => 'B' +/// 3 => 'C' +/// 4 => 'AA +/// 5 => 'AB' +/// 6 => 'AC' +/// 7 => 'BA' +/// ... +/// ``` +/// +/// This system is commonly used in spreadsheet software. +fn alphabetic(symbols: &[char], mut n: u64) -> EcoString { + let n_digits = symbols.len() as u64; if n == 0 { return '-'.into(); } - let n_digits = N_DIGITS as u64; - let mut cs = EcoString::new(); - while n > 0 { + let mut s = EcoString::new(); + while n != 0 { n -= 1; - cs.push(alphabet[(n % n_digits) as usize]); + s.push(symbols[(n % n_digits) as usize]); n /= n_digits; } - cs.chars().rev().collect() + s.chars().rev().collect() } -/// Stringify a number using a base-10 counting system with a zero digit. +/// Stringify a number using the symbols provided, defaulting to the arabic +/// representation when the number is greater than the number of symbols. /// -/// This function assumes that the digits occupy contiguous codepoints. -fn decimal(start: char, mut n: u64) -> EcoString { - if n == 0 { - return start.into(); +/// Consider the situation where ['0', 'A', 'B', 'C'] are the provided symbols, +/// +/// ```text +/// 0 => '0' +/// 1 => 'A' +/// 2 => 'B' +/// 3 => 'C' +/// 4 => '4' +/// ... +/// n => 'n' +/// ``` +fn fixed(symbols: &[char], n: u64) -> EcoString { + let n_digits = symbols.len() as u64; + if n < n_digits { + return symbols[(n) as usize].into(); } - let mut cs = EcoString::new(); - while n > 0 { - cs.push(char::from_u32((start as u32) + ((n % 10) as u32)).unwrap()); - n /= 10; - } - cs.chars().rev().collect() + eco_format!("{n}") +} + +/// Stringify a number using a base-n (where n is the number of provided +/// symbols) system with a zero symbol. +/// +/// Consider the situation where ['0', '1', '2'] are the provided symbols, +/// +/// ```text +/// 0 => '0' +/// 1 => '1' +/// 2 => '2' +/// 3 => '10' +/// 4 => '11' +/// 5 => '12' +/// 6 => '20' +/// ... +/// ``` +/// +/// which is the familiar trinary counting system. +fn numeric(symbols: &[char], mut n: u64) -> EcoString { + let n_digits = symbols.len() as u64; + if n == 0 { + return symbols[0].into(); + } + let mut s = EcoString::new(); + while n != 0 { + s.push(symbols[(n % n_digits) as usize]); + n /= n_digits; + } + s.chars().rev().collect() +} + +/// Stringify a number using repeating symbols. +/// +/// Consider the situation where ['A', 'B', 'C'] are the provided symbols, +/// +/// ```text +/// 0 => '-' +/// 1 => 'A' +/// 2 => 'B' +/// 3 => 'C' +/// 4 => 'AA' +/// 5 => 'BB' +/// 6 => 'CC' +/// 7 => 'AAA' +/// ... +/// ``` +fn symbolic(symbols: &[char], n: u64) -> EcoString { + let n_digits = symbols.len() as u64; + if n == 0 { + return '-'.into(); + } + EcoString::from(symbols[((n - 1) % n_digits) as usize]) + .repeat((n.div_ceil(n_digits)) as usize) } diff --git a/tests/suite/model/numbering.typ b/tests/suite/model/numbering.typ index 6af989ff1..2d6a3d6a6 100644 --- a/tests/suite/model/numbering.typ +++ b/tests/suite/model/numbering.typ @@ -19,50 +19,32 @@ // Greek. #t( pat: "α", - "𐆊", "αʹ", "βʹ", "γʹ", "δʹ", "εʹ", "ϛʹ", "ζʹ", "ηʹ", "θʹ", "ιʹ", - "ιαʹ", "ιβʹ", "ιγʹ", "ιδʹ", "ιεʹ", "ιϛʹ", "ιζʹ", "ιηʹ", "ιθʹ", "κʹ", - 241, "σμαʹ", - 999, "ϡϙθʹ", + "𐆊", "α", "β", "γ", "δ", "ε", "ϛ", "ζ", "η", "θ", "ι", + "ια", "ιβ", "ιγ", "ιδ", "ιε", "ιϛ", "ιζ", "ιη", "ιθ", "κ", + 241, "σμα", + 999, "ϡϟθ", 1005, "͵αε", - 1999, "͵αϡϙθ", - 2999, "͵βϡϙθ", + 1999, "͵αϡϟθ", + 2999, "͵βϡϟθ", 3000, "͵γ", - 3398, "͵γτϙη", + 3398, "͵γτϟη", 4444, "͵δυμδ", 5683, "͵εχπγ", 9184, "͵θρπδ", - 9999, "͵θϡϙθ", - 20000, "αΜβʹ", - 20001, "αΜβʹ, αʹ", - 97554, "αΜθʹ, ͵ζφνδ", - 99999, "αΜθʹ, ͵θϡϙθ", - 1000000, "αΜρʹ", - 1000001, "αΜρʹ, αʹ", - 1999999, "αΜρϙθʹ, ͵θϡϙθ", - 2345678, "αΜσλδʹ, ͵εχοη", - 9999999, "αΜϡϙθʹ, ͵θϡϙθ", - 10000000, "αΜ͵α", - 90000001, "αΜ͵θ, αʹ", - 100000000, "βΜαʹ", - 1000000000, "βΜιʹ", - 2000000000, "βΜκʹ", - 2000000001, "βΜκʹ, αʹ", - 2000010001, "βΜκʹ, αΜαʹ, αʹ", - 2056839184, "βΜκʹ, αΜ͵εχπγ, ͵θρπδ", - 12312398676, "βΜρκγʹ, αΜ͵ασλθ, ͵ηχοϛ", + 9999, "͵θϡϟθ", ) #t( pat: sym.Alpha, - "𐆊", "Αʹ", "Βʹ", "Γʹ", "Δʹ", "Εʹ", "Ϛʹ", "Ζʹ", "Ηʹ", "Θʹ", "Ιʹ", - "ΙΑʹ", "ΙΒʹ", "ΙΓʹ", "ΙΔʹ", "ΙΕʹ", "ΙϚʹ", "ΙΖʹ", "ΙΗʹ", "ΙΘʹ", "Κʹ", - 241, "ΣΜΑʹ", + "𐆊", "Α", "Β", "Γ", "Δ", "Ε", "Ϛ", "Ζ", "Η", "Θ", "Ι", + "ΙΑ", "ΙΒ", "ΙΓ", "ΙΔ", "ΙΕ", "ΙϚ", "ΙΖ", "ΙΗ", "ΙΘ", "Κ", + 241, "ΣΜΑ", ) // Symbols. #t(pat: "*", "-", "*", "†", "‡", "§", "¶", "‖", "**") // Hebrew. -#t(pat: "א", step: 2, 9, "ט׳", "י״א", "י״ג") +#t(pat: "א", step: 2, 9, "ט", "יא", "יג", 15, "טו", 16, "טז") // Chinese. #t(pat: "一", step: 2, 9, "九", "十一", "十三", "十五", "十七", "十九") From 6725061841e327227a49f90134136264a5b8c584 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Mon, 9 Jun 2025 10:46:29 -0300 Subject: [PATCH 143/172] Pin colspan and rowspan for blank cells (#6401) --- crates/typst-library/src/layout/grid/mod.rs | 9 ++++++++- crates/typst-library/src/model/table.rs | 9 ++++++++- .../ref/issue-6399-grid-cell-colspan-set-rule.png | Bin 0 -> 232 bytes .../ref/issue-6399-grid-cell-rowspan-set-rule.png | Bin 0 -> 232 bytes tests/suite/layout/grid/colspan.typ | 4 ++++ tests/suite/layout/grid/rowspan.typ | 4 ++++ 6 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 tests/ref/issue-6399-grid-cell-colspan-set-rule.png create mode 100644 tests/ref/issue-6399-grid-cell-rowspan-set-rule.png diff --git a/crates/typst-library/src/layout/grid/mod.rs b/crates/typst-library/src/layout/grid/mod.rs index 6616c3311..369df11ee 100644 --- a/crates/typst-library/src/layout/grid/mod.rs +++ b/crates/typst-library/src/layout/grid/mod.rs @@ -755,7 +755,14 @@ impl Show for Packed { impl Default for Packed { fn default() -> Self { - Packed::new(GridCell::new(Content::default())) + Packed::new( + // Explicitly set colspan and rowspan to ensure they won't be + // overridden by set rules (default cells are created after + // colspans and rowspans are processed in the resolver) + GridCell::new(Content::default()) + .with_colspan(NonZeroUsize::ONE) + .with_rowspan(NonZeroUsize::ONE), + ) } } diff --git a/crates/typst-library/src/model/table.rs b/crates/typst-library/src/model/table.rs index 6f4461bd4..373230897 100644 --- a/crates/typst-library/src/model/table.rs +++ b/crates/typst-library/src/model/table.rs @@ -770,7 +770,14 @@ impl Show for Packed { impl Default for Packed { fn default() -> Self { - Packed::new(TableCell::new(Content::default())) + Packed::new( + // Explicitly set colspan and rowspan to ensure they won't be + // overridden by set rules (default cells are created after + // colspans and rowspans are processed in the resolver) + TableCell::new(Content::default()) + .with_colspan(NonZeroUsize::ONE) + .with_rowspan(NonZeroUsize::ONE), + ) } } diff --git a/tests/ref/issue-6399-grid-cell-colspan-set-rule.png b/tests/ref/issue-6399-grid-cell-colspan-set-rule.png new file mode 100644 index 0000000000000000000000000000000000000000..a40eda78dc1708901754f8c1ce78df5e1456bd85 GIT binary patch literal 232 zcmVP)z*yZ?bhY?VIE=NYmcAAT^~PQKe|$;bk39t~ z(J%jRT{)Fb7Mp(B-ul;Of9(7{A-^WBe&N2~@hE*P4*P$s=!*N($KUtV{9iC*UDngb zzv*J}O~3#9GXJQ){PTQM$^RMsPxZck{y-Ot7nD4oS^oM!(fnUs)!&-xf2}TGIa+d! iT0Cm;sKuieBLM(bBzWrxf}HRG0000P)z*yZ?bhY?VIE=NYmcAAT^~PQKe|$;bk39t~ z(J%jRT{)Fb7Mp(B-ul;Of9(7{A-^WBe&N2~@hE*P4*P$s=!*N($KUtV{9iC*UDngb zzv*J}O~3#9GXJQ){PTQM$^RMsPxZck{y-Ot7nD4oS^oM!(fnUs)!&-xf2}TGIa+d! iT0Cm;sKuieBLM(bBzWrxf}HRG0000 Date: Mon, 9 Jun 2025 09:48:55 -0400 Subject: [PATCH 144/172] Clean up some parser comments (#6398) --- crates/typst-syntax/src/parser.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index ecd0d78a5..a68815806 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -1571,10 +1571,10 @@ struct Token { prev_end: usize, } -/// Information about a newline if present (currently only relevant in Markup). +/// Information about newlines in a group of trivia. #[derive(Debug, Clone, Copy)] struct Newline { - /// The column of the start of our token in its line. + /// The column of the start of the next token in its line. column: Option, /// Whether any of our newlines were paragraph breaks. parbreak: bool, @@ -1587,7 +1587,7 @@ enum AtNewline { Continue, /// Stop at any newline. Stop, - /// Continue only if there is no continuation with `else` or `.` (Code only). + /// Continue only if there is a continuation with `else` or `.` (Code only). ContextualContinue, /// Stop only at a parbreak, not normal newlines (Markup only). StopParBreak, @@ -1610,9 +1610,10 @@ impl AtNewline { }, AtNewline::StopParBreak => parbreak, AtNewline::RequireColumn(min_col) => { - // Don't stop if this newline doesn't start a column (this may - // be checked on the boundary of lexer modes, since we only - // report a column in Markup). + // When the column is `None`, the newline doesn't start a + // column, and we continue parsing. This may happen on the + // boundary of lexer modes, since we only report a column in + // Markup. column.is_some_and(|column| column <= min_col) } } From df4c08f852ba3342e69caa721067804a7152e166 Mon Sep 17 00:00:00 2001 From: cAttte <26514199+cAttte@users.noreply.github.com> Date: Mon, 9 Jun 2025 11:16:47 -0300 Subject: [PATCH 145/172] Autocomplete fixes for math mode (#6415) --- crates/typst-ide/src/complete.rs | 16 +++++++++++++++- crates/typst-ide/src/utils.rs | 4 +++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index 4a36045ae..a042b1640 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -298,13 +298,20 @@ fn complete_math(ctx: &mut CompletionContext) -> bool { return false; } - // Start of an interpolated identifier: "#|". + // Start of an interpolated identifier: "$#|$". if ctx.leaf.kind() == SyntaxKind::Hash { ctx.from = ctx.cursor; code_completions(ctx, true); return true; } + // Behind existing interpolated identifier: "$#pa|$". + if ctx.leaf.kind() == SyntaxKind::Ident { + ctx.from = ctx.leaf.offset(); + code_completions(ctx, true); + return true; + } + // Behind existing atom or identifier: "$a|$" or "$abc|$". if matches!( ctx.leaf.kind(), @@ -1666,6 +1673,13 @@ mod tests { test("#{() .a}", -2).must_include(["at", "any", "all"]); } + /// Test that autocomplete in math uses the correct global scope. + #[test] + fn test_autocomplete_math_scope() { + test("$#col$", -2).must_include(["colbreak"]).must_exclude(["colon"]); + test("$col$", -2).must_include(["colon"]).must_exclude(["colbreak"]); + } + /// Test that the `before_window` doesn't slice into invalid byte /// boundaries. #[test] diff --git a/crates/typst-ide/src/utils.rs b/crates/typst-ide/src/utils.rs index 887e851f9..13de402ba 100644 --- a/crates/typst-ide/src/utils.rs +++ b/crates/typst-ide/src/utils.rs @@ -114,7 +114,9 @@ pub fn globals<'a>(world: &'a dyn IdeWorld, leaf: &LinkedNode) -> &'a Scope { | Some(SyntaxKind::Math) | Some(SyntaxKind::MathFrac) | Some(SyntaxKind::MathAttach) - ); + ) && leaf + .prev_leaf() + .is_none_or(|prev| !matches!(prev.kind(), SyntaxKind::Hash)); let library = world.library(); if in_math { From 2a3746c51de9231436013a2885a6d7096b0e4028 Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Mon, 9 Jun 2025 17:25:33 +0300 Subject: [PATCH 146/172] Update docs for gradient.repeat (#6385) --- crates/typst-library/src/visualize/gradient.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/typst-library/src/visualize/gradient.rs b/crates/typst-library/src/visualize/gradient.rs index 45f388ccd..5d7859a37 100644 --- a/crates/typst-library/src/visualize/gradient.rs +++ b/crates/typst-library/src/visualize/gradient.rs @@ -549,7 +549,7 @@ impl Gradient { } /// Repeats this gradient a given number of times, optionally mirroring it - /// at each repetition. + /// at every second repetition. /// /// ```example /// #circle( @@ -564,7 +564,17 @@ impl Gradient { &self, /// The number of times to repeat the gradient. repetitions: Spanned, - /// Whether to mirror the gradient at each repetition. + /// Whether to mirror the gradient at every second repetition, i.e., + /// the first instance (and all odd ones) stays unchanged. + /// + /// ```example + /// #circle( + /// radius: 40pt, + /// fill: gradient + /// .conic(green, black) + /// .repeat(2, mirror: true) + /// ) + /// ``` #[named] #[default(false)] mirror: bool, From e632bffc2ed4c005e5e989b527a05e87f077a8a0 Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Mon, 9 Jun 2025 19:34:39 +0300 Subject: [PATCH 147/172] Document how to escape lr delimiter auto-scaling (#6410) Co-authored-by: Laurenz --- docs/reference/groups.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/reference/groups.yml b/docs/reference/groups.yml index 8fea3a1f2..e5aa7e999 100644 --- a/docs/reference/groups.yml +++ b/docs/reference/groups.yml @@ -112,11 +112,18 @@ a few more functions that create delimiter pairings for absolute, ceiled, and floored values as well as norms. + To prevent a delimiter from being matched by Typst, and thus auto-scaled, + escape it with a backslash. To instead disable auto-scaling completely, use + `{set math.lr(size: 1em)}`. + # Example ```example $ [a, b/2] $ $ lr(]sum_(x=1)^n], size: #50%) x $ $ abs((x + y) / 2) $ + $ \{ (x / y) \} $ + #set math.lr(size: 1em) + $ { (a / b), a, b in (0; 1/2] } $ ``` - name: calc From 82da96ed957a68017e092e2606226b45c34324f1 Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski <133046678+wrzian@users.noreply.github.com> Date: Tue, 10 Jun 2025 05:11:27 -0400 Subject: [PATCH 148/172] Improve number lexing (#5969) --- crates/typst-syntax/src/lexer.rs | 154 ++++++++++++++++-------------- tests/ref/double-percent.png | Bin 496 -> 0 bytes tests/suite/foundations/float.typ | 8 +- tests/suite/layout/length.typ | 36 +++++-- tests/suite/layout/relative.typ | 7 +- 5 files changed, 118 insertions(+), 87 deletions(-) delete mode 100644 tests/ref/double-percent.png diff --git a/crates/typst-syntax/src/lexer.rs b/crates/typst-syntax/src/lexer.rs index ac69eb616..7d363d7b5 100644 --- a/crates/typst-syntax/src/lexer.rs +++ b/crates/typst-syntax/src/lexer.rs @@ -807,86 +807,96 @@ impl Lexer<'_> { } } - fn number(&mut self, mut start: usize, c: char) -> SyntaxKind { + fn number(&mut self, start: usize, first_c: char) -> SyntaxKind { // Handle alternative integer bases. - let mut base = 10; - if c == '0' { - if self.s.eat_if('b') { - base = 2; - } else if self.s.eat_if('o') { - base = 8; - } else if self.s.eat_if('x') { - base = 16; - } - if base != 10 { - start = self.s.cursor(); - } - } - - // Read the first part (integer or fractional depending on `first`). - self.s.eat_while(if base == 16 { - char::is_ascii_alphanumeric - } else { - char::is_ascii_digit - }); - - // Read the fractional part if not already done. - // Make sure not to confuse a range for the decimal separator. - if c != '.' - && !self.s.at("..") - && !self.s.scout(1).is_some_and(is_id_start) - && self.s.eat_if('.') - && base == 10 - { - self.s.eat_while(char::is_ascii_digit); - } - - // Read the exponent. - if !self.s.at("em") && self.s.eat_if(['e', 'E']) && base == 10 { - self.s.eat_if(['+', '-']); - self.s.eat_while(char::is_ascii_digit); - } - - // Read the suffix. - let suffix_start = self.s.cursor(); - if !self.s.eat_if('%') { - self.s.eat_while(char::is_ascii_alphanumeric); - } - - let number = self.s.get(start..suffix_start); - let suffix = self.s.from(suffix_start); - - let kind = if i64::from_str_radix(number, base).is_ok() { - SyntaxKind::Int - } else if base == 10 && number.parse::().is_ok() { - SyntaxKind::Float - } else { - return self.error(match base { - 2 => eco_format!("invalid binary number: 0b{}", number), - 8 => eco_format!("invalid octal number: 0o{}", number), - 16 => eco_format!("invalid hexadecimal number: 0x{}", number), - _ => eco_format!("invalid number: {}", number), - }); + let base = match first_c { + '0' if self.s.eat_if('b') => 2, + '0' if self.s.eat_if('o') => 8, + '0' if self.s.eat_if('x') => 16, + _ => 10, }; - if suffix.is_empty() { - return kind; + // Read the initial digits. + if base == 16 { + self.s.eat_while(char::is_ascii_alphanumeric); + } else { + self.s.eat_while(char::is_ascii_digit); } - if !matches!( - suffix, - "pt" | "mm" | "cm" | "in" | "deg" | "rad" | "em" | "fr" | "%" - ) { - return self.error(eco_format!("invalid number suffix: {}", suffix)); + // Read floating point digits and exponents. + let mut is_float = false; + if base == 10 { + // Read digits following a dot. Make sure not to confuse a spread + // operator or a method call for the decimal separator. + if first_c == '.' { + is_float = true; // We already ate the trailing digits above. + } else if !self.s.at("..") + && !self.s.scout(1).is_some_and(is_id_start) + && self.s.eat_if('.') + { + is_float = true; + self.s.eat_while(char::is_ascii_digit); + } + + // Read the exponent. + if !self.s.at("em") && self.s.eat_if(['e', 'E']) { + is_float = true; + self.s.eat_if(['+', '-']); + self.s.eat_while(char::is_ascii_digit); + } } - if base != 10 { - let kind = self.error(eco_format!("invalid base-{base} prefix")); - self.hint("numbers with a unit cannot have a base prefix"); - return kind; - } + let number = self.s.from(start); + let suffix = self.s.eat_while(|c: char| c.is_ascii_alphanumeric() || c == '%'); - SyntaxKind::Numeric + let mut suffix_result = match suffix { + "" => Ok(None), + "pt" | "mm" | "cm" | "in" | "deg" | "rad" | "em" | "fr" | "%" => Ok(Some(())), + _ => Err(eco_format!("invalid number suffix: {suffix}")), + }; + + let number_result = if is_float && number.parse::().is_err() { + // The only invalid case should be when a float lacks digits after + // the exponent: e.g. `1.2e`, `2.3E-`, or `1EM`. + Err(eco_format!("invalid floating point number: {number}")) + } else if base == 10 { + Ok(()) + } else { + let name = match base { + 2 => "binary", + 8 => "octal", + 16 => "hexadecimal", + _ => unreachable!(), + }; + // The index `[2..]` skips the leading `0b`/`0o`/`0x`. + match i64::from_str_radix(&number[2..], base) { + Ok(_) if suffix.is_empty() => Ok(()), + Ok(value) => { + if suffix_result.is_ok() { + suffix_result = Err(eco_format!( + "try using a decimal number: {value}{suffix}" + )); + } + Err(eco_format!("{name} numbers cannot have a suffix")) + } + Err(_) => Err(eco_format!("invalid {name} number: {number}")), + } + }; + + // Return our number or write an error with helpful hints. + match (number_result, suffix_result) { + // Valid numbers :D + (Ok(()), Ok(None)) if is_float => SyntaxKind::Float, + (Ok(()), Ok(None)) => SyntaxKind::Int, + (Ok(()), Ok(Some(()))) => SyntaxKind::Numeric, + // Invalid numbers :( + (Err(number_err), Err(suffix_err)) => { + let err = self.error(number_err); + self.hint(suffix_err); + err + } + (Ok(()), Err(msg)) | (Err(msg), Ok(_)) => self.error(msg), + } } fn string(&mut self) -> SyntaxKind { diff --git a/tests/ref/double-percent.png b/tests/ref/double-percent.png deleted file mode 100644 index 61a0d6143cd1615b0fa0051d0442b32be6fd2491..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 496 zcmV+=2f`SbJhrlzKag@yFj3(}_~!2H?CkmK@#wC} z)^MQTk+#!kn$&BZ+=Z^zaiQj?#p$ujwY9a5j*hIXtmfwC)^eiSeW~fQ&FJXp>gwu| zk&(y8$MMbHoSdBU^75mjqwT@g;g-6}%F6cO>gu-7`||eg?(V$2yr7_<{{H@khK6)> zbh5It#KgqGDRSzy&iUx@&|Q?$VwdpA+xFh+_xJby`~2XNx8aw%>a@-K@b&-x{>PGM zX#fBKrb$FWRCwC$(?t%$KoCUHa+sN!nVFdxY~TMVk>bQRm`IWOt-g9ws|F#2{4FP1O>0000 Date: Tue, 10 Jun 2025 14:46:27 +0200 Subject: [PATCH 149/172] Report errors in external files (#6308) Co-authored-by: Laurenz --- Cargo.lock | 3 + Cargo.toml | 1 + crates/typst-cli/src/compile.rs | 4 +- crates/typst-cli/src/timings.rs | 2 +- crates/typst-cli/src/world.rs | 23 +- crates/typst-layout/Cargo.toml | 1 + crates/typst-layout/src/image.rs | 14 +- crates/typst-library/Cargo.toml | 1 + crates/typst-library/src/diag.rs | 303 ++++++++++++- crates/typst-library/src/foundations/bytes.rs | 11 + .../typst-library/src/foundations/plugin.rs | 4 +- crates/typst-library/src/loading/cbor.rs | 4 +- crates/typst-library/src/loading/csv.rs | 38 +- crates/typst-library/src/loading/json.rs | 13 +- crates/typst-library/src/loading/mod.rs | 49 ++- crates/typst-library/src/loading/read.rs | 15 +- crates/typst-library/src/loading/toml.rs | 28 +- crates/typst-library/src/loading/xml.rs | 11 +- crates/typst-library/src/loading/yaml.rs | 23 +- .../typst-library/src/model/bibliography.rs | 119 +++--- crates/typst-library/src/text/raw.rs | 81 ++-- .../typst-library/src/visualize/image/mod.rs | 20 +- .../typst-library/src/visualize/image/svg.rs | 26 +- crates/typst-syntax/Cargo.toml | 1 + crates/typst-syntax/src/lib.rs | 2 + crates/typst-syntax/src/lines.rs | 402 ++++++++++++++++++ crates/typst-syntax/src/source.rs | 326 +------------- tests/src/collect.rs | 98 ++++- tests/src/run.rs | 74 ++-- tests/src/world.rs | 21 +- tests/suite/loading/csv.typ | 4 +- tests/suite/loading/json.typ | 2 +- tests/suite/loading/read.typ | 2 +- tests/suite/loading/toml.typ | 2 +- tests/suite/loading/xml.typ | 2 +- tests/suite/loading/yaml.typ | 2 +- tests/suite/scripting/import.typ | 1 + tests/suite/visualize/image.typ | 4 +- 38 files changed, 1165 insertions(+), 572 deletions(-) create mode 100644 crates/typst-syntax/src/lines.rs diff --git a/Cargo.lock b/Cargo.lock index a9b3756a6..b699d2450 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3039,6 +3039,7 @@ dependencies = [ "icu_provider_blob", "icu_segmenter", "kurbo", + "memchr", "rustybuzz", "smallvec", "ttf-parser", @@ -3112,6 +3113,7 @@ dependencies = [ "unicode-segmentation", "unscanny", "usvg", + "utf8_iter", "wasmi", "xmlwriter", ] @@ -3200,6 +3202,7 @@ dependencies = [ name = "typst-syntax" version = "0.13.1" dependencies = [ + "comemo", "ecow", "serde", "toml", diff --git a/Cargo.toml b/Cargo.toml index b4890e3c1..b548245fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -135,6 +135,7 @@ unicode-segmentation = "1" unscanny = "0.1" ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] } usvg = { version = "0.45", default-features = false, features = ["text"] } +utf8_iter = "1.0.4" walkdir = "2" wasmi = "0.40.0" web-sys = "0.3" diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index 4edb4c323..207bb7d09 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -16,7 +16,7 @@ use typst::diag::{ use typst::foundations::{Datetime, Smart}; use typst::html::HtmlDocument; use typst::layout::{Frame, Page, PageRanges, PagedDocument}; -use typst::syntax::{FileId, Source, Span}; +use typst::syntax::{FileId, Lines, Span}; use typst::WorldExt; use typst_pdf::{PdfOptions, PdfStandards, Timestamp}; @@ -696,7 +696,7 @@ fn label(world: &SystemWorld, span: Span) -> Option> { impl<'a> codespan_reporting::files::Files<'a> for SystemWorld { type FileId = FileId; type Name = String; - type Source = Source; + type Source = Lines; fn name(&'a self, id: FileId) -> CodespanResult { let vpath = id.vpath(); diff --git a/crates/typst-cli/src/timings.rs b/crates/typst-cli/src/timings.rs index 9f017dc12..3d10bbc67 100644 --- a/crates/typst-cli/src/timings.rs +++ b/crates/typst-cli/src/timings.rs @@ -85,6 +85,6 @@ fn resolve_span(world: &SystemWorld, span: Span) -> Option<(String, u32)> { let id = span.id()?; let source = world.source(id).ok()?; let range = source.range(span)?; - let line = source.byte_to_line(range.start)?; + let line = source.lines().byte_to_line(range.start)?; Some((format!("{id:?}"), line as u32 + 1)) } diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs index 2da03d4d5..f63d34b63 100644 --- a/crates/typst-cli/src/world.rs +++ b/crates/typst-cli/src/world.rs @@ -9,7 +9,7 @@ use ecow::{eco_format, EcoString}; use parking_lot::Mutex; use typst::diag::{FileError, FileResult}; use typst::foundations::{Bytes, Datetime, Dict, IntoValue}; -use typst::syntax::{FileId, Source, VirtualPath}; +use typst::syntax::{FileId, Lines, Source, VirtualPath}; use typst::text::{Font, FontBook}; use typst::utils::LazyHash; use typst::{Library, World}; @@ -181,10 +181,20 @@ impl SystemWorld { } } - /// Lookup a source file by id. + /// Lookup line metadata for a file by id. #[track_caller] - pub fn lookup(&self, id: FileId) -> Source { - self.source(id).expect("file id does not point to any source file") + pub fn lookup(&self, id: FileId) -> Lines { + self.slot(id, |slot| { + if let Some(source) = slot.source.get() { + let source = source.as_ref().expect("file is not valid"); + source.lines() + } else if let Some(bytes) = slot.file.get() { + let bytes = bytes.as_ref().expect("file is not valid"); + Lines::try_from(bytes).expect("file is not valid utf-8") + } else { + panic!("file id does not point to any source file"); + } + }) } } @@ -339,6 +349,11 @@ impl SlotCell { self.accessed = false; } + /// Gets the contents of the cell. + fn get(&self) -> Option<&FileResult> { + self.data.as_ref() + } + /// Gets the contents of the cell or initialize them. fn get_or_init( &mut self, diff --git a/crates/typst-layout/Cargo.toml b/crates/typst-layout/Cargo.toml index 438e09e43..cc355a3db 100644 --- a/crates/typst-layout/Cargo.toml +++ b/crates/typst-layout/Cargo.toml @@ -30,6 +30,7 @@ icu_provider_adapters = { workspace = true } icu_provider_blob = { workspace = true } icu_segmenter = { workspace = true } kurbo = { workspace = true } +memchr = { workspace = true } rustybuzz = { workspace = true } smallvec = { workspace = true } ttf-parser = { workspace = true } diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index 8136a25a3..a8f4a0c81 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -1,6 +1,6 @@ use std::ffi::OsStr; -use typst_library::diag::{warning, At, SourceResult, StrResult}; +use typst_library::diag::{warning, At, LoadedWithin, SourceResult, StrResult}; use typst_library::engine::Engine; use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain}; use typst_library::introspection::Locator; @@ -27,17 +27,17 @@ pub fn layout_image( // Take the format that was explicitly defined, or parse the extension, // or try to detect the format. - let Derived { source, derived: data } = &elem.source; + let Derived { source, derived: loaded } = &elem.source; let format = match elem.format(styles) { Smart::Custom(v) => v, - Smart::Auto => determine_format(source, data).at(span)?, + Smart::Auto => determine_format(source, &loaded.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_ok_and(|s| s.contains(" ImageKind::Raster( RasterImage::new( - data.clone(), + loaded.data.clone(), format, elem.icc(styles).as_ref().map(|icc| icc.derived.clone()), ) @@ -61,11 +61,11 @@ pub fn layout_image( ), ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg( SvgImage::with_fonts( - data.clone(), + loaded.data.clone(), engine.world, &families(styles).map(|f| f.as_str()).collect::>(), ) - .at(span)?, + .within(loaded)?, ), }; diff --git a/crates/typst-library/Cargo.toml b/crates/typst-library/Cargo.toml index b210637a8..f4b219882 100644 --- a/crates/typst-library/Cargo.toml +++ b/crates/typst-library/Cargo.toml @@ -66,6 +66,7 @@ unicode-normalization = { workspace = true } unicode-segmentation = { workspace = true } unscanny = { workspace = true } usvg = { workspace = true } +utf8_iter = { workspace = true } wasmi = { workspace = true } xmlwriter = { workspace = true } diff --git a/crates/typst-library/src/diag.rs b/crates/typst-library/src/diag.rs index 49cbd02c6..41b92ed65 100644 --- a/crates/typst-library/src/diag.rs +++ b/crates/typst-library/src/diag.rs @@ -1,17 +1,20 @@ //! Diagnostics. -use std::fmt::{self, Display, Formatter}; +use std::fmt::{self, Display, Formatter, Write as _}; use std::io; use std::path::{Path, PathBuf}; use std::str::Utf8Error; use std::string::FromUtf8Error; +use az::SaturatingAs; use comemo::Tracked; use ecow::{eco_vec, EcoVec}; use typst_syntax::package::{PackageSpec, PackageVersion}; -use typst_syntax::{Span, Spanned, SyntaxError}; +use typst_syntax::{Lines, Span, Spanned, SyntaxError}; +use utf8_iter::ErrorReportingUtf8Chars; use crate::engine::Engine; +use crate::loading::{LoadSource, Loaded}; use crate::{World, WorldExt}; /// Early-return with a [`StrResult`] or [`SourceResult`]. @@ -148,7 +151,7 @@ pub struct Warned { pub warnings: EcoVec, } -/// An error or warning in a source file. +/// An error or warning in a source or text file. /// /// The contained spans will only be detached if any of the input source files /// were detached. @@ -568,31 +571,287 @@ impl From for EcoString { } } +/// A result type with a data-loading-related error. +pub type LoadResult = Result; + +/// A call site independent error that occurred during data loading. This avoids +/// polluting the memoization with [`Span`]s and [`FileId`]s from source files. +/// Can be turned into a [`SourceDiagnostic`] using the [`LoadedWithin::within`] +/// method available on [`LoadResult`]. +/// +/// [`FileId`]: typst_syntax::FileId +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct LoadError { + /// The position in the file at which the error occured. + pos: ReportPos, + /// Must contain a message formatted like this: `"failed to do thing (cause)"`. + message: EcoString, +} + +impl LoadError { + /// Creates a new error from a position in a file, a base message + /// (e.g. `failed to parse JSON`) and a concrete error (e.g. `invalid + /// number`) + pub fn new( + pos: impl Into, + message: impl std::fmt::Display, + error: impl std::fmt::Display, + ) -> Self { + Self { + pos: pos.into(), + message: eco_format!("{message} ({error})"), + } + } +} + +impl From for LoadError { + fn from(err: Utf8Error) -> Self { + let start = err.valid_up_to(); + let end = start + err.error_len().unwrap_or(0); + LoadError::new( + start..end, + "failed to convert to string", + "file is not valid utf-8", + ) + } +} + +/// Convert a [`LoadResult`] to a [`SourceResult`] by adding the [`Loaded`] +/// context. +pub trait LoadedWithin { + /// Report an error, possibly in an external file. + fn within(self, loaded: &Loaded) -> SourceResult; +} + +impl LoadedWithin for Result +where + E: Into, +{ + fn within(self, loaded: &Loaded) -> SourceResult { + self.map_err(|err| { + let LoadError { pos, message } = err.into(); + load_err_in_text(loaded, pos, message) + }) + } +} + +/// Report an error, possibly in an external file. This will delegate to +/// [`load_err_in_invalid_text`] if the data isn't valid utf-8. +fn load_err_in_text( + loaded: &Loaded, + pos: impl Into, + mut message: EcoString, +) -> EcoVec { + let pos = pos.into(); + // This also does utf-8 validation. Only report an error in an external + // file if it is human readable (valid utf-8), otherwise fall back to + // `load_err_in_invalid_text`. + let lines = Lines::try_from(&loaded.data); + match (loaded.source.v, lines) { + (LoadSource::Path(file_id), Ok(lines)) => { + if let Some(range) = pos.range(&lines) { + let span = Span::from_range(file_id, range); + return eco_vec![SourceDiagnostic::error(span, message)]; + } + + // Either `ReportPos::None` was provided, or resolving the range + // from the line/column failed. If present report the possibly + // wrong line/column in the error message anyway. + let span = Span::from_range(file_id, 0..loaded.data.len()); + if let Some(pair) = pos.line_col(&lines) { + message.pop(); + let (line, col) = pair.numbers(); + write!(&mut message, " at {line}:{col})").ok(); + } + eco_vec![SourceDiagnostic::error(span, message)] + } + (LoadSource::Bytes, Ok(lines)) => { + if let Some(pair) = pos.line_col(&lines) { + message.pop(); + let (line, col) = pair.numbers(); + write!(&mut message, " at {line}:{col})").ok(); + } + eco_vec![SourceDiagnostic::error(loaded.source.span, message)] + } + _ => load_err_in_invalid_text(loaded, pos, message), + } +} + +/// Report an error (possibly from an external file) that isn't valid utf-8. +fn load_err_in_invalid_text( + loaded: &Loaded, + pos: impl Into, + mut message: EcoString, +) -> EcoVec { + let line_col = pos.into().try_line_col(&loaded.data).map(|p| p.numbers()); + match (loaded.source.v, line_col) { + (LoadSource::Path(file), _) => { + message.pop(); + if let Some(package) = file.package() { + write!( + &mut message, + " in {package}{}", + file.vpath().as_rooted_path().display() + ) + .ok(); + } else { + write!(&mut message, " in {}", file.vpath().as_rootless_path().display()) + .ok(); + }; + if let Some((line, col)) = line_col { + write!(&mut message, ":{line}:{col}").ok(); + } + message.push(')'); + } + (LoadSource::Bytes, Some((line, col))) => { + message.pop(); + write!(&mut message, " at {line}:{col})").ok(); + } + (LoadSource::Bytes, None) => (), + } + eco_vec![SourceDiagnostic::error(loaded.source.span, message)] +} + +/// A position at which an error was reported. +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +pub enum ReportPos { + /// Contains a range, and a line/column pair. + Full(std::ops::Range, LineCol), + /// Contains a range. + Range(std::ops::Range), + /// Contains a line/column pair. + LineCol(LineCol), + #[default] + None, +} + +impl From> for ReportPos { + fn from(value: std::ops::Range) -> Self { + Self::Range(value.start.saturating_as()..value.end.saturating_as()) + } +} + +impl From for ReportPos { + fn from(value: LineCol) -> Self { + Self::LineCol(value) + } +} + +impl ReportPos { + /// Creates a position from a pre-existing range and line-column pair. + pub fn full(range: std::ops::Range, pair: LineCol) -> Self { + let range = range.start.saturating_as()..range.end.saturating_as(); + Self::Full(range, pair) + } + + /// Tries to determine the byte range for this position. + fn range(&self, lines: &Lines) -> Option> { + match self { + ReportPos::Full(range, _) => Some(range.start as usize..range.end as usize), + ReportPos::Range(range) => Some(range.start as usize..range.end as usize), + &ReportPos::LineCol(pair) => { + let i = + lines.line_column_to_byte(pair.line as usize, pair.col as usize)?; + Some(i..i) + } + ReportPos::None => None, + } + } + + /// Tries to determine the line/column for this position. + fn line_col(&self, lines: &Lines) -> Option { + match self { + &ReportPos::Full(_, pair) => Some(pair), + ReportPos::Range(range) => { + let (line, col) = lines.byte_to_line_column(range.start as usize)?; + Some(LineCol::zero_based(line, col)) + } + &ReportPos::LineCol(pair) => Some(pair), + ReportPos::None => None, + } + } + + /// Either gets the line/column pair, or tries to compute it from possibly + /// invalid utf-8 data. + fn try_line_col(&self, bytes: &[u8]) -> Option { + match self { + &ReportPos::Full(_, pair) => Some(pair), + ReportPos::Range(range) => { + LineCol::try_from_byte_pos(range.start as usize, bytes) + } + &ReportPos::LineCol(pair) => Some(pair), + ReportPos::None => None, + } + } +} + +/// A line/column pair. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct LineCol { + /// The 0-based line. + line: u32, + /// The 0-based column. + col: u32, +} + +impl LineCol { + /// Constructs the line/column pair from 0-based indices. + pub fn zero_based(line: usize, col: usize) -> Self { + Self { + line: line.saturating_as(), + col: col.saturating_as(), + } + } + + /// Constructs the line/column pair from 1-based numbers. + pub fn one_based(line: usize, col: usize) -> Self { + Self::zero_based(line.saturating_sub(1), col.saturating_sub(1)) + } + + /// Try to compute a line/column pair from possibly invalid utf-8 data. + pub fn try_from_byte_pos(pos: usize, bytes: &[u8]) -> Option { + let bytes = &bytes[..pos]; + let mut line = 0; + #[allow(clippy::double_ended_iterator_last)] + let line_start = memchr::memchr_iter(b'\n', bytes) + .inspect(|_| line += 1) + .last() + .map(|i| i + 1) + .unwrap_or(bytes.len()); + + let col = ErrorReportingUtf8Chars::new(&bytes[line_start..]).count(); + Some(LineCol::zero_based(line, col)) + } + + /// Returns the 0-based line/column indices. + pub fn indices(&self) -> (usize, usize) { + (self.line as usize, self.col as usize) + } + + /// Returns the 1-based line/column numbers. + pub fn numbers(&self) -> (usize, usize) { + (self.line as usize + 1, self.col as usize + 1) + } +} + /// Format a user-facing error message for an XML-like file format. -pub fn format_xml_like_error(format: &str, error: roxmltree::Error) -> EcoString { - match error { - roxmltree::Error::UnexpectedCloseTag(expected, actual, pos) => { - eco_format!( - "failed to parse {format} (found closing tag '{actual}' \ - instead of '{expected}' in line {})", - pos.row - ) +pub fn format_xml_like_error(format: &str, error: roxmltree::Error) -> LoadError { + let pos = LineCol::one_based(error.pos().row as usize, error.pos().col as usize); + let message = match error { + roxmltree::Error::UnexpectedCloseTag(expected, actual, _) => { + eco_format!("failed to parse {format} (found closing tag '{actual}' instead of '{expected}')") } - roxmltree::Error::UnknownEntityReference(entity, pos) => { - eco_format!( - "failed to parse {format} (unknown entity '{entity}' in line {})", - pos.row - ) + roxmltree::Error::UnknownEntityReference(entity, _) => { + eco_format!("failed to parse {format} (unknown entity '{entity}')") } - roxmltree::Error::DuplicatedAttribute(attr, pos) => { - eco_format!( - "failed to parse {format} (duplicate attribute '{attr}' in line {})", - pos.row - ) + roxmltree::Error::DuplicatedAttribute(attr, _) => { + eco_format!("failed to parse {format} (duplicate attribute '{attr}')") } roxmltree::Error::NoRootNode => { eco_format!("failed to parse {format} (missing root node)") } err => eco_format!("failed to parse {format} ({err})"), - } + }; + + LoadError { pos: pos.into(), message } } diff --git a/crates/typst-library/src/foundations/bytes.rs b/crates/typst-library/src/foundations/bytes.rs index d633c99ad..180dcdad5 100644 --- a/crates/typst-library/src/foundations/bytes.rs +++ b/crates/typst-library/src/foundations/bytes.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use ecow::{eco_format, EcoString}; use serde::{Serialize, Serializer}; +use typst_syntax::Lines; use typst_utils::LazyHash; use crate::diag::{bail, StrResult}; @@ -286,6 +287,16 @@ impl Serialize for Bytes { } } +impl TryFrom<&Bytes> for Lines { + type Error = Utf8Error; + + #[comemo::memoize] + fn try_from(value: &Bytes) -> Result, Utf8Error> { + let text = value.as_str()?; + Ok(Lines::new(text.to_string())) + } +} + /// Any type that can back a byte buffer. trait Bytelike: Send + Sync { fn as_bytes(&self) -> &[u8]; diff --git a/crates/typst-library/src/foundations/plugin.rs b/crates/typst-library/src/foundations/plugin.rs index 31f8cd732..a04443bf4 100644 --- a/crates/typst-library/src/foundations/plugin.rs +++ b/crates/typst-library/src/foundations/plugin.rs @@ -151,8 +151,8 @@ pub fn plugin( /// A [path]($syntax/#paths) to a WebAssembly file or raw WebAssembly bytes. source: Spanned, ) -> SourceResult { - let data = source.load(engine.world)?; - Plugin::module(data).at(source.span) + let loaded = source.load(engine.world)?; + Plugin::module(loaded.data).at(source.span) } #[scope] diff --git a/crates/typst-library/src/loading/cbor.rs b/crates/typst-library/src/loading/cbor.rs index aa14c5c77..d95f73844 100644 --- a/crates/typst-library/src/loading/cbor.rs +++ b/crates/typst-library/src/loading/cbor.rs @@ -23,8 +23,8 @@ pub fn cbor( /// A [path]($syntax/#paths) to a CBOR file or raw CBOR bytes. source: Spanned, ) -> SourceResult { - let data = source.load(engine.world)?; - ciborium::from_reader(data.as_slice()) + let loaded = source.load(engine.world)?; + ciborium::from_reader(loaded.data.as_slice()) .map_err(|err| eco_format!("failed to parse CBOR ({err})")) .at(source.span) } diff --git a/crates/typst-library/src/loading/csv.rs b/crates/typst-library/src/loading/csv.rs index 6afb5baeb..d5b54a06c 100644 --- a/crates/typst-library/src/loading/csv.rs +++ b/crates/typst-library/src/loading/csv.rs @@ -1,7 +1,7 @@ -use ecow::{eco_format, EcoString}; +use az::SaturatingAs; use typst_syntax::Spanned; -use crate::diag::{bail, At, SourceResult}; +use crate::diag::{bail, LineCol, LoadError, LoadedWithin, ReportPos, SourceResult}; use crate::engine::Engine; use crate::foundations::{cast, func, scope, Array, Dict, IntoValue, Type, Value}; use crate::loading::{DataSource, Load, Readable}; @@ -44,7 +44,7 @@ pub fn csv( #[default(RowType::Array)] row_type: RowType, ) -> SourceResult { - let data = source.load(engine.world)?; + let loaded = source.load(engine.world)?; let mut builder = ::csv::ReaderBuilder::new(); let has_headers = row_type == RowType::Dict; @@ -53,7 +53,7 @@ pub fn csv( // Counting lines from 1 by default. let mut line_offset: usize = 1; - let mut reader = builder.from_reader(data.as_slice()); + let mut reader = builder.from_reader(loaded.data.as_slice()); let mut headers: Option<::csv::StringRecord> = None; if has_headers { @@ -62,9 +62,9 @@ pub fn csv( headers = Some( reader .headers() + .cloned() .map_err(|err| format_csv_error(err, 1)) - .at(source.span)? - .clone(), + .within(&loaded)?, ); } @@ -74,7 +74,7 @@ pub fn csv( // incorrect with `has_headers` set to `false`. See issue: // https://github.com/BurntSushi/rust-csv/issues/184 let line = line + line_offset; - let row = result.map_err(|err| format_csv_error(err, line)).at(source.span)?; + let row = result.map_err(|err| format_csv_error(err, line)).within(&loaded)?; let item = if let Some(headers) = &headers { let mut dict = Dict::new(); for (field, value) in headers.iter().zip(&row) { @@ -164,15 +164,23 @@ cast! { } /// Format the user-facing CSV error message. -fn format_csv_error(err: ::csv::Error, line: usize) -> EcoString { +fn format_csv_error(err: ::csv::Error, line: usize) -> LoadError { + let msg = "failed to parse CSV"; + let pos = (err.kind().position()) + .map(|pos| { + let start = pos.byte().saturating_as(); + ReportPos::from(start..start) + }) + .unwrap_or(LineCol::one_based(line, 1).into()); match err.kind() { - ::csv::ErrorKind::Utf8 { .. } => "file is not valid utf-8".into(), - ::csv::ErrorKind::UnequalLengths { expected_len, len, .. } => { - eco_format!( - "failed to parse CSV (found {len} instead of \ - {expected_len} fields in line {line})" - ) + ::csv::ErrorKind::Utf8 { .. } => { + LoadError::new(pos, msg, "file is not valid utf-8") } - _ => eco_format!("failed to parse CSV ({err})"), + ::csv::ErrorKind::UnequalLengths { expected_len, len, .. } => { + let err = + format!("found {len} instead of {expected_len} fields in line {line}"); + LoadError::new(pos, msg, err) + } + _ => LoadError::new(pos, "failed to parse CSV", err), } } diff --git a/crates/typst-library/src/loading/json.rs b/crates/typst-library/src/loading/json.rs index aa908cca4..7d0732ba0 100644 --- a/crates/typst-library/src/loading/json.rs +++ b/crates/typst-library/src/loading/json.rs @@ -1,7 +1,7 @@ use ecow::eco_format; use typst_syntax::Spanned; -use crate::diag::{At, SourceResult}; +use crate::diag::{At, LineCol, LoadError, LoadedWithin, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Str, Value}; use crate::loading::{DataSource, Load, Readable}; @@ -54,10 +54,13 @@ pub fn json( /// A [path]($syntax/#paths) to a JSON file or raw JSON bytes. source: Spanned, ) -> SourceResult { - let data = source.load(engine.world)?; - serde_json::from_slice(data.as_slice()) - .map_err(|err| eco_format!("failed to parse JSON ({err})")) - .at(source.span) + let loaded = source.load(engine.world)?; + serde_json::from_slice(loaded.data.as_slice()) + .map_err(|err| { + let pos = LineCol::one_based(err.line(), err.column()); + LoadError::new(pos, "failed to parse JSON", err) + }) + .within(&loaded) } #[scope] diff --git a/crates/typst-library/src/loading/mod.rs b/crates/typst-library/src/loading/mod.rs index c57e02888..67f4be834 100644 --- a/crates/typst-library/src/loading/mod.rs +++ b/crates/typst-library/src/loading/mod.rs @@ -17,7 +17,7 @@ mod yaml_; use comemo::Tracked; use ecow::EcoString; -use typst_syntax::Spanned; +use typst_syntax::{FileId, Spanned}; pub use self::cbor_::*; pub use self::csv_::*; @@ -74,39 +74,44 @@ pub trait Load { } impl Load for Spanned { - type Output = Bytes; + type Output = Loaded; - fn load(&self, world: Tracked) -> SourceResult { + fn load(&self, world: Tracked) -> SourceResult { self.as_ref().load(world) } } impl Load for Spanned<&DataSource> { - type Output = Bytes; + type Output = Loaded; - fn load(&self, world: Tracked) -> SourceResult { + fn load(&self, world: Tracked) -> SourceResult { match &self.v { DataSource::Path(path) => { let file_id = self.span.resolve_path(path).at(self.span)?; - world.file(file_id).at(self.span) + let data = world.file(file_id).at(self.span)?; + let source = Spanned::new(LoadSource::Path(file_id), self.span); + Ok(Loaded::new(source, data)) + } + DataSource::Bytes(data) => { + let source = Spanned::new(LoadSource::Bytes, self.span); + Ok(Loaded::new(source, data.clone())) } - DataSource::Bytes(bytes) => Ok(bytes.clone()), } } } impl Load for Spanned> { - type Output = Vec; + type Output = Vec; - fn load(&self, world: Tracked) -> SourceResult> { + fn load(&self, world: Tracked) -> SourceResult { self.as_ref().load(world) } } impl Load for Spanned<&OneOrMultiple> { - type Output = Vec; + type Output = Vec; - fn load(&self, world: Tracked) -> SourceResult> { + fn load(&self, world: Tracked) -> SourceResult { self.v .0 .iter() @@ -115,6 +120,28 @@ impl Load for Spanned<&OneOrMultiple> { } } +/// Data loaded from a [`DataSource`]. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Loaded { + /// Details about where `data` was loaded from. + pub source: Spanned, + /// The loaded data. + pub data: Bytes, +} + +impl Loaded { + pub fn new(source: Spanned, bytes: Bytes) -> Self { + Self { source, data: bytes } + } +} + +/// A loaded [`DataSource`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum LoadSource { + Path(FileId), + Bytes, +} + /// A value that can be read from a file. #[derive(Debug, Clone, PartialEq, Hash)] pub enum Readable { diff --git a/crates/typst-library/src/loading/read.rs b/crates/typst-library/src/loading/read.rs index 32dadc799..91e6e4366 100644 --- a/crates/typst-library/src/loading/read.rs +++ b/crates/typst-library/src/loading/read.rs @@ -1,11 +1,10 @@ use ecow::EcoString; use typst_syntax::Spanned; -use crate::diag::{At, FileError, SourceResult}; +use crate::diag::{LoadedWithin, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, Cast}; -use crate::loading::Readable; -use crate::World; +use crate::loading::{DataSource, Load, Readable}; /// Reads plain text or data from a file. /// @@ -36,14 +35,10 @@ pub fn read( #[default(Some(Encoding::Utf8))] encoding: Option, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; + let loaded = path.map(DataSource::Path).load(engine.world)?; Ok(match encoding { - None => Readable::Bytes(data), - Some(Encoding::Utf8) => { - Readable::Str(data.to_str().map_err(FileError::from).at(span)?) - } + None => Readable::Bytes(loaded.data), + Some(Encoding::Utf8) => Readable::Str(loaded.data.to_str().within(&loaded)?), }) } diff --git a/crates/typst-library/src/loading/toml.rs b/crates/typst-library/src/loading/toml.rs index f04b2e746..a4252feca 100644 --- a/crates/typst-library/src/loading/toml.rs +++ b/crates/typst-library/src/loading/toml.rs @@ -1,7 +1,7 @@ -use ecow::{eco_format, EcoString}; -use typst_syntax::{is_newline, Spanned}; +use ecow::eco_format; +use typst_syntax::Spanned; -use crate::diag::{At, FileError, SourceResult}; +use crate::diag::{At, LoadError, LoadedWithin, ReportPos, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Str, Value}; use crate::loading::{DataSource, Load, Readable}; @@ -32,11 +32,9 @@ pub fn toml( /// A [path]($syntax/#paths) to a TOML file or raw TOML bytes. source: Spanned, ) -> SourceResult { - let data = source.load(engine.world)?; - let raw = data.as_str().map_err(FileError::from).at(source.span)?; - ::toml::from_str(raw) - .map_err(|err| format_toml_error(err, raw)) - .at(source.span) + let loaded = source.load(engine.world)?; + let raw = loaded.data.as_str().within(&loaded)?; + ::toml::from_str(raw).map_err(format_toml_error).within(&loaded) } #[scope] @@ -71,15 +69,7 @@ impl toml { } /// Format the user-facing TOML error message. -fn format_toml_error(error: ::toml::de::Error, raw: &str) -> EcoString { - if let Some(head) = error.span().and_then(|range| raw.get(..range.start)) { - let line = head.lines().count(); - let column = 1 + head.chars().rev().take_while(|&c| !is_newline(c)).count(); - eco_format!( - "failed to parse TOML ({} at line {line} column {column})", - error.message(), - ) - } else { - eco_format!("failed to parse TOML ({})", error.message()) - } +fn format_toml_error(error: ::toml::de::Error) -> LoadError { + let pos = error.span().map(ReportPos::from).unwrap_or_default(); + LoadError::new(pos, "failed to parse TOML", error.message()) } diff --git a/crates/typst-library/src/loading/xml.rs b/crates/typst-library/src/loading/xml.rs index e76c4e9cf..0023c5df5 100644 --- a/crates/typst-library/src/loading/xml.rs +++ b/crates/typst-library/src/loading/xml.rs @@ -1,8 +1,7 @@ -use ecow::EcoString; use roxmltree::ParsingOptions; use typst_syntax::Spanned; -use crate::diag::{format_xml_like_error, At, FileError, SourceResult}; +use crate::diag::{format_xml_like_error, LoadError, LoadedWithin, SourceResult}; use crate::engine::Engine; use crate::foundations::{dict, func, scope, Array, Dict, IntoValue, Str, Value}; use crate::loading::{DataSource, Load, Readable}; @@ -61,14 +60,14 @@ pub fn xml( /// A [path]($syntax/#paths) to an XML file or raw XML bytes. source: Spanned, ) -> SourceResult { - let data = source.load(engine.world)?; - let text = data.as_str().map_err(FileError::from).at(source.span)?; + let loaded = source.load(engine.world)?; + let text = loaded.data.as_str().within(&loaded)?; let document = roxmltree::Document::parse_with_options( text, ParsingOptions { allow_dtd: true, ..Default::default() }, ) .map_err(format_xml_error) - .at(source.span)?; + .within(&loaded)?; Ok(convert_xml(document.root())) } @@ -111,6 +110,6 @@ fn convert_xml(node: roxmltree::Node) -> Value { } /// Format the user-facing XML error message. -fn format_xml_error(error: roxmltree::Error) -> EcoString { +fn format_xml_error(error: roxmltree::Error) -> LoadError { format_xml_like_error("XML", error) } diff --git a/crates/typst-library/src/loading/yaml.rs b/crates/typst-library/src/loading/yaml.rs index 3f48113e8..0edf1f901 100644 --- a/crates/typst-library/src/loading/yaml.rs +++ b/crates/typst-library/src/loading/yaml.rs @@ -1,7 +1,7 @@ use ecow::eco_format; use typst_syntax::Spanned; -use crate::diag::{At, SourceResult}; +use crate::diag::{At, LineCol, LoadError, LoadedWithin, ReportPos, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Str, Value}; use crate::loading::{DataSource, Load, Readable}; @@ -44,10 +44,10 @@ pub fn yaml( /// A [path]($syntax/#paths) to a YAML file or raw YAML bytes. source: Spanned, ) -> SourceResult { - let data = source.load(engine.world)?; - serde_yaml::from_slice(data.as_slice()) - .map_err(|err| eco_format!("failed to parse YAML ({err})")) - .at(source.span) + let loaded = source.load(engine.world)?; + serde_yaml::from_slice(loaded.data.as_slice()) + .map_err(format_yaml_error) + .within(&loaded) } #[scope] @@ -76,3 +76,16 @@ impl yaml { .at(span) } } + +/// Format the user-facing YAML error message. +pub fn format_yaml_error(error: serde_yaml::Error) -> LoadError { + let pos = error + .location() + .map(|loc| { + let line_col = LineCol::one_based(loc.line(), loc.column()); + let range = loc.index()..loc.index(); + ReportPos::full(range, line_col) + }) + .unwrap_or_default(); + LoadError::new(pos, "failed to parse YAML", error) +} diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index 51e3b03b0..114356575 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -19,7 +19,10 @@ use smallvec::{smallvec, SmallVec}; use typst_syntax::{Span, Spanned}; use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr}; -use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult}; +use crate::diag::{ + bail, error, At, HintedStrResult, LoadError, LoadResult, LoadedWithin, ReportPos, + SourceResult, StrResult, +}; use crate::engine::{Engine, Sink}; use crate::foundations::{ elem, Bytes, CastInfo, Content, Derived, FromValue, IntoValue, Label, NativeElement, @@ -31,7 +34,7 @@ use crate::layout::{ BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem, Sides, Sizing, TrackSizings, }; -use crate::loading::{DataSource, Load}; +use crate::loading::{format_yaml_error, DataSource, Load, LoadSource, Loaded}; use crate::model::{ CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem, Url, @@ -294,24 +297,21 @@ impl Bibliography { world: Tracked, sources: Spanned>, ) -> SourceResult, Self>> { - let data = sources.load(world)?; - let bibliography = Self::decode(&sources.v, &data).at(sources.span)?; + let loaded = sources.load(world)?; + let bibliography = Self::decode(&loaded)?; Ok(Derived::new(sources.v, bibliography)) } /// Decode a bibliography from loaded data sources. #[comemo::memoize] #[typst_macros::time(name = "load bibliography")] - fn decode( - sources: &OneOrMultiple, - data: &[Bytes], - ) -> StrResult { + fn decode(data: &[Loaded]) -> SourceResult { let mut map = IndexMap::new(); let mut duplicates = Vec::::new(); // We might have multiple bib/yaml files - for (source, data) in sources.0.iter().zip(data) { - let library = decode_library(source, data)?; + for d in data.iter() { + let library = decode_library(d)?; for entry in library { match map.entry(Label::new(PicoStr::intern(entry.key()))) { indexmap::map::Entry::Vacant(vacant) => { @@ -325,7 +325,11 @@ impl Bibliography { } if !duplicates.is_empty() { - bail!("duplicate bibliography keys: {}", duplicates.join(", ")); + // TODO: Store spans of entries for duplicate key error messages. + // Requires hayagriva entries to store their location, which should + // be fine, since they are 1kb anyway. + let span = data.first().unwrap().source.span; + bail!(span, "duplicate bibliography keys: {}", duplicates.join(", ")); } Ok(Bibliography(Arc::new(ManuallyHash::new(map, typst_utils::hash128(data))))) @@ -351,36 +355,47 @@ impl Debug for Bibliography { } /// Decode on library from one data source. -fn decode_library(source: &DataSource, data: &Bytes) -> StrResult { - let src = data.as_str().map_err(FileError::from)?; +fn decode_library(loaded: &Loaded) -> SourceResult { + let data = loaded.data.as_str().within(loaded)?; - if let DataSource::Path(path) = source { + if let LoadSource::Path(file_id) = loaded.source.v { // If we got a path, use the extension to determine whether it is // YAML or BibLaTeX. - let ext = Path::new(path.as_str()) + let ext = file_id + .vpath() + .as_rooted_path() .extension() .and_then(OsStr::to_str) .unwrap_or_default(); match ext.to_lowercase().as_str() { - "yml" | "yaml" => hayagriva::io::from_yaml_str(src) - .map_err(|err| eco_format!("failed to parse YAML ({err})")), - "bib" => hayagriva::io::from_biblatex_str(src) - .map_err(|errors| format_biblatex_error(src, Some(path), errors)), - _ => bail!("unknown bibliography format (must be .yml/.yaml or .bib)"), + "yml" | "yaml" => hayagriva::io::from_yaml_str(data) + .map_err(format_yaml_error) + .within(loaded), + "bib" => hayagriva::io::from_biblatex_str(data) + .map_err(format_biblatex_error) + .within(loaded), + _ => bail!( + loaded.source.span, + "unknown bibliography format (must be .yml/.yaml or .bib)" + ), } } else { // If we just got bytes, we need to guess. If it can be decoded as // hayagriva YAML, we'll use that. - let haya_err = match hayagriva::io::from_yaml_str(src) { + let haya_err = match hayagriva::io::from_yaml_str(data) { Ok(library) => return Ok(library), Err(err) => err, }; // If it can be decoded as BibLaTeX, we use that isntead. - let bib_errs = match hayagriva::io::from_biblatex_str(src) { - Ok(library) => return Ok(library), - Err(err) => err, + let bib_errs = match hayagriva::io::from_biblatex_str(data) { + // If the file is almost valid yaml, but contains no `@` character + // it will be successfully parsed as an empty BibLaTeX library, + // since BibLaTeX does support arbitrary text outside of entries. + Ok(library) if !library.is_empty() => return Ok(library), + Ok(_) => None, + Err(err) => Some(err), }; // If neither decoded correctly, check whether `:` or `{` appears @@ -388,7 +403,7 @@ fn decode_library(source: &DataSource, data: &Bytes) -> StrResult { // and emit the more appropriate error. let mut yaml = 0; let mut biblatex = 0; - for c in src.chars() { + for c in data.chars() { match c { ':' => yaml += 1, '{' => biblatex += 1, @@ -396,37 +411,33 @@ fn decode_library(source: &DataSource, data: &Bytes) -> StrResult { } } - if yaml > biblatex { - bail!("failed to parse YAML ({haya_err})") - } else { - Err(format_biblatex_error(src, None, bib_errs)) + match bib_errs { + Some(bib_errs) if biblatex >= yaml => { + Err(format_biblatex_error(bib_errs)).within(loaded) + } + _ => Err(format_yaml_error(haya_err)).within(loaded), } } } /// Format a BibLaTeX loading error. -fn format_biblatex_error( - src: &str, - path: Option<&str>, - errors: Vec, -) -> EcoString { - let Some(error) = errors.first() else { - return match path { - Some(path) => eco_format!("failed to parse BibLaTeX file ({path})"), - None => eco_format!("failed to parse BibLaTeX"), - }; +fn format_biblatex_error(errors: Vec) -> LoadError { + // TODO: return multiple errors? + let Some(error) = errors.into_iter().next() else { + // TODO: can this even happen, should we just unwrap? + return LoadError::new( + ReportPos::None, + "failed to parse BibLaTeX", + "something went wrong", + ); }; - let (span, msg) = match error { - BibLaTeXError::Parse(error) => (&error.span, error.kind.to_string()), - BibLaTeXError::Type(error) => (&error.span, error.kind.to_string()), + let (range, msg) = match error { + BibLaTeXError::Parse(error) => (error.span, error.kind.to_string()), + BibLaTeXError::Type(error) => (error.span, error.kind.to_string()), }; - let line = src.get(..span.start).unwrap_or_default().lines().count(); - match path { - Some(path) => eco_format!("failed to parse BibLaTeX file ({path}:{line}: {msg})"), - None => eco_format!("failed to parse BibLaTeX ({line}: {msg})"), - } + LoadError::new(range, "failed to parse BibLaTeX", msg) } /// A loaded CSL style. @@ -442,8 +453,8 @@ impl CslStyle { let style = match &source { CslSource::Named(style) => Self::from_archived(*style), CslSource::Normal(source) => { - let data = Spanned::new(source, span).load(world)?; - Self::from_data(data).at(span)? + let loaded = Spanned::new(source, span).load(world)?; + Self::from_data(&loaded.data).within(&loaded)? } }; Ok(Derived::new(source, style)) @@ -464,16 +475,18 @@ impl CslStyle { /// Load a CSL style from file contents. #[comemo::memoize] - pub fn from_data(data: Bytes) -> StrResult { - let text = data.as_str().map_err(FileError::from)?; + pub fn from_data(bytes: &Bytes) -> LoadResult { + let text = bytes.as_str()?; citationberg::IndependentStyle::from_xml(text) .map(|style| { Self(Arc::new(ManuallyHash::new( style, - typst_utils::hash128(&(TypeId::of::(), data)), + typst_utils::hash128(&(TypeId::of::(), bytes)), ))) }) - .map_err(|err| eco_format!("failed to load CSL style ({err})")) + .map_err(|err| { + LoadError::new(ReportPos::None, "failed to load CSL style", err) + }) } /// Get the underlying independent style. diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index d5c07424d..f2485e16b 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -3,15 +3,17 @@ use std::ops::Range; use std::sync::{Arc, LazyLock}; use comemo::Tracked; -use ecow::{eco_format, EcoString, EcoVec}; -use syntect::highlighting as synt; -use syntect::parsing::{SyntaxDefinition, SyntaxSet, SyntaxSetBuilder}; +use ecow::{EcoString, EcoVec}; +use syntect::highlighting::{self as synt}; +use syntect::parsing::{ParseSyntaxError, SyntaxDefinition, SyntaxSet, SyntaxSetBuilder}; use typst_syntax::{split_newlines, LinkedNode, Span, Spanned}; use typst_utils::ManuallyHash; use unicode_segmentation::UnicodeSegmentation; use super::Lang; -use crate::diag::{At, FileError, SourceResult, StrResult}; +use crate::diag::{ + LineCol, LoadError, LoadResult, LoadedWithin, ReportPos, SourceResult, +}; use crate::engine::Engine; use crate::foundations::{ cast, elem, scope, Bytes, Content, Derived, NativeElement, OneOrMultiple, Packed, @@ -539,40 +541,29 @@ impl RawSyntax { world: Tracked, sources: Spanned>, ) -> SourceResult, Vec>> { - let data = sources.load(world)?; - let list = sources - .v - .0 + let loaded = sources.load(world)?; + let list = loaded .iter() - .zip(&data) - .map(|(source, data)| Self::decode(source, data)) - .collect::>() - .at(sources.span)?; + .map(|data| Self::decode(&data.data).within(data)) + .collect::>()?; Ok(Derived::new(sources.v, list)) } /// Decode a syntax from a loaded source. #[comemo::memoize] #[typst_macros::time(name = "load syntaxes")] - fn decode(source: &DataSource, data: &Bytes) -> StrResult { - let src = data.as_str().map_err(FileError::from)?; - let syntax = SyntaxDefinition::load_from_str(src, false, None).map_err( - |err| match source { - DataSource::Path(path) => { - eco_format!("failed to parse syntax file `{path}` ({err})") - } - DataSource::Bytes(_) => { - eco_format!("failed to parse syntax ({err})") - } - }, - )?; + fn decode(bytes: &Bytes) -> LoadResult { + let str = bytes.as_str()?; + + let syntax = SyntaxDefinition::load_from_str(str, false, None) + .map_err(format_syntax_error)?; let mut builder = SyntaxSetBuilder::new(); builder.add(syntax); Ok(RawSyntax(Arc::new(ManuallyHash::new( builder.build(), - typst_utils::hash128(data), + typst_utils::hash128(bytes), )))) } @@ -582,6 +573,24 @@ impl RawSyntax { } } +fn format_syntax_error(error: ParseSyntaxError) -> LoadError { + let pos = syntax_error_pos(&error); + LoadError::new(pos, "failed to parse syntax", error) +} + +fn syntax_error_pos(error: &ParseSyntaxError) -> ReportPos { + match error { + ParseSyntaxError::InvalidYaml(scan_error) => { + let m = scan_error.marker(); + ReportPos::full( + m.index()..m.index(), + LineCol::one_based(m.line(), m.col() + 1), + ) + } + _ => ReportPos::None, + } +} + /// A loaded syntect theme. #[derive(Debug, Clone, PartialEq, Hash)] pub struct RawTheme(Arc>); @@ -592,18 +601,18 @@ impl RawTheme { world: Tracked, source: Spanned, ) -> SourceResult> { - let data = source.load(world)?; - let theme = Self::decode(&data).at(source.span)?; + let loaded = source.load(world)?; + let theme = Self::decode(&loaded.data).within(&loaded)?; Ok(Derived::new(source.v, theme)) } /// Decode a theme from bytes. #[comemo::memoize] - fn decode(data: &Bytes) -> StrResult { - let mut cursor = std::io::Cursor::new(data.as_slice()); - let theme = synt::ThemeSet::load_from_reader(&mut cursor) - .map_err(|err| eco_format!("failed to parse theme ({err})"))?; - Ok(RawTheme(Arc::new(ManuallyHash::new(theme, typst_utils::hash128(data))))) + fn decode(bytes: &Bytes) -> LoadResult { + let mut cursor = std::io::Cursor::new(bytes.as_slice()); + let theme = + synt::ThemeSet::load_from_reader(&mut cursor).map_err(format_theme_error)?; + Ok(RawTheme(Arc::new(ManuallyHash::new(theme, typst_utils::hash128(bytes))))) } /// Get the underlying syntect theme. @@ -612,6 +621,14 @@ impl RawTheme { } } +fn format_theme_error(error: syntect::LoadingError) -> LoadError { + let pos = match &error { + syntect::LoadingError::ParseSyntax(err, _) => syntax_error_pos(err), + _ => ReportPos::None, + }; + LoadError::new(pos, "failed to parse theme", error) +} + /// A highlighted line of raw text. /// /// This is a helper element that is synthesized by [`raw`] elements. diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index f9e345e70..f5109798b 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -22,7 +22,7 @@ use crate::foundations::{ Smart, StyleChain, }; use crate::layout::{BlockElem, Length, Rel, Sizing}; -use crate::loading::{DataSource, Load, Readable}; +use crate::loading::{DataSource, Load, LoadSource, Loaded, Readable}; use crate::model::Figurable; use crate::text::LocalName; @@ -65,10 +65,10 @@ pub struct ImageElem { #[required] #[parse( let source = args.expect::>("source")?; - let data = source.load(engine.world)?; - Derived::new(source.v, data) + let loaded = source.load(engine.world)?; + Derived::new(source.v, loaded) )] - pub source: Derived, + pub source: Derived, /// The image's format. /// @@ -154,8 +154,8 @@ pub struct ImageElem { /// to `{auto}`, Typst will try to extract an ICC profile from the image. #[parse(match args.named::>>("icc")? { Some(Spanned { v: Smart::Custom(source), span }) => Some(Smart::Custom({ - let data = Spanned::new(&source, span).load(engine.world)?; - Derived::new(source, data) + let loaded = Spanned::new(&source, span).load(engine.world)?; + Derived::new(source, loaded.data) })), Some(Spanned { v: Smart::Auto, .. }) => Some(Smart::Auto), None => None, @@ -173,7 +173,7 @@ impl ImageElem { pub fn decode( span: Span, /// The data to decode as an image. Can be a string for SVGs. - data: Readable, + data: Spanned, /// The image's format. Detected automatically by default. #[named] format: Option>, @@ -193,8 +193,10 @@ impl ImageElem { #[named] scaling: Option>, ) -> StrResult { - let bytes = data.into_bytes(); - let source = Derived::new(DataSource::Bytes(bytes.clone()), bytes); + let bytes = data.v.into_bytes(); + let loaded = + Loaded::new(Spanned::new(LoadSource::Bytes, data.span), bytes.clone()); + let source = Derived::new(DataSource::Bytes(bytes), loaded); let mut elem = ImageElem::new(source); if let Some(format) = format { elem.push_format(format); diff --git a/crates/typst-library/src/visualize/image/svg.rs b/crates/typst-library/src/visualize/image/svg.rs index 9bf1ead0d..1a3f6d474 100644 --- a/crates/typst-library/src/visualize/image/svg.rs +++ b/crates/typst-library/src/visualize/image/svg.rs @@ -3,10 +3,9 @@ use std::hash::{Hash, Hasher}; use std::sync::{Arc, Mutex}; use comemo::Tracked; -use ecow::EcoString; use siphasher::sip128::{Hasher128, SipHasher13}; -use crate::diag::{format_xml_like_error, StrResult}; +use crate::diag::{format_xml_like_error, LoadError, LoadResult, ReportPos}; use crate::foundations::Bytes; use crate::layout::Axes; use crate::text::{ @@ -30,7 +29,7 @@ impl SvgImage { /// Decode an SVG image without fonts. #[comemo::memoize] #[typst_macros::time(name = "load svg")] - pub fn new(data: Bytes) -> StrResult { + pub fn new(data: Bytes) -> LoadResult { let tree = usvg::Tree::from_data(&data, &base_options()).map_err(format_usvg_error)?; Ok(Self(Arc::new(Repr { data, size: tree_size(&tree), font_hash: 0, tree }))) @@ -43,7 +42,7 @@ impl SvgImage { data: Bytes, world: Tracked, families: &[&str], - ) -> StrResult { + ) -> LoadResult { let book = world.book(); let resolver = Mutex::new(FontResolver::new(world, book, families)); let tree = usvg::Tree::from_data( @@ -125,16 +124,15 @@ fn tree_size(tree: &usvg::Tree) -> Axes { } /// Format the user-facing SVG decoding error message. -fn format_usvg_error(error: usvg::Error) -> EcoString { - match error { - usvg::Error::NotAnUtf8Str => "file is not valid utf-8".into(), - usvg::Error::MalformedGZip => "file is not compressed correctly".into(), - usvg::Error::ElementsLimitReached => "file is too large".into(), - usvg::Error::InvalidSize => { - "failed to parse SVG (width, height, or viewbox is invalid)".into() - } - usvg::Error::ParsingFailed(error) => format_xml_like_error("SVG", error), - } +fn format_usvg_error(error: usvg::Error) -> LoadError { + let error = match error { + usvg::Error::NotAnUtf8Str => "file is not valid utf-8", + usvg::Error::MalformedGZip => "file is not compressed correctly", + usvg::Error::ElementsLimitReached => "file is too large", + usvg::Error::InvalidSize => "width, height, or viewbox is invalid", + usvg::Error::ParsingFailed(error) => return format_xml_like_error("SVG", error), + }; + LoadError::new(ReportPos::None, "failed to parse SVG", error) } /// Provides Typst's fonts to usvg. diff --git a/crates/typst-syntax/Cargo.toml b/crates/typst-syntax/Cargo.toml index 263595bd4..c20f6a087 100644 --- a/crates/typst-syntax/Cargo.toml +++ b/crates/typst-syntax/Cargo.toml @@ -15,6 +15,7 @@ readme = { workspace = true } [dependencies] typst-timing = { workspace = true } typst-utils = { workspace = true } +comemo = { workspace = true } ecow = { workspace = true } serde = { workspace = true } toml = { workspace = true } diff --git a/crates/typst-syntax/src/lib.rs b/crates/typst-syntax/src/lib.rs index 5e7b710fc..1249f88e9 100644 --- a/crates/typst-syntax/src/lib.rs +++ b/crates/typst-syntax/src/lib.rs @@ -7,6 +7,7 @@ mod file; mod highlight; mod kind; mod lexer; +mod lines; mod node; mod parser; mod path; @@ -22,6 +23,7 @@ pub use self::lexer::{ is_id_continue, is_id_start, is_ident, is_newline, is_valid_label_literal_id, link_prefix, split_newlines, }; +pub use self::lines::Lines; pub use self::node::{LinkedChildren, LinkedNode, Side, SyntaxError, SyntaxNode}; pub use self::parser::{parse, parse_code, parse_math}; pub use self::path::VirtualPath; diff --git a/crates/typst-syntax/src/lines.rs b/crates/typst-syntax/src/lines.rs new file mode 100644 index 000000000..fa1e77563 --- /dev/null +++ b/crates/typst-syntax/src/lines.rs @@ -0,0 +1,402 @@ +use std::hash::{Hash, Hasher}; +use std::iter::zip; +use std::ops::Range; +use std::sync::Arc; + +use crate::is_newline; + +/// A text buffer and metadata about lines. +#[derive(Clone)] +pub struct Lines(Arc>); + +#[derive(Clone)] +struct Repr { + lines: Vec, + text: T, +} + +/// Metadata about a line. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct Line { + /// The UTF-8 byte offset where the line starts. + byte_idx: usize, + /// The UTF-16 codepoint offset where the line starts. + utf16_idx: usize, +} + +impl> Lines { + /// Create from the text buffer and compute the line metadata. + pub fn new(text: T) -> Self { + let lines = lines(text.as_ref()); + Lines(Arc::new(Repr { lines, text })) + } + + /// The text as a string slice. + pub fn text(&self) -> &str { + self.0.text.as_ref() + } + + /// Get the length of the file in UTF-8 encoded bytes. + pub fn len_bytes(&self) -> usize { + self.0.text.as_ref().len() + } + + /// Get the length of the file in UTF-16 code units. + pub fn len_utf16(&self) -> usize { + let last = self.0.lines.last().unwrap(); + last.utf16_idx + len_utf16(&self.text()[last.byte_idx..]) + } + + /// Get the length of the file in lines. + pub fn len_lines(&self) -> usize { + self.0.lines.len() + } + + /// Return the index of the UTF-16 code unit at the byte index. + pub fn byte_to_utf16(&self, byte_idx: usize) -> Option { + let line_idx = self.byte_to_line(byte_idx)?; + let line = self.0.lines.get(line_idx)?; + let head = self.text().get(line.byte_idx..byte_idx)?; + Some(line.utf16_idx + len_utf16(head)) + } + + /// Return the index of the line that contains the given byte index. + pub fn byte_to_line(&self, byte_idx: usize) -> Option { + (byte_idx <= self.text().len()).then(|| { + match self.0.lines.binary_search_by_key(&byte_idx, |line| line.byte_idx) { + Ok(i) => i, + Err(i) => i - 1, + } + }) + } + + /// Return the index of the column at the byte index. + /// + /// The column is defined as the number of characters in the line before the + /// byte index. + pub fn byte_to_column(&self, byte_idx: usize) -> Option { + let line = self.byte_to_line(byte_idx)?; + let start = self.line_to_byte(line)?; + let head = self.text().get(start..byte_idx)?; + Some(head.chars().count()) + } + + /// Return the index of the line and column at the byte index. + pub fn byte_to_line_column(&self, byte_idx: usize) -> Option<(usize, usize)> { + let line = self.byte_to_line(byte_idx)?; + let start = self.line_to_byte(line)?; + let head = self.text().get(start..byte_idx)?; + let col = head.chars().count(); + Some((line, col)) + } + + /// Return the byte index at the UTF-16 code unit. + pub fn utf16_to_byte(&self, utf16_idx: usize) -> Option { + let line = self.0.lines.get( + match self.0.lines.binary_search_by_key(&utf16_idx, |line| line.utf16_idx) { + Ok(i) => i, + Err(i) => i - 1, + }, + )?; + + let text = self.text(); + let mut k = line.utf16_idx; + for (i, c) in text[line.byte_idx..].char_indices() { + if k >= utf16_idx { + return Some(line.byte_idx + i); + } + k += c.len_utf16(); + } + + (k == utf16_idx).then_some(text.len()) + } + + /// Return the byte position at which the given line starts. + pub fn line_to_byte(&self, line_idx: usize) -> Option { + self.0.lines.get(line_idx).map(|line| line.byte_idx) + } + + /// Return the range which encloses the given line. + pub fn line_to_range(&self, line_idx: usize) -> Option> { + let start = self.line_to_byte(line_idx)?; + let end = self.line_to_byte(line_idx + 1).unwrap_or(self.text().len()); + Some(start..end) + } + + /// Return the byte index of the given (line, column) pair. + /// + /// The column defines the number of characters to go beyond the start of + /// the line. + pub fn line_column_to_byte( + &self, + line_idx: usize, + column_idx: usize, + ) -> Option { + let range = self.line_to_range(line_idx)?; + let line = self.text().get(range.clone())?; + let mut chars = line.chars(); + for _ in 0..column_idx { + chars.next(); + } + Some(range.start + (line.len() - chars.as_str().len())) + } +} + +impl Lines { + /// Fully replace the source text. + /// + /// This performs a naive (suffix/prefix-based) diff of the old and new text + /// to produce the smallest single edit that transforms old into new and + /// then calls [`edit`](Self::edit) with it. + /// + /// Returns whether any changes were made. + pub fn replace(&mut self, new: &str) -> bool { + let Some((prefix, suffix)) = self.replacement_range(new) else { + return false; + }; + + let old = self.text(); + let replace = prefix..old.len() - suffix; + let with = &new[prefix..new.len() - suffix]; + self.edit(replace, with); + + true + } + + /// Returns the common prefix and suffix lengths. + /// Returns [`None`] if the old and new strings are equal. + pub fn replacement_range(&self, new: &str) -> Option<(usize, usize)> { + let old = self.text(); + + let mut prefix = + zip(old.bytes(), new.bytes()).take_while(|(x, y)| x == y).count(); + + if prefix == old.len() && prefix == new.len() { + return None; + } + + while !old.is_char_boundary(prefix) || !new.is_char_boundary(prefix) { + prefix -= 1; + } + + let mut suffix = zip(old[prefix..].bytes().rev(), new[prefix..].bytes().rev()) + .take_while(|(x, y)| x == y) + .count(); + + while !old.is_char_boundary(old.len() - suffix) + || !new.is_char_boundary(new.len() - suffix) + { + suffix += 1; + } + + Some((prefix, suffix)) + } + + /// Edit the source file by replacing the given range. + /// + /// Returns the range in the new source that was ultimately reparsed. + /// + /// The method panics if the `replace` range is out of bounds. + #[track_caller] + pub fn edit(&mut self, replace: Range, with: &str) { + let start_byte = replace.start; + let start_utf16 = self.byte_to_utf16(start_byte).unwrap(); + let line = self.byte_to_line(start_byte).unwrap(); + + let inner = Arc::make_mut(&mut self.0); + + // Update the text itself. + inner.text.replace_range(replace.clone(), with); + + // Remove invalidated line starts. + inner.lines.truncate(line + 1); + + // Handle adjoining of \r and \n. + if inner.text[..start_byte].ends_with('\r') && with.starts_with('\n') { + inner.lines.pop(); + } + + // Recalculate the line starts after the edit. + inner.lines.extend(lines_from( + start_byte, + start_utf16, + &inner.text[start_byte..], + )); + } +} + +impl Hash for Lines { + fn hash(&self, state: &mut H) { + self.0.text.hash(state); + } +} + +impl> AsRef for Lines { + fn as_ref(&self) -> &str { + self.0.text.as_ref() + } +} + +/// Create a line vector. +fn lines(text: &str) -> Vec { + std::iter::once(Line { byte_idx: 0, utf16_idx: 0 }) + .chain(lines_from(0, 0, text)) + .collect() +} + +/// Compute a line iterator from an offset. +fn lines_from( + byte_offset: usize, + utf16_offset: usize, + text: &str, +) -> impl Iterator + '_ { + let mut s = unscanny::Scanner::new(text); + let mut utf16_idx = utf16_offset; + + std::iter::from_fn(move || { + s.eat_until(|c: char| { + utf16_idx += c.len_utf16(); + is_newline(c) + }); + + if s.done() { + return None; + } + + if s.eat() == Some('\r') && s.eat_if('\n') { + utf16_idx += 1; + } + + Some(Line { byte_idx: byte_offset + s.cursor(), utf16_idx }) + }) +} + +/// The number of code units this string would use if it was encoded in +/// UTF16. This runs in linear time. +fn len_utf16(string: &str) -> usize { + string.chars().map(char::len_utf16).sum() +} + +#[cfg(test)] +mod tests { + use super::*; + + const TEST: &str = "ä\tcde\nf💛g\r\nhi\rjkl"; + + #[test] + fn test_source_file_new() { + let lines = Lines::new(TEST); + assert_eq!( + lines.0.lines, + [ + Line { byte_idx: 0, utf16_idx: 0 }, + Line { byte_idx: 7, utf16_idx: 6 }, + Line { byte_idx: 15, utf16_idx: 12 }, + Line { byte_idx: 18, utf16_idx: 15 }, + ] + ); + } + + #[test] + fn test_source_file_pos_to_line() { + let lines = Lines::new(TEST); + assert_eq!(lines.byte_to_line(0), Some(0)); + assert_eq!(lines.byte_to_line(2), Some(0)); + assert_eq!(lines.byte_to_line(6), Some(0)); + assert_eq!(lines.byte_to_line(7), Some(1)); + assert_eq!(lines.byte_to_line(8), Some(1)); + assert_eq!(lines.byte_to_line(12), Some(1)); + assert_eq!(lines.byte_to_line(21), Some(3)); + assert_eq!(lines.byte_to_line(22), None); + } + + #[test] + fn test_source_file_pos_to_column() { + let lines = Lines::new(TEST); + assert_eq!(lines.byte_to_column(0), Some(0)); + assert_eq!(lines.byte_to_column(2), Some(1)); + assert_eq!(lines.byte_to_column(6), Some(5)); + assert_eq!(lines.byte_to_column(7), Some(0)); + assert_eq!(lines.byte_to_column(8), Some(1)); + assert_eq!(lines.byte_to_column(12), Some(2)); + } + + #[test] + fn test_source_file_utf16() { + #[track_caller] + fn roundtrip(lines: &Lines<&str>, byte_idx: usize, utf16_idx: usize) { + let middle = lines.byte_to_utf16(byte_idx).unwrap(); + let result = lines.utf16_to_byte(middle).unwrap(); + assert_eq!(middle, utf16_idx); + assert_eq!(result, byte_idx); + } + + let lines = Lines::new(TEST); + roundtrip(&lines, 0, 0); + roundtrip(&lines, 2, 1); + roundtrip(&lines, 3, 2); + roundtrip(&lines, 8, 7); + roundtrip(&lines, 12, 9); + roundtrip(&lines, 21, 18); + assert_eq!(lines.byte_to_utf16(22), None); + assert_eq!(lines.utf16_to_byte(19), None); + } + + #[test] + fn test_source_file_roundtrip() { + #[track_caller] + fn roundtrip(lines: &Lines<&str>, byte_idx: usize) { + let line = lines.byte_to_line(byte_idx).unwrap(); + let column = lines.byte_to_column(byte_idx).unwrap(); + let result = lines.line_column_to_byte(line, column).unwrap(); + assert_eq!(result, byte_idx); + } + + let lines = Lines::new(TEST); + roundtrip(&lines, 0); + roundtrip(&lines, 7); + roundtrip(&lines, 12); + roundtrip(&lines, 21); + } + + #[test] + fn test_source_file_edit() { + // This tests only the non-parser parts. The reparsing itself is + // tested separately. + #[track_caller] + fn test(prev: &str, range: Range, with: &str, after: &str) { + let reference = Lines::new(after); + + let mut edited = Lines::new(prev.to_string()); + edited.edit(range.clone(), with); + assert_eq!(edited.text(), reference.text()); + assert_eq!(edited.0.lines, reference.0.lines); + + let mut replaced = Lines::new(prev.to_string()); + replaced.replace(&{ + let mut s = prev.to_string(); + s.replace_range(range, with); + s + }); + assert_eq!(replaced.text(), reference.text()); + assert_eq!(replaced.0.lines, reference.0.lines); + } + + // Test inserting at the beginning. + test("abc\n", 0..0, "hi\n", "hi\nabc\n"); + test("\nabc", 0..0, "hi\r", "hi\r\nabc"); + + // Test editing in the middle. + test(TEST, 4..16, "❌", "ä\tc❌i\rjkl"); + + // Test appending. + test("abc\ndef", 7..7, "hi", "abc\ndefhi"); + test("abc\ndef\n", 8..8, "hi", "abc\ndef\nhi"); + + // Test appending with adjoining \r and \n. + test("abc\ndef\r", 8..8, "\nghi", "abc\ndef\r\nghi"); + + // Test removing everything. + test(TEST, 0..21, "", ""); + } +} diff --git a/crates/typst-syntax/src/source.rs b/crates/typst-syntax/src/source.rs index 6ff94c73f..514cb9a4a 100644 --- a/crates/typst-syntax/src/source.rs +++ b/crates/typst-syntax/src/source.rs @@ -2,14 +2,14 @@ use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; -use std::iter::zip; use std::ops::Range; use std::sync::Arc; use typst_utils::LazyHash; +use crate::lines::Lines; use crate::reparser::reparse; -use crate::{is_newline, parse, FileId, LinkedNode, Span, SyntaxNode, VirtualPath}; +use crate::{parse, FileId, LinkedNode, Span, SyntaxNode, VirtualPath}; /// A source file. /// @@ -24,9 +24,8 @@ pub struct Source(Arc); #[derive(Clone)] struct Repr { id: FileId, - text: LazyHash, root: LazyHash, - lines: Vec, + lines: LazyHash>, } impl Source { @@ -37,8 +36,7 @@ impl Source { root.numberize(id, Span::FULL).unwrap(); Self(Arc::new(Repr { id, - lines: lines(&text), - text: LazyHash::new(text), + lines: LazyHash::new(Lines::new(text)), root: LazyHash::new(root), })) } @@ -58,9 +56,14 @@ impl Source { self.0.id } + /// The whole source as a string slice. + pub fn lines(&self) -> Lines { + Lines::clone(&self.0.lines) + } + /// The whole source as a string slice. pub fn text(&self) -> &str { - &self.0.text + self.0.lines.text() } /// Slice out the part of the source code enclosed by the range. @@ -77,29 +80,12 @@ impl Source { /// Returns the range in the new source that was ultimately reparsed. pub fn replace(&mut self, new: &str) -> Range { let _scope = typst_timing::TimingScope::new("replace source"); - let old = self.text(); - let mut prefix = - zip(old.bytes(), new.bytes()).take_while(|(x, y)| x == y).count(); - - if prefix == old.len() && prefix == new.len() { + let Some((prefix, suffix)) = self.0.lines.replacement_range(new) else { return 0..0; - } - - while !old.is_char_boundary(prefix) || !new.is_char_boundary(prefix) { - prefix -= 1; - } - - let mut suffix = zip(old[prefix..].bytes().rev(), new[prefix..].bytes().rev()) - .take_while(|(x, y)| x == y) - .count(); - - while !old.is_char_boundary(old.len() - suffix) - || !new.is_char_boundary(new.len() - suffix) - { - suffix += 1; - } + }; + let old = self.text(); let replace = prefix..old.len() - suffix; let with = &new[prefix..new.len() - suffix]; self.edit(replace, with) @@ -112,48 +98,28 @@ impl Source { /// The method panics if the `replace` range is out of bounds. #[track_caller] pub fn edit(&mut self, replace: Range, with: &str) -> Range { - let start_byte = replace.start; - let start_utf16 = self.byte_to_utf16(start_byte).unwrap(); - let line = self.byte_to_line(start_byte).unwrap(); - let inner = Arc::make_mut(&mut self.0); - // Update the text itself. - inner.text.replace_range(replace.clone(), with); - - // Remove invalidated line starts. - inner.lines.truncate(line + 1); - - // Handle adjoining of \r and \n. - if inner.text[..start_byte].ends_with('\r') && with.starts_with('\n') { - inner.lines.pop(); - } - - // Recalculate the line starts after the edit. - inner.lines.extend(lines_from( - start_byte, - start_utf16, - &inner.text[start_byte..], - )); + // Update the text and lines. + inner.lines.edit(replace.clone(), with); // Incrementally reparse the replaced range. - reparse(&mut inner.root, &inner.text, replace, with.len()) + reparse(&mut inner.root, inner.lines.text(), replace, with.len()) } /// Get the length of the file in UTF-8 encoded bytes. pub fn len_bytes(&self) -> usize { - self.text().len() + self.0.lines.len_bytes() } /// Get the length of the file in UTF-16 code units. pub fn len_utf16(&self) -> usize { - let last = self.0.lines.last().unwrap(); - last.utf16_idx + len_utf16(&self.0.text[last.byte_idx..]) + self.0.lines.len_utf16() } /// Get the length of the file in lines. pub fn len_lines(&self) -> usize { - self.0.lines.len() + self.0.lines.len_lines() } /// Find the node with the given span. @@ -171,85 +137,6 @@ impl Source { pub fn range(&self, span: Span) -> Option> { Some(self.find(span)?.range()) } - - /// Return the index of the UTF-16 code unit at the byte index. - pub fn byte_to_utf16(&self, byte_idx: usize) -> Option { - let line_idx = self.byte_to_line(byte_idx)?; - let line = self.0.lines.get(line_idx)?; - let head = self.0.text.get(line.byte_idx..byte_idx)?; - Some(line.utf16_idx + len_utf16(head)) - } - - /// Return the index of the line that contains the given byte index. - pub fn byte_to_line(&self, byte_idx: usize) -> Option { - (byte_idx <= self.0.text.len()).then(|| { - match self.0.lines.binary_search_by_key(&byte_idx, |line| line.byte_idx) { - Ok(i) => i, - Err(i) => i - 1, - } - }) - } - - /// Return the index of the column at the byte index. - /// - /// The column is defined as the number of characters in the line before the - /// byte index. - pub fn byte_to_column(&self, byte_idx: usize) -> Option { - let line = self.byte_to_line(byte_idx)?; - let start = self.line_to_byte(line)?; - let head = self.get(start..byte_idx)?; - Some(head.chars().count()) - } - - /// Return the byte index at the UTF-16 code unit. - pub fn utf16_to_byte(&self, utf16_idx: usize) -> Option { - let line = self.0.lines.get( - match self.0.lines.binary_search_by_key(&utf16_idx, |line| line.utf16_idx) { - Ok(i) => i, - Err(i) => i - 1, - }, - )?; - - let mut k = line.utf16_idx; - for (i, c) in self.0.text[line.byte_idx..].char_indices() { - if k >= utf16_idx { - return Some(line.byte_idx + i); - } - k += c.len_utf16(); - } - - (k == utf16_idx).then_some(self.0.text.len()) - } - - /// Return the byte position at which the given line starts. - pub fn line_to_byte(&self, line_idx: usize) -> Option { - self.0.lines.get(line_idx).map(|line| line.byte_idx) - } - - /// Return the range which encloses the given line. - pub fn line_to_range(&self, line_idx: usize) -> Option> { - let start = self.line_to_byte(line_idx)?; - let end = self.line_to_byte(line_idx + 1).unwrap_or(self.0.text.len()); - Some(start..end) - } - - /// Return the byte index of the given (line, column) pair. - /// - /// The column defines the number of characters to go beyond the start of - /// the line. - pub fn line_column_to_byte( - &self, - line_idx: usize, - column_idx: usize, - ) -> Option { - let range = self.line_to_range(line_idx)?; - let line = self.get(range.clone())?; - let mut chars = line.chars(); - for _ in 0..column_idx { - chars.next(); - } - Some(range.start + (line.len() - chars.as_str().len())) - } } impl Debug for Source { @@ -261,7 +148,7 @@ impl Debug for Source { impl Hash for Source { fn hash(&self, state: &mut H) { self.0.id.hash(state); - self.0.text.hash(state); + self.0.lines.hash(state); self.0.root.hash(state); } } @@ -271,176 +158,3 @@ impl AsRef for Source { self.text() } } - -/// Metadata about a line. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -struct Line { - /// The UTF-8 byte offset where the line starts. - byte_idx: usize, - /// The UTF-16 codepoint offset where the line starts. - utf16_idx: usize, -} - -/// Create a line vector. -fn lines(text: &str) -> Vec { - std::iter::once(Line { byte_idx: 0, utf16_idx: 0 }) - .chain(lines_from(0, 0, text)) - .collect() -} - -/// Compute a line iterator from an offset. -fn lines_from( - byte_offset: usize, - utf16_offset: usize, - text: &str, -) -> impl Iterator + '_ { - let mut s = unscanny::Scanner::new(text); - let mut utf16_idx = utf16_offset; - - std::iter::from_fn(move || { - s.eat_until(|c: char| { - utf16_idx += c.len_utf16(); - is_newline(c) - }); - - if s.done() { - return None; - } - - if s.eat() == Some('\r') && s.eat_if('\n') { - utf16_idx += 1; - } - - Some(Line { byte_idx: byte_offset + s.cursor(), utf16_idx }) - }) -} - -/// The number of code units this string would use if it was encoded in -/// UTF16. This runs in linear time. -fn len_utf16(string: &str) -> usize { - string.chars().map(char::len_utf16).sum() -} - -#[cfg(test)] -mod tests { - use super::*; - - const TEST: &str = "ä\tcde\nf💛g\r\nhi\rjkl"; - - #[test] - fn test_source_file_new() { - let source = Source::detached(TEST); - assert_eq!( - source.0.lines, - [ - Line { byte_idx: 0, utf16_idx: 0 }, - Line { byte_idx: 7, utf16_idx: 6 }, - Line { byte_idx: 15, utf16_idx: 12 }, - Line { byte_idx: 18, utf16_idx: 15 }, - ] - ); - } - - #[test] - fn test_source_file_pos_to_line() { - let source = Source::detached(TEST); - assert_eq!(source.byte_to_line(0), Some(0)); - assert_eq!(source.byte_to_line(2), Some(0)); - assert_eq!(source.byte_to_line(6), Some(0)); - assert_eq!(source.byte_to_line(7), Some(1)); - assert_eq!(source.byte_to_line(8), Some(1)); - assert_eq!(source.byte_to_line(12), Some(1)); - assert_eq!(source.byte_to_line(21), Some(3)); - assert_eq!(source.byte_to_line(22), None); - } - - #[test] - fn test_source_file_pos_to_column() { - let source = Source::detached(TEST); - assert_eq!(source.byte_to_column(0), Some(0)); - assert_eq!(source.byte_to_column(2), Some(1)); - assert_eq!(source.byte_to_column(6), Some(5)); - assert_eq!(source.byte_to_column(7), Some(0)); - assert_eq!(source.byte_to_column(8), Some(1)); - assert_eq!(source.byte_to_column(12), Some(2)); - } - - #[test] - fn test_source_file_utf16() { - #[track_caller] - fn roundtrip(source: &Source, byte_idx: usize, utf16_idx: usize) { - let middle = source.byte_to_utf16(byte_idx).unwrap(); - let result = source.utf16_to_byte(middle).unwrap(); - assert_eq!(middle, utf16_idx); - assert_eq!(result, byte_idx); - } - - let source = Source::detached(TEST); - roundtrip(&source, 0, 0); - roundtrip(&source, 2, 1); - roundtrip(&source, 3, 2); - roundtrip(&source, 8, 7); - roundtrip(&source, 12, 9); - roundtrip(&source, 21, 18); - assert_eq!(source.byte_to_utf16(22), None); - assert_eq!(source.utf16_to_byte(19), None); - } - - #[test] - fn test_source_file_roundtrip() { - #[track_caller] - fn roundtrip(source: &Source, byte_idx: usize) { - let line = source.byte_to_line(byte_idx).unwrap(); - let column = source.byte_to_column(byte_idx).unwrap(); - let result = source.line_column_to_byte(line, column).unwrap(); - assert_eq!(result, byte_idx); - } - - let source = Source::detached(TEST); - roundtrip(&source, 0); - roundtrip(&source, 7); - roundtrip(&source, 12); - roundtrip(&source, 21); - } - - #[test] - fn test_source_file_edit() { - // This tests only the non-parser parts. The reparsing itself is - // tested separately. - #[track_caller] - fn test(prev: &str, range: Range, with: &str, after: &str) { - let reference = Source::detached(after); - - let mut edited = Source::detached(prev); - edited.edit(range.clone(), with); - assert_eq!(edited.text(), reference.text()); - assert_eq!(edited.0.lines, reference.0.lines); - - let mut replaced = Source::detached(prev); - replaced.replace(&{ - let mut s = prev.to_string(); - s.replace_range(range, with); - s - }); - assert_eq!(replaced.text(), reference.text()); - assert_eq!(replaced.0.lines, reference.0.lines); - } - - // Test inserting at the beginning. - test("abc\n", 0..0, "hi\n", "hi\nabc\n"); - test("\nabc", 0..0, "hi\r", "hi\r\nabc"); - - // Test editing in the middle. - test(TEST, 4..16, "❌", "ä\tc❌i\rjkl"); - - // Test appending. - test("abc\ndef", 7..7, "hi", "abc\ndefhi"); - test("abc\ndef\n", 8..8, "hi", "abc\ndef\nhi"); - - // Test appending with adjoining \r and \n. - test("abc\ndef\r", 8..8, "\nghi", "abc\ndef\r\nghi"); - - // Test removing everything. - test(TEST, 0..21, "", ""); - } -} diff --git a/tests/src/collect.rs b/tests/src/collect.rs index 84af04d2d..173488b01 100644 --- a/tests/src/collect.rs +++ b/tests/src/collect.rs @@ -7,7 +7,9 @@ use std::sync::LazyLock; use ecow::{eco_format, EcoString}; use typst_syntax::package::PackageVersion; -use typst_syntax::{is_id_continue, is_ident, is_newline, FileId, Source, VirtualPath}; +use typst_syntax::{ + is_id_continue, is_ident, is_newline, FileId, Lines, Source, VirtualPath, +}; use unscanny::Scanner; /// Collects all tests from all files. @@ -79,6 +81,8 @@ impl Display for FileSize { pub struct Note { pub pos: FilePos, pub kind: NoteKind, + /// The file [`Self::range`] belongs to. + pub file: FileId, pub range: Option>, pub message: String, } @@ -341,9 +345,28 @@ impl<'a> Parser<'a> { let kind: NoteKind = head.parse().ok()?; self.s.eat_if(' '); + let mut file = None; + if self.s.eat_if('"') { + let path = self.s.eat_until(|c| is_newline(c) || c == '"'); + if !self.s.eat_if('"') { + self.error("expected closing quote after file path"); + return None; + } + + let vpath = VirtualPath::new(path); + file = Some(FileId::new(None, vpath)); + + self.s.eat_if(' '); + } + let mut range = None; if self.s.at('-') || self.s.at(char::is_numeric) { - range = self.parse_range(source); + if let Some(file) = file { + range = self.parse_range_external(file); + } else { + range = self.parse_range(source); + } + if range.is_none() { self.error("range is malformed"); return None; @@ -359,11 +382,78 @@ impl<'a> Parser<'a> { Some(Note { pos: FilePos::new(self.path, self.line), kind, + file: file.unwrap_or(source.id()), range, message, }) } + #[cfg(not(feature = "default"))] + fn parse_range_external(&mut self, _file: FileId) -> Option> { + panic!("external file ranges are not expected when testing `typst_syntax`"); + } + + /// Parse a range in an external file, optionally abbreviated as just a position + /// if the range is empty. + #[cfg(feature = "default")] + fn parse_range_external(&mut self, file: FileId) -> Option> { + use typst::foundations::Bytes; + + use crate::world::{read, system_path}; + + let path = match system_path(file) { + Ok(path) => path, + Err(err) => { + self.error(err.to_string()); + return None; + } + }; + + let bytes = match read(&path) { + Ok(data) => Bytes::new(data), + Err(err) => { + self.error(err.to_string()); + return None; + } + }; + + let start = self.parse_line_col()?; + let lines = Lines::try_from(&bytes).expect( + "errors shouldn't be annotated for files \ + that aren't human readable (not valid utf-8)", + ); + let range = if self.s.eat_if('-') { + let (line, col) = start; + let start = lines.line_column_to_byte(line, col); + let (line, col) = self.parse_line_col()?; + let end = lines.line_column_to_byte(line, col); + Option::zip(start, end).map(|(a, b)| a..b) + } else { + let (line, col) = start; + lines.line_column_to_byte(line, col).map(|i| i..i) + }; + if range.is_none() { + self.error("range is out of bounds"); + } + range + } + + /// Parses absolute `line:column` indices in an external file. + fn parse_line_col(&mut self) -> Option<(usize, usize)> { + let line = self.parse_number()?; + if !self.s.eat_if(':') { + self.error("positions in external files always require both `:`"); + return None; + } + let col = self.parse_number()?; + if line < 0 || col < 0 { + self.error("line and column numbers must be positive"); + return None; + } + + Some(((line as usize).saturating_sub(1), (col as usize).saturating_sub(1))) + } + /// Parse a range, optionally abbreviated as just a position if the range /// is empty. fn parse_range(&mut self, source: &Source) -> Option> { @@ -389,13 +479,13 @@ impl<'a> Parser<'a> { let line_idx = (line_idx_in_test + comments).checked_add_signed(line_delta)?; let column_idx = if column < 0 { // Negative column index is from the back. - let range = source.line_to_range(line_idx)?; + let range = source.lines().line_to_range(line_idx)?; text[range].chars().count().saturating_add_signed(column) } else { usize::try_from(column).ok()?.checked_sub(1)? }; - source.line_column_to_byte(line_idx, column_idx) + source.lines().line_column_to_byte(line_idx, column_idx) } /// Parse a number. diff --git a/tests/src/run.rs b/tests/src/run.rs index 4d08362cf..a34e38db5 100644 --- a/tests/src/run.rs +++ b/tests/src/run.rs @@ -10,10 +10,11 @@ use typst::layout::{Abs, Frame, FrameItem, PagedDocument, Transform}; use typst::visualize::Color; use typst::{Document, WorldExt}; use typst_pdf::PdfOptions; +use typst_syntax::FileId; use crate::collect::{Attr, FileSize, NoteKind, Test}; use crate::logger::TestResult; -use crate::world::TestWorld; +use crate::world::{system_path, TestWorld}; /// Runs a single test. /// @@ -117,7 +118,7 @@ impl<'a> Runner<'a> { if seen { continue; } - let note_range = self.format_range(¬e.range); + let note_range = self.format_range(note.file, ¬e.range); if first { log!(self, "not emitted"); first = false; @@ -208,10 +209,6 @@ impl<'a> Runner<'a> { /// Compare a subset of notes with a given kind against diagnostics of /// that same kind. fn check_diagnostic(&mut self, kind: NoteKind, diag: &SourceDiagnostic) { - // Ignore diagnostics from other sources than the test file itself. - if diag.span.id().is_some_and(|id| id != self.test.source.id()) { - return; - } // TODO: remove this once HTML export is stable if diag.message == "html export is under active development and incomplete" { return; @@ -219,11 +216,11 @@ impl<'a> Runner<'a> { let message = diag.message.replace("\\", "/"); let range = self.world.range(diag.span); - self.validate_note(kind, range.clone(), &message); + self.validate_note(kind, diag.span.id(), range.clone(), &message); // Check hints. for hint in &diag.hints { - self.validate_note(NoteKind::Hint, range.clone(), hint); + self.validate_note(NoteKind::Hint, diag.span.id(), range.clone(), hint); } } @@ -235,15 +232,18 @@ impl<'a> Runner<'a> { fn validate_note( &mut self, kind: NoteKind, + file: Option, range: Option>, message: &str, ) { // Try to find perfect match. + let file = file.unwrap_or(self.test.source.id()); if let Some((i, _)) = self.test.notes.iter().enumerate().find(|&(i, note)| { !self.seen[i] && note.kind == kind && note.range == range && note.message == message + && note.file == file }) { self.seen[i] = true; return; @@ -257,7 +257,7 @@ impl<'a> Runner<'a> { && (note.range == range || note.message == message) }) else { // Not even a close match, diagnostic is not annotated. - let diag_range = self.format_range(&range); + let diag_range = self.format_range(file, &range); log!(into: self.not_annotated, " {kind}: {diag_range} {}", message); return; }; @@ -267,10 +267,10 @@ impl<'a> Runner<'a> { // Range is wrong. if range != note.range { - let note_range = self.format_range(¬e.range); - let note_text = self.text_for_range(¬e.range); - let diag_range = self.format_range(&range); - let diag_text = self.text_for_range(&range); + let note_range = self.format_range(note.file, ¬e.range); + let note_text = self.text_for_range(note.file, ¬e.range); + let diag_range = self.format_range(file, &range); + let diag_text = self.text_for_range(file, &range); log!(self, "mismatched range ({}):", note.pos); log!(self, " message | {}", note.message); log!(self, " annotated | {note_range:<9} | {note_text}"); @@ -286,39 +286,49 @@ impl<'a> Runner<'a> { } /// Display the text for a range. - fn text_for_range(&self, range: &Option>) -> String { + fn text_for_range(&self, file: FileId, range: &Option>) -> String { let Some(range) = range else { return "No text".into() }; if range.is_empty() { - "(empty)".into() - } else { - format!("`{}`", self.test.source.text()[range.clone()].replace('\n', "\\n")) + return "(empty)".into(); } + + let lines = self.world.lookup(file); + lines.text()[range.clone()].replace('\n', "\\n").replace('\r', "\\r") } /// Display a byte range as a line:column range. - fn format_range(&self, range: &Option>) -> String { + fn format_range(&self, file: FileId, range: &Option>) -> String { let Some(range) = range else { return "No range".into() }; + + let mut preamble = String::new(); + if file != self.test.source.id() { + preamble = format!("\"{}\" ", system_path(file).unwrap().display()); + } + if range.start == range.end { - self.format_pos(range.start) + format!("{preamble}{}", self.format_pos(file, range.start)) } else { - format!("{}-{}", self.format_pos(range.start,), self.format_pos(range.end,)) + format!( + "{preamble}{}-{}", + self.format_pos(file, range.start), + self.format_pos(file, range.end) + ) } } /// Display a position as a line:column pair. - fn format_pos(&self, pos: usize) -> String { - if let (Some(line_idx), Some(column_idx)) = - (self.test.source.byte_to_line(pos), self.test.source.byte_to_column(pos)) - { - let line = self.test.pos.line + line_idx; - let column = column_idx + 1; - if line == 1 { - format!("{column}") - } else { - format!("{line}:{column}") - } + fn format_pos(&self, file: FileId, pos: usize) -> String { + let lines = self.world.lookup(file); + + let res = lines.byte_to_line_column(pos).map(|(line, col)| (line + 1, col + 1)); + let Some((line, col)) = res else { + return "oob".into(); + }; + + if line == 1 { + format!("{col}") } else { - "oob".into() + format!("{line}:{col}") } } } diff --git a/tests/src/world.rs b/tests/src/world.rs index fe2bd45ea..bc3e690b2 100644 --- a/tests/src/world.rs +++ b/tests/src/world.rs @@ -20,6 +20,7 @@ use typst::text::{Font, FontBook, TextElem, TextSize}; use typst::utils::{singleton, LazyHash}; use typst::visualize::Color; use typst::{Feature, Library, World}; +use typst_syntax::Lines; /// A world that provides access to the tests environment. #[derive(Clone)] @@ -84,6 +85,22 @@ impl TestWorld { let mut map = self.base.slots.lock(); f(map.entry(id).or_insert_with(|| FileSlot::new(id))) } + + /// Lookup line metadata for a file by id. + #[track_caller] + pub fn lookup(&self, id: FileId) -> Lines { + self.slot(id, |slot| { + if let Some(source) = slot.source.get() { + let source = source.as_ref().expect("file is not valid"); + source.lines() + } else if let Some(bytes) = slot.file.get() { + let bytes = bytes.as_ref().expect("file is not valid"); + Lines::try_from(bytes).expect("file is not valid utf-8") + } else { + panic!("file id does not point to any source file"); + } + }) + } } /// Shared foundation of all test worlds. @@ -149,7 +166,7 @@ impl FileSlot { } /// The file system path for a file ID. -fn system_path(id: FileId) -> FileResult { +pub(crate) fn system_path(id: FileId) -> FileResult { let root: PathBuf = match id.package() { Some(spec) => format!("tests/packages/{}-{}", spec.name, spec.version).into(), None => PathBuf::new(), @@ -159,7 +176,7 @@ fn system_path(id: FileId) -> FileResult { } /// Read a file. -fn read(path: &Path) -> FileResult> { +pub(crate) fn read(path: &Path) -> FileResult> { // Resolve asset. if let Ok(suffix) = path.strip_prefix("assets/") { return typst_dev_assets::get(&suffix.to_string_lossy()) diff --git a/tests/suite/loading/csv.typ b/tests/suite/loading/csv.typ index 6f57ec458..046345bec 100644 --- a/tests/suite/loading/csv.typ +++ b/tests/suite/loading/csv.typ @@ -18,12 +18,12 @@ #csv("nope.csv") --- csv-invalid --- -// Error: 6-28 failed to parse CSV (found 3 instead of 2 fields in line 3) +// Error: "/assets/data/bad.csv" 3:1 failed to parse CSV (found 3 instead of 2 fields in line 3) #csv("/assets/data/bad.csv") --- csv-invalid-row-type-dict --- // Test error numbering with dictionary rows. -// Error: 6-28 failed to parse CSV (found 3 instead of 2 fields in line 3) +// Error: "/assets/data/bad.csv" 3:1 failed to parse CSV (found 3 instead of 2 fields in line 3) #csv("/assets/data/bad.csv", row-type: dictionary) --- csv-invalid-delimiter --- diff --git a/tests/suite/loading/json.typ b/tests/suite/loading/json.typ index c8df1ff6e..9e433992d 100644 --- a/tests/suite/loading/json.typ +++ b/tests/suite/loading/json.typ @@ -6,7 +6,7 @@ #test(data.at(2).weight, 150) --- json-invalid --- -// Error: 7-30 failed to parse JSON (expected value at line 3 column 14) +// Error: "/assets/data/bad.json" 3:14 failed to parse JSON (expected value at line 3 column 14) #json("/assets/data/bad.json") --- json-decode-deprecated --- diff --git a/tests/suite/loading/read.typ b/tests/suite/loading/read.typ index b5c9c0892..57bfc1d5c 100644 --- a/tests/suite/loading/read.typ +++ b/tests/suite/loading/read.typ @@ -8,5 +8,5 @@ #let data = read("/assets/text/missing.txt") --- read-invalid-utf-8 --- -// Error: 18-40 file is not valid utf-8 +// Error: 18-40 failed to convert to string (file is not valid utf-8 in assets/text/bad.txt:1:1) #let data = read("/assets/text/bad.txt") diff --git a/tests/suite/loading/toml.typ b/tests/suite/loading/toml.typ index a4318a015..9d65da452 100644 --- a/tests/suite/loading/toml.typ +++ b/tests/suite/loading/toml.typ @@ -37,7 +37,7 @@ )) --- toml-invalid --- -// Error: 7-30 failed to parse TOML (expected `.`, `=` at line 1 column 16) +// Error: "/assets/data/bad.toml" 1:16-2:1 failed to parse TOML (expected `.`, `=`) #toml("/assets/data/bad.toml") --- toml-decode-deprecated --- diff --git a/tests/suite/loading/xml.typ b/tests/suite/loading/xml.typ index 933f3c480..eed7db0ae 100644 --- a/tests/suite/loading/xml.typ +++ b/tests/suite/loading/xml.typ @@ -24,7 +24,7 @@ ),)) --- xml-invalid --- -// Error: 6-28 failed to parse XML (found closing tag 'data' instead of 'hello' in line 3) +// Error: "/assets/data/bad.xml" 3:0 failed to parse XML (found closing tag 'data' instead of 'hello') #xml("/assets/data/bad.xml") --- xml-decode-deprecated --- diff --git a/tests/suite/loading/yaml.typ b/tests/suite/loading/yaml.typ index a8089052c..ad171c6ef 100644 --- a/tests/suite/loading/yaml.typ +++ b/tests/suite/loading/yaml.typ @@ -13,7 +13,7 @@ #test(data.at("1"), "ok") --- yaml-invalid --- -// Error: 7-30 failed to parse YAML (did not find expected ',' or ']' at line 2 column 1, while parsing a flow sequence at line 1 column 18) +// Error: "/assets/data/bad.yaml" 2:1 failed to parse YAML (did not find expected ',' or ']' at line 2 column 1, while parsing a flow sequence at line 1 column 18) #yaml("/assets/data/bad.yaml") --- yaml-decode-deprecated --- diff --git a/tests/suite/scripting/import.typ b/tests/suite/scripting/import.typ index 49b66ee56..382e444cc 100644 --- a/tests/suite/scripting/import.typ +++ b/tests/suite/scripting/import.typ @@ -334,6 +334,7 @@ --- import-cyclic-in-other-file --- // Cyclic import in other file. +// Error: "tests/suite/scripting/modules/cycle2.typ" 2:9-2:21 cyclic import #import "./modules/cycle1.typ": * This is never reached. diff --git a/tests/suite/visualize/image.typ b/tests/suite/visualize/image.typ index 73c4feff8..45c70c4b8 100644 --- a/tests/suite/visualize/image.typ +++ b/tests/suite/visualize/image.typ @@ -167,7 +167,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B #image("/assets/plugins/hello.wasm") --- image-bad-svg --- -// Error: 2-33 failed to parse SVG (found closing tag 'g' instead of 'style' in line 4) +// Error: "/assets/images/bad.svg" 4:3 failed to parse SVG (found closing tag 'g' instead of 'style') #image("/assets/images/bad.svg") --- image-decode-svg --- @@ -176,7 +176,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B #image.decode(``.text, format: "svg") --- image-decode-bad-svg --- -// Error: 2-168 failed to parse SVG (missing root node) +// Error: 15-152 failed to parse SVG (missing root node at 1:1) // Warning: 8-14 `image.decode` is deprecated, directly pass bytes to `image` instead #image.decode(``.text, format: "svg") From 7c7b962b98a09c1baabdd03ff4ccad8f6d817b37 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Tue, 10 Jun 2025 11:41:16 -0300 Subject: [PATCH 150/172] Table multiple headers and subheaders (#6168) --- crates/typst-layout/src/grid/layouter.rs | 598 +++++++++++------ crates/typst-layout/src/grid/lines.rs | 29 +- crates/typst-layout/src/grid/repeated.rs | 496 +++++++++++++-- crates/typst-layout/src/grid/rowspans.rs | 245 ++++--- crates/typst-library/src/foundations/int.rs | 17 +- crates/typst-library/src/layout/grid/mod.rs | 13 +- .../typst-library/src/layout/grid/resolve.rs | 487 +++++++++----- crates/typst-library/src/model/table.rs | 70 +- crates/typst-syntax/src/span.rs | 13 +- crates/typst-utils/src/lib.rs | 11 +- crates/typst-utils/src/pico.rs | 5 +- ...grid-footer-non-repeatable-unbreakable.png | Bin 0 -> 365 bytes .../grid-footer-repeatable-unbreakable.png | Bin 0 -> 340 bytes .../grid-header-and-large-auto-contiguous.png | Bin 0 -> 894 bytes .../grid-header-and-rowspan-contiguous-1.png | Bin 0 -> 815 bytes .../grid-header-and-rowspan-contiguous-2.png | Bin 0 -> 815 bytes tests/ref/grid-header-multiple.png | Bin 0 -> 214 bytes ...header-non-repeating-orphan-prevention.png | Bin 0 -> 453 bytes ...id-header-not-at-first-row-two-columns.png | Bin 0 -> 176 bytes tests/ref/grid-header-not-at-first-row.png | Bin 0 -> 176 bytes tests/ref/grid-header-not-at-the-top.png | Bin 0 -> 605 bytes tests/ref/grid-header-replace-doesnt-fit.png | Bin 0 -> 559 bytes tests/ref/grid-header-replace-orphan.png | Bin 0 -> 559 bytes tests/ref/grid-header-replace.png | Bin 0 -> 692 bytes tests/ref/grid-header-skip.png | Bin 0 -> 432 bytes ...-header-too-large-non-repeating-orphan.png | Bin 0 -> 372 bytes ...arge-repeating-orphan-not-at-first-row.png | Bin 0 -> 398 bytes ...too-large-repeating-orphan-with-footer.png | Bin 0 -> 576 bytes ...grid-header-too-large-repeating-orphan.png | Bin 0 -> 321 bytes ...-subheaders-alone-no-orphan-prevention.png | Bin 0 -> 254 bytes ...alone-with-footer-no-orphan-prevention.png | Bin 0 -> 378 bytes .../ref/grid-subheaders-alone-with-footer.png | Bin 0 -> 319 bytes ...gutter-and-footer-no-orphan-prevention.png | Bin 0 -> 382 bytes ...alone-with-gutter-no-orphan-prevention.png | Bin 0 -> 254 bytes tests/ref/grid-subheaders-alone.png | Bin 0 -> 256 bytes ...ders-basic-non-consecutive-with-footer.png | Bin 0 -> 279 bytes .../grid-subheaders-basic-non-consecutive.png | Bin 0 -> 256 bytes tests/ref/grid-subheaders-basic-replace.png | Bin 0 -> 321 bytes .../ref/grid-subheaders-basic-with-footer.png | Bin 0 -> 256 bytes tests/ref/grid-subheaders-basic.png | Bin 0 -> 210 bytes tests/ref/grid-subheaders-colorful.png | Bin 0 -> 11005 bytes tests/ref/grid-subheaders-demo.png | Bin 0 -> 5064 bytes ...multi-page-row-right-after-with-footer.png | Bin 0 -> 1207 bytes ...-subheaders-multi-page-row-right-after.png | Bin 0 -> 1127 bytes ...-subheaders-multi-page-row-with-footer.png | Bin 0 -> 1345 bytes tests/ref/grid-subheaders-multi-page-row.png | Bin 0 -> 1173 bytes ...d-subheaders-multi-page-rowspan-gutter.png | Bin 0 -> 1560 bytes ...headers-multi-page-rowspan-right-after.png | Bin 0 -> 1421 bytes ...headers-multi-page-rowspan-with-footer.png | Bin 0 -> 1190 bytes .../grid-subheaders-multi-page-rowspan.png | Bin 0 -> 1048 bytes .../grid-subheaders-non-repeat-replace.png | Bin 0 -> 878 bytes tests/ref/grid-subheaders-non-repeat.png | Bin 0 -> 614 bytes ...repeating-header-before-multi-page-row.png | Bin 0 -> 410 bytes ...eaders-non-repeating-orphan-prevention.png | Bin 0 -> 347 bytes ...s-non-repeating-replace-didnt-fit-once.png | Bin 0 -> 895 bytes ...ubheaders-non-repeating-replace-orphan.png | Bin 0 -> 964 bytes tests/ref/grid-subheaders-repeat-gutter.png | Bin 0 -> 503 bytes ...grid-subheaders-repeat-non-consecutive.png | Bin 0 -> 599 bytes ...bheaders-repeat-replace-didnt-fit-once.png | Bin 0 -> 877 bytes ...ubheaders-repeat-replace-double-orphan.png | Bin 0 -> 950 bytes ...-repeat-replace-gutter-orphan-at-child.png | Bin 0 -> 806 bytes ...repeat-replace-gutter-orphan-at-gutter.png | Bin 0 -> 758 bytes .../grid-subheaders-repeat-replace-gutter.png | Bin 0 -> 782 bytes ...headers-repeat-replace-multiple-levels.png | Bin 0 -> 877 bytes .../grid-subheaders-repeat-replace-orphan.png | Bin 0 -> 939 bytes ...-subheaders-repeat-replace-short-lived.png | Bin 0 -> 795 bytes ...ders-repeat-replace-with-footer-orphan.png | Bin 0 -> 961 bytes ...-subheaders-repeat-replace-with-footer.png | Bin 0 -> 992 bytes tests/ref/grid-subheaders-repeat-replace.png | Bin 0 -> 953 bytes ...aders-repeat-short-lived-also-replaces.png | Bin 0 -> 899 bytes .../grid-subheaders-repeat-with-footer.png | Bin 0 -> 584 bytes tests/ref/grid-subheaders-repeat.png | Bin 0 -> 472 bytes ...subheaders-repeating-orphan-prevention.png | Bin 0 -> 347 bytes ...aders-short-lived-no-orphan-prevention.png | Bin 0 -> 287 bytes ...large-non-repeating-orphan-before-auto.png | Bin 0 -> 460 bytes ...e-non-repeating-orphan-before-relative.png | Bin 0 -> 542 bytes ...too-large-repeating-orphan-before-auto.png | Bin 0 -> 525 bytes ...large-repeating-orphan-before-relative.png | Bin 0 -> 437 bytes tests/ref/html/multi-header-inside-table.html | 69 ++ tests/ref/html/multi-header-table.html | 49 ++ ...59-column-override-stays-inside-header.png | Bin 0 -> 674 bytes tests/suite/layout/grid/footers.typ | 23 + tests/suite/layout/grid/headers.typ | 171 ++++- tests/suite/layout/grid/html.typ | 75 +++ tests/suite/layout/grid/subheaders.typ | 602 ++++++++++++++++++ 85 files changed, 2453 insertions(+), 520 deletions(-) create mode 100644 tests/ref/grid-footer-non-repeatable-unbreakable.png create mode 100644 tests/ref/grid-footer-repeatable-unbreakable.png create mode 100644 tests/ref/grid-header-and-large-auto-contiguous.png create mode 100644 tests/ref/grid-header-and-rowspan-contiguous-1.png create mode 100644 tests/ref/grid-header-and-rowspan-contiguous-2.png create mode 100644 tests/ref/grid-header-multiple.png create mode 100644 tests/ref/grid-header-non-repeating-orphan-prevention.png create mode 100644 tests/ref/grid-header-not-at-first-row-two-columns.png create mode 100644 tests/ref/grid-header-not-at-first-row.png create mode 100644 tests/ref/grid-header-not-at-the-top.png create mode 100644 tests/ref/grid-header-replace-doesnt-fit.png create mode 100644 tests/ref/grid-header-replace-orphan.png create mode 100644 tests/ref/grid-header-replace.png create mode 100644 tests/ref/grid-header-skip.png create mode 100644 tests/ref/grid-header-too-large-non-repeating-orphan.png create mode 100644 tests/ref/grid-header-too-large-repeating-orphan-not-at-first-row.png create mode 100644 tests/ref/grid-header-too-large-repeating-orphan-with-footer.png create mode 100644 tests/ref/grid-header-too-large-repeating-orphan.png create mode 100644 tests/ref/grid-subheaders-alone-no-orphan-prevention.png create mode 100644 tests/ref/grid-subheaders-alone-with-footer-no-orphan-prevention.png create mode 100644 tests/ref/grid-subheaders-alone-with-footer.png create mode 100644 tests/ref/grid-subheaders-alone-with-gutter-and-footer-no-orphan-prevention.png create mode 100644 tests/ref/grid-subheaders-alone-with-gutter-no-orphan-prevention.png create mode 100644 tests/ref/grid-subheaders-alone.png create mode 100644 tests/ref/grid-subheaders-basic-non-consecutive-with-footer.png create mode 100644 tests/ref/grid-subheaders-basic-non-consecutive.png create mode 100644 tests/ref/grid-subheaders-basic-replace.png create mode 100644 tests/ref/grid-subheaders-basic-with-footer.png create mode 100644 tests/ref/grid-subheaders-basic.png create mode 100644 tests/ref/grid-subheaders-colorful.png create mode 100644 tests/ref/grid-subheaders-demo.png create mode 100644 tests/ref/grid-subheaders-multi-page-row-right-after-with-footer.png create mode 100644 tests/ref/grid-subheaders-multi-page-row-right-after.png create mode 100644 tests/ref/grid-subheaders-multi-page-row-with-footer.png create mode 100644 tests/ref/grid-subheaders-multi-page-row.png create mode 100644 tests/ref/grid-subheaders-multi-page-rowspan-gutter.png create mode 100644 tests/ref/grid-subheaders-multi-page-rowspan-right-after.png create mode 100644 tests/ref/grid-subheaders-multi-page-rowspan-with-footer.png create mode 100644 tests/ref/grid-subheaders-multi-page-rowspan.png create mode 100644 tests/ref/grid-subheaders-non-repeat-replace.png create mode 100644 tests/ref/grid-subheaders-non-repeat.png create mode 100644 tests/ref/grid-subheaders-non-repeating-header-before-multi-page-row.png create mode 100644 tests/ref/grid-subheaders-non-repeating-orphan-prevention.png create mode 100644 tests/ref/grid-subheaders-non-repeating-replace-didnt-fit-once.png create mode 100644 tests/ref/grid-subheaders-non-repeating-replace-orphan.png create mode 100644 tests/ref/grid-subheaders-repeat-gutter.png create mode 100644 tests/ref/grid-subheaders-repeat-non-consecutive.png create mode 100644 tests/ref/grid-subheaders-repeat-replace-didnt-fit-once.png create mode 100644 tests/ref/grid-subheaders-repeat-replace-double-orphan.png create mode 100644 tests/ref/grid-subheaders-repeat-replace-gutter-orphan-at-child.png create mode 100644 tests/ref/grid-subheaders-repeat-replace-gutter-orphan-at-gutter.png create mode 100644 tests/ref/grid-subheaders-repeat-replace-gutter.png create mode 100644 tests/ref/grid-subheaders-repeat-replace-multiple-levels.png create mode 100644 tests/ref/grid-subheaders-repeat-replace-orphan.png create mode 100644 tests/ref/grid-subheaders-repeat-replace-short-lived.png create mode 100644 tests/ref/grid-subheaders-repeat-replace-with-footer-orphan.png create mode 100644 tests/ref/grid-subheaders-repeat-replace-with-footer.png create mode 100644 tests/ref/grid-subheaders-repeat-replace.png create mode 100644 tests/ref/grid-subheaders-repeat-short-lived-also-replaces.png create mode 100644 tests/ref/grid-subheaders-repeat-with-footer.png create mode 100644 tests/ref/grid-subheaders-repeat.png create mode 100644 tests/ref/grid-subheaders-repeating-orphan-prevention.png create mode 100644 tests/ref/grid-subheaders-short-lived-no-orphan-prevention.png create mode 100644 tests/ref/grid-subheaders-too-large-non-repeating-orphan-before-auto.png create mode 100644 tests/ref/grid-subheaders-too-large-non-repeating-orphan-before-relative.png create mode 100644 tests/ref/grid-subheaders-too-large-repeating-orphan-before-auto.png create mode 100644 tests/ref/grid-subheaders-too-large-repeating-orphan-before-relative.png create mode 100644 tests/ref/html/multi-header-inside-table.html create mode 100644 tests/ref/html/multi-header-table.html create mode 100644 tests/ref/issue-5359-column-override-stays-inside-header.png create mode 100644 tests/suite/layout/grid/subheaders.typ diff --git a/crates/typst-layout/src/grid/layouter.rs b/crates/typst-layout/src/grid/layouter.rs index 99b85eddb..42fe38dbe 100644 --- a/crates/typst-layout/src/grid/layouter.rs +++ b/crates/typst-layout/src/grid/layouter.rs @@ -3,7 +3,9 @@ use std::fmt::Debug; use typst_library::diag::{bail, SourceResult}; use typst_library::engine::Engine; use typst_library::foundations::{Resolve, StyleChain}; -use typst_library::layout::grid::resolve::{Cell, CellGrid, LinePosition, Repeatable}; +use typst_library::layout::grid::resolve::{ + Cell, CellGrid, Header, LinePosition, Repeatable, +}; use typst_library::layout::{ Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel, Size, Sizing, @@ -30,10 +32,8 @@ pub struct GridLayouter<'a> { pub(super) rcols: Vec, /// The sum of `rcols`. pub(super) width: Abs, - /// Resolve row sizes, by region. + /// Resolved row sizes, by region. pub(super) rrows: Vec>, - /// Rows in the current region. - pub(super) lrows: Vec, /// The amount of unbreakable rows remaining to be laid out in the /// current unbreakable row group. While this is positive, no region breaks /// should occur. @@ -41,24 +41,155 @@ pub struct GridLayouter<'a> { /// Rowspans not yet laid out because not all of their spanned rows were /// laid out yet. pub(super) rowspans: Vec, - /// The initial size of the current region before we started subtracting. - pub(super) initial: Size, + /// Grid layout state for the current region. + pub(super) current: Current, /// Frames for finished regions. pub(super) finished: Vec, + /// The amount and height of header rows on each finished region. + pub(super) finished_header_rows: Vec, /// Whether this is an RTL grid. pub(super) is_rtl: bool, - /// The simulated header height. - /// This field is reset in `layout_header` and properly updated by + /// Currently repeating headers, one per level. Sorted by increasing + /// levels. + /// + /// Note that some levels may be absent, in particular level 0, which does + /// not exist (so all levels are >= 1). + pub(super) repeating_headers: Vec<&'a Header>, + /// Headers, repeating or not, awaiting their first successful layout. + /// Sorted by increasing levels. + pub(super) pending_headers: &'a [Repeatable

], + /// Next headers to be processed. + pub(super) upcoming_headers: &'a [Repeatable
], + /// State of the row being currently laid out. + /// + /// This is kept as a field to avoid passing down too many parameters from + /// `layout_row` into called functions, which would then have to pass them + /// down to `push_row`, which reads these values. + pub(super) row_state: RowState, + /// The span of the grid element. + pub(super) span: Span, +} + +/// Grid layout state for the current region. This should be reset or updated +/// on each region break. +pub(super) struct Current { + /// The initial size of the current region before we started subtracting. + pub(super) initial: Size, + /// The height of the region after repeated headers were placed and footers + /// prepared. This also includes pending repeating headers from the start, + /// even if they were not repeated yet, since they will be repeated in the + /// next region anyway (bar orphan prevention). + /// + /// This is used to quickly tell if any additional space in the region has + /// been occupied since then, meaning that additional space will become + /// available after a region break (see + /// [`GridLayouter::may_progress_with_repeats`]). + pub(super) initial_after_repeats: Abs, + /// Whether `layouter.regions.may_progress()` was `true` at the top of the + /// region. + pub(super) could_progress_at_top: bool, + /// Rows in the current region. + pub(super) lrows: Vec, + /// The amount of repeated header rows at the start of the current region. + /// Thus, excludes rows from pending headers (which were placed for the + /// first time). + /// + /// Note that `repeating_headers` and `pending_headers` can change if we + /// find a new header inside the region (not at the top), so this field + /// is required to access information from the top of the region. + /// + /// This information is used on finish region to calculate the total height + /// of resolved header rows at the top of the region, which is used by + /// multi-page rowspans so they can properly skip the header rows at the + /// top of each region during layout. + pub(super) repeated_header_rows: usize, + /// The end bound of the row range of the last repeating header at the + /// start of the region. + /// + /// The last row might have disappeared from layout due to being empty, so + /// this is how we can become aware of where the last header ends without + /// having to check the vector of rows. Line layout uses this to determine + /// when to prioritize the last lines under a header. + /// + /// A value of zero indicates no repeated headers were placed. + pub(super) last_repeated_header_end: usize, + /// Stores the length of `lrows` before a sequence of rows equipped with + /// orphan prevention was laid out. In this case, if no more rows without + /// orphan prevention are laid out after those rows before the region ends, + /// the rows will be removed, and there may be an attempt to place them + /// again in the new region. Effectively, this is the mechanism used for + /// orphan prevention of rows. + /// + /// At the moment, this is only used by repeated headers (they aren't laid + /// out if alone in the region) and by new headers, which are moved to the + /// `pending_headers` vector and so will automatically be placed again + /// until they fit and are not orphans in at least one region (or exactly + /// one, for non-repeated headers). + pub(super) lrows_orphan_snapshot: Option, + /// The height of effectively repeating headers, that is, ignoring + /// non-repeating pending headers, in the current region. + /// + /// This is used by multi-page auto rows so they can inform cell layout on + /// how much space should be taken by headers if they break across regions. + /// In particular, non-repeating headers only occupy the initial region, + /// but disappear on new regions, so they can be ignored. + /// + /// This field is reset on each new region and properly updated by /// `layout_auto_row` and `layout_relative_row`, and should not be read /// before all header rows are fully laid out. It is usually fine because /// header rows themselves are unbreakable, and unbreakable rows do not /// need to read this field at all. - pub(super) header_height: Abs, + /// + /// This height is not only computed at the beginning of the region. It is + /// updated whenever a new header is found, subtracting the height of + /// headers which stopped repeating and adding the height of all new + /// headers. + pub(super) repeating_header_height: Abs, + /// The height for each repeating header that was placed in this region. + /// Note that this includes headers not at the top of the region, before + /// their first repetition (pending headers), and excludes headers removed + /// by virtue of a new, conflicting header being found (short-lived + /// headers). + /// + /// This is used to know how much to update `repeating_header_height` by + /// when finding a new header and causing existing repeating headers to + /// stop. + pub(super) repeating_header_heights: Vec, /// The simulated footer height for this region. + /// /// The simulation occurs before any rows are laid out for a region. pub(super) footer_height: Abs, - /// The span of the grid element. - pub(super) span: Span, +} + +/// Data about the row being laid out right now. +#[derive(Debug, Default)] +pub(super) struct RowState { + /// If this is `Some`, this will be updated by the currently laid out row's + /// height if it is auto or relative. This is used for header height + /// calculation. + pub(super) current_row_height: Option, + /// This is `true` when laying out non-short lived headers and footers. + /// That is, headers and footers which are not immediately followed or + /// preceded (respectively) by conflicting headers and footers of same or + /// lower level, or the end or start of the table (respectively), which + /// would cause them to never repeat, even once. + /// + /// If this is `false`, the next row to be laid out will remove an active + /// orphan snapshot and will flush pending headers, as there is no risk + /// that they will be orphans anymore. + pub(super) in_active_repeatable: bool, +} + +/// Data about laid out repeated header rows for a specific finished region. +#[derive(Debug, Default)] +pub(super) struct FinishedHeaderRowInfo { + /// The amount of repeated headers at the top of the region. + pub(super) repeated_amount: usize, + /// The end bound of the row range of the last repeated header at the top + /// of the region. + pub(super) last_repeated_header_end: usize, + /// The total height of repeated headers at the top of the region. + pub(super) repeated_height: Abs, } /// Details about a resulting row piece. @@ -114,14 +245,27 @@ impl<'a> GridLayouter<'a> { rcols: vec![Abs::zero(); grid.cols.len()], width: Abs::zero(), rrows: vec![], - lrows: vec![], unbreakable_rows_left: 0, rowspans: vec![], - initial: regions.size, finished: vec![], + finished_header_rows: vec![], is_rtl: TextElem::dir_in(styles) == Dir::RTL, - header_height: Abs::zero(), - footer_height: Abs::zero(), + repeating_headers: vec![], + upcoming_headers: &grid.headers, + pending_headers: Default::default(), + row_state: RowState::default(), + current: Current { + initial: regions.size, + initial_after_repeats: regions.size.y, + could_progress_at_top: regions.may_progress(), + lrows: vec![], + repeated_header_rows: 0, + last_repeated_header_end: 0, + lrows_orphan_snapshot: None, + repeating_header_height: Abs::zero(), + repeating_header_heights: vec![], + footer_height: Abs::zero(), + }, span, } } @@ -130,38 +274,57 @@ impl<'a> GridLayouter<'a> { pub fn layout(mut self, engine: &mut Engine) -> SourceResult { self.measure_columns(engine)?; - if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { - // Ensure rows in the first region will be aware of the possible - // presence of the footer. - self.prepare_footer(footer, engine, 0)?; - if matches!(self.grid.header, None | Some(Repeatable::NotRepeated(_))) { - // No repeatable header, so we won't subtract it later. - self.regions.size.y -= self.footer_height; + if let Some(footer) = &self.grid.footer { + if footer.repeated { + // Ensure rows in the first region will be aware of the + // possible presence of the footer. + self.prepare_footer(footer, engine, 0)?; + self.regions.size.y -= self.current.footer_height; + self.current.initial_after_repeats = self.regions.size.y; } } - for y in 0..self.grid.rows.len() { - if let Some(Repeatable::Repeated(header)) = &self.grid.header { - if y < header.end { - if y == 0 { - self.layout_header(header, engine, 0)?; - self.regions.size.y -= self.footer_height; - } + let mut y = 0; + let mut consecutive_header_count = 0; + while y < self.grid.rows.len() { + if let Some(next_header) = self.upcoming_headers.get(consecutive_header_count) + { + if next_header.range.contains(&y) { + self.place_new_headers(&mut consecutive_header_count, engine)?; + y = next_header.range.end; + // Skip header rows during normal layout. continue; } } - if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { - if y >= footer.start { + if let Some(footer) = &self.grid.footer { + if footer.repeated && y >= footer.start { if y == footer.start { self.layout_footer(footer, engine, self.finished.len())?; + self.flush_orphans(); } + y = footer.end; continue; } } self.layout_row(y, engine, 0)?; + + // After the first non-header row is placed, pending headers are no + // longer orphans and can repeat, so we move them to repeating + // headers. + // + // Note that this is usually done in `push_row`, since the call to + // `layout_row` above might trigger region breaks (for multi-page + // auto rows), whereas this needs to be called as soon as any part + // of a row is laid out. However, it's possible a row has no + // visible output and thus does not push any rows even though it + // was successfully laid out, in which case we additionally flush + // here just in case. + self.flush_orphans(); + + y += 1; } self.finish_region(engine, true)?; @@ -184,12 +347,46 @@ impl<'a> GridLayouter<'a> { self.render_fills_strokes() } - /// Layout the given row. + /// Layout a row with a certain initial state, returning the final state. + #[inline] + pub(super) fn layout_row_with_state( + &mut self, + y: usize, + engine: &mut Engine, + disambiguator: usize, + initial_state: RowState, + ) -> SourceResult { + // Keep a copy of the previous value in the stack, as this function can + // call itself recursively (e.g. if a region break is triggered and a + // header is placed), so we shouldn't outright overwrite it, but rather + // save and later restore the state when back to this call. + let previous = std::mem::replace(&mut self.row_state, initial_state); + + // Keep it as a separate function to allow inlining the return below, + // as it's usually not needed. + self.layout_row_internal(y, engine, disambiguator)?; + + Ok(std::mem::replace(&mut self.row_state, previous)) + } + + /// Layout the given row with the default row state. + #[inline] pub(super) fn layout_row( &mut self, y: usize, engine: &mut Engine, disambiguator: usize, + ) -> SourceResult<()> { + self.layout_row_with_state(y, engine, disambiguator, RowState::default())?; + Ok(()) + } + + /// Layout the given row using the current state. + pub(super) fn layout_row_internal( + &mut self, + y: usize, + engine: &mut Engine, + disambiguator: usize, ) -> SourceResult<()> { // Skip to next region if current one is full, but only for content // rows, not for gutter rows, and only if we aren't laying out an @@ -206,13 +403,18 @@ impl<'a> GridLayouter<'a> { } // Don't layout gutter rows at the top of a region. - if is_content_row || !self.lrows.is_empty() { + if is_content_row || !self.current.lrows.is_empty() { match self.grid.rows[y] { Sizing::Auto => self.layout_auto_row(engine, disambiguator, y)?, Sizing::Rel(v) => { self.layout_relative_row(engine, disambiguator, v, y)? } - Sizing::Fr(v) => self.lrows.push(Row::Fr(v, y, disambiguator)), + Sizing::Fr(v) => { + if !self.row_state.in_active_repeatable { + self.flush_orphans(); + } + self.current.lrows.push(Row::Fr(v, y, disambiguator)) + } } } @@ -225,8 +427,13 @@ impl<'a> GridLayouter<'a> { fn render_fills_strokes(mut self) -> SourceResult { let mut finished = std::mem::take(&mut self.finished); let frame_amount = finished.len(); - for ((frame_index, frame), rows) in - finished.iter_mut().enumerate().zip(&self.rrows) + for (((frame_index, frame), rows), finished_header_rows) in + finished.iter_mut().enumerate().zip(&self.rrows).zip( + self.finished_header_rows + .iter() + .map(Some) + .chain(std::iter::repeat(None)), + ) { if self.rcols.is_empty() || rows.is_empty() { continue; @@ -347,7 +554,8 @@ impl<'a> GridLayouter<'a> { let hline_indices = rows .iter() .map(|piece| piece.y) - .chain(std::iter::once(self.grid.rows.len())); + .chain(std::iter::once(self.grid.rows.len())) + .enumerate(); // Converts a row to the corresponding index in the vector of // hlines. @@ -372,7 +580,7 @@ impl<'a> GridLayouter<'a> { }; let mut prev_y = None; - for (y, dy) in hline_indices.zip(hline_offsets) { + for ((i, y), dy) in hline_indices.zip(hline_offsets) { // Position of lines below the row index in the previous iteration. let expected_prev_line_position = prev_y .map(|prev_y| { @@ -383,47 +591,40 @@ impl<'a> GridLayouter<'a> { }) .unwrap_or(LinePosition::Before); - // FIXME: In the future, directly specify in 'self.rrows' when - // we place a repeated header rather than its original rows. - // That would let us remove most of those verbose checks, both - // in 'lines.rs' and here. Those checks also aren't fully - // accurate either, since they will also trigger when some rows - // have been removed between the header and what's below it. - let is_under_repeated_header = self - .grid - .header - .as_ref() - .and_then(Repeatable::as_repeated) - .zip(prev_y) - .is_some_and(|(header, prev_y)| { - // Note: 'y == header.end' would mean we're right below - // the NON-REPEATED header, so that case should return - // false. - prev_y < header.end && y > header.end - }); + // Header's lines at the bottom have priority when repeated. + // This will store the end bound of the last header if the + // current iteration is calculating lines under it. + let last_repeated_header_end_above = match finished_header_rows { + Some(info) if prev_y.is_some() && i == info.repeated_amount => { + Some(info.last_repeated_header_end) + } + _ => None, + }; // If some grid rows were omitted between the previous resolved // row and the current one, we ensure lines below the previous // row don't "disappear" and are considered, albeit with less // priority. However, don't do this when we're below a header, // as it must have more priority instead of less, so it is - // chained later instead of before. The exception is when the + // chained later instead of before (stored in the + // 'header_hlines' variable below). The exception is when the // last row in the header is removed, in which case we append // both the lines under the row above us and also (later) the // lines under the header's (removed) last row. - let prev_lines = prev_y - .filter(|prev_y| { - prev_y + 1 != y - && (!is_under_repeated_header - || self - .grid - .header - .as_ref() - .and_then(Repeatable::as_repeated) - .is_some_and(|header| prev_y + 1 != header.end)) - }) - .map(|prev_y| get_hlines_at(prev_y + 1)) - .unwrap_or(&[]); + let prev_lines = match prev_y { + Some(prev_y) + if prev_y + 1 != y + && last_repeated_header_end_above.is_none_or( + |last_repeated_header_end| { + prev_y + 1 != last_repeated_header_end + }, + ) => + { + get_hlines_at(prev_y + 1) + } + + _ => &[], + }; let expected_hline_position = expected_line_position(y, y == self.grid.rows.len()); @@ -441,15 +642,13 @@ impl<'a> GridLayouter<'a> { }; let mut expected_header_line_position = LinePosition::Before; - let header_hlines = if let Some((Repeatable::Repeated(header), prev_y)) = - self.grid.header.as_ref().zip(prev_y) - { - if is_under_repeated_header - && (!self.grid.has_gutter + let header_hlines = match (last_repeated_header_end_above, prev_y) { + (Some(header_end_above), Some(prev_y)) + if !self.grid.has_gutter || matches!( self.grid.rows[prev_y], Sizing::Rel(length) if length.is_zero() - )) + ) => { // For lines below a header, give priority to the // lines originally below the header rather than @@ -468,15 +667,13 @@ impl<'a> GridLayouter<'a> { // column-gutter is specified, for example. In that // case, we still repeat the line under the gutter. expected_header_line_position = expected_line_position( - header.end, - header.end == self.grid.rows.len(), + header_end_above, + header_end_above == self.grid.rows.len(), ); - get_hlines_at(header.end) - } else { - &[] + get_hlines_at(header_end_above) } - } else { - &[] + + _ => &[], }; // The effective hlines to be considered at this row index are @@ -529,6 +726,7 @@ impl<'a> GridLayouter<'a> { grid, rows, local_top_y, + last_repeated_header_end_above, in_last_region, y, x, @@ -941,15 +1139,9 @@ impl<'a> GridLayouter<'a> { let frame = self.layout_single_row(engine, disambiguator, first, y)?; self.push_row(frame, y, true); - if self - .grid - .header - .as_ref() - .and_then(Repeatable::as_repeated) - .is_some_and(|header| y < header.end) - { - // Add to header height. - self.header_height += first; + if let Some(row_height) = &mut self.row_state.current_row_height { + // Add to header height, as we are in a header row. + *row_height += first; } return Ok(()); @@ -958,19 +1150,21 @@ impl<'a> GridLayouter<'a> { // Expand all but the last region. // Skip the first region if the space is eaten up by an fr row. let len = resolved.len(); - for ((i, region), target) in self - .regions - .iter() - .enumerate() - .zip(&mut resolved[..len - 1]) - .skip(self.lrows.iter().any(|row| matches!(row, Row::Fr(..))) as usize) + for ((i, region), target) in + self.regions + .iter() + .enumerate() + .zip(&mut resolved[..len - 1]) + .skip(self.current.lrows.iter().any(|row| matches!(row, Row::Fr(..))) + as usize) { // Subtract header and footer heights from the region height when - // it's not the first. + // it's not the first. Ignore non-repeating headers as they only + // appear on the first region by definition. target.set_max( region.y - if i > 0 { - self.header_height + self.footer_height + self.current.repeating_header_height + self.current.footer_height } else { Abs::zero() }, @@ -1181,25 +1375,19 @@ impl<'a> GridLayouter<'a> { let resolved = v.resolve(self.styles).relative_to(self.regions.base().y); let frame = self.layout_single_row(engine, disambiguator, resolved, y)?; - if self - .grid - .header - .as_ref() - .and_then(Repeatable::as_repeated) - .is_some_and(|header| y < header.end) - { - // Add to header height. - self.header_height += resolved; + if let Some(row_height) = &mut self.row_state.current_row_height { + // Add to header height, as we are in a header row. + *row_height += resolved; } // Skip to fitting region, but only if we aren't part of an unbreakable - // row group. We use 'in_last_with_offset' so our 'in_last' call - // properly considers that a header and a footer would be added on each - // region break. + // row group. We use 'may_progress_with_repeats' to stop trying if we + // would skip to a region with the same height and where the same + // headers would be repeated. let height = frame.height(); while self.unbreakable_rows_left == 0 && !self.regions.size.y.fits(height) - && !in_last_with_offset(self.regions, self.header_height + self.footer_height) + && self.may_progress_with_repeats() { self.finish_region(engine, false)?; @@ -1323,8 +1511,13 @@ impl<'a> GridLayouter<'a> { /// will be pushed for this particular row. It can be `false` for rows /// spanning multiple regions. fn push_row(&mut self, frame: Frame, y: usize, is_last: bool) { + if !self.row_state.in_active_repeatable { + // There is now a row after the rows equipped with orphan + // prevention, so no need to keep moving them anymore. + self.flush_orphans(); + } self.regions.size.y -= frame.height(); - self.lrows.push(Row::Frame(frame, y, is_last)); + self.current.lrows.push(Row::Frame(frame, y, is_last)); } /// Finish rows for one region. @@ -1333,68 +1526,73 @@ impl<'a> GridLayouter<'a> { engine: &mut Engine, last: bool, ) -> SourceResult<()> { + // The latest rows have orphan prevention (headers) and no other rows + // were placed, so remove those rows and try again in a new region, + // unless this is the last region. + if let Some(orphan_snapshot) = self.current.lrows_orphan_snapshot.take() { + if !last { + self.current.lrows.truncate(orphan_snapshot); + self.current.repeated_header_rows = + self.current.repeated_header_rows.min(orphan_snapshot); + + if orphan_snapshot == 0 { + // Removed all repeated headers. + self.current.last_repeated_header_end = 0; + } + } + } + if self + .current .lrows .last() .is_some_and(|row| self.grid.is_gutter_track(row.index())) { // Remove the last row in the region if it is a gutter row. - self.lrows.pop().unwrap(); + self.current.lrows.pop().unwrap(); + self.current.repeated_header_rows = + self.current.repeated_header_rows.min(self.current.lrows.len()); } - // If no rows other than the footer have been laid out so far, and - // there are rows beside the footer, then don't lay it out at all. - // This check doesn't apply, and is thus overridden, when there is a - // header. - let mut footer_would_be_orphan = self.lrows.is_empty() - && !in_last_with_offset( - self.regions, - self.header_height + self.footer_height, - ) - && self - .grid - .footer - .as_ref() - .and_then(Repeatable::as_repeated) - .is_some_and(|footer| footer.start != 0); - - if let Some(Repeatable::Repeated(header)) = &self.grid.header { - if self.grid.rows.len() > header.end - && self - .grid - .footer - .as_ref() - .and_then(Repeatable::as_repeated) - .is_none_or(|footer| footer.start != header.end) - && self.lrows.last().is_some_and(|row| row.index() < header.end) - && !in_last_with_offset( - self.regions, - self.header_height + self.footer_height, - ) - { - // Header and footer would be alone in this region, but there are more - // rows beyond the header and the footer. Push an empty region. - self.lrows.clear(); - footer_would_be_orphan = true; - } - } + // If no rows other than the footer have been laid out so far + // (e.g. due to header orphan prevention), and there are rows + // beside the footer, then don't lay it out at all. + // + // It is worth noting that the footer is made non-repeatable at + // the grid resolving stage if it is short-lived, that is, if + // it is at the start of the table (or right after headers at + // the start of the table). + // + // TODO(subfooters): explicitly check for short-lived footers. + // TODO(subfooters): widow prevention for non-repeated footers with a + // similar mechanism / when implementing multiple footers. + let footer_would_be_widow = matches!(&self.grid.footer, Some(footer) if footer.repeated) + && self.current.lrows.is_empty() + && self.current.could_progress_at_top; let mut laid_out_footer_start = None; - if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { - // Don't layout the footer if it would be alone with the header in - // the page, and don't layout it twice. - if !footer_would_be_orphan - && self.lrows.iter().all(|row| row.index() < footer.start) - { - laid_out_footer_start = Some(footer.start); - self.layout_footer(footer, engine, self.finished.len())?; + if !footer_would_be_widow { + if let Some(footer) = &self.grid.footer { + // Don't layout the footer if it would be alone with the header + // in the page (hence the widow check), and don't layout it + // twice (check below). + // + // TODO(subfooters): this check can be replaced by a vector of + // repeating footers in the future, and/or some "pending + // footers" vector for footers we're about to place. + if footer.repeated + && self.current.lrows.iter().all(|row| row.index() < footer.start) + { + laid_out_footer_start = Some(footer.start); + self.layout_footer(footer, engine, self.finished.len())?; + } } } // Determine the height of existing rows in the region. let mut used = Abs::zero(); let mut fr = Fr::zero(); - for row in &self.lrows { + for row in &self.current.lrows { match row { Row::Frame(frame, _, _) => used += frame.height(), Row::Fr(v, _, _) => fr += *v, @@ -1403,9 +1601,9 @@ impl<'a> GridLayouter<'a> { // Determine the size of the grid in this region, expanding fully if // there are fr rows. - let mut size = Size::new(self.width, used).min(self.initial); - if fr.get() > 0.0 && self.initial.y.is_finite() { - size.y = self.initial.y; + let mut size = Size::new(self.width, used).min(self.current.initial); + if fr.get() > 0.0 && self.current.initial.y.is_finite() { + size.y = self.current.initial.y; } // The frame for the region. @@ -1413,9 +1611,10 @@ impl<'a> GridLayouter<'a> { let mut pos = Point::zero(); let mut rrows = vec![]; let current_region = self.finished.len(); + let mut repeated_header_row_height = Abs::zero(); // Place finished rows and layout fractional rows. - for row in std::mem::take(&mut self.lrows) { + for (i, row) in std::mem::take(&mut self.current.lrows).into_iter().enumerate() { let (frame, y, is_last) = match row { Row::Frame(frame, y, is_last) => (frame, y, is_last), Row::Fr(v, y, disambiguator) => { @@ -1426,6 +1625,9 @@ impl<'a> GridLayouter<'a> { }; let height = frame.height(); + if i < self.current.repeated_header_rows { + repeated_header_row_height += height; + } // Ensure rowspans which span this row will have enough space to // be laid out over it later. @@ -1504,7 +1706,11 @@ impl<'a> GridLayouter<'a> { // we have to check the same index again in the next // iteration. let rowspan = self.rowspans.remove(i); - self.layout_rowspan(rowspan, Some((&mut output, &rrows)), engine)?; + self.layout_rowspan( + rowspan, + Some((&mut output, repeated_header_row_height)), + engine, + )?; } else { i += 1; } @@ -1515,21 +1721,40 @@ impl<'a> GridLayouter<'a> { pos.y += height; } - self.finish_region_internal(output, rrows); + self.finish_region_internal( + output, + rrows, + FinishedHeaderRowInfo { + repeated_amount: self.current.repeated_header_rows, + last_repeated_header_end: self.current.last_repeated_header_end, + repeated_height: repeated_header_row_height, + }, + ); if !last { + self.current.repeated_header_rows = 0; + self.current.last_repeated_header_end = 0; + self.current.repeating_header_height = Abs::zero(); + self.current.repeating_header_heights.clear(); + let disambiguator = self.finished.len(); - if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { + if let Some(footer) = + self.grid.footer.as_ref().and_then(Repeatable::as_repeated) + { self.prepare_footer(footer, engine, disambiguator)?; } - if let Some(Repeatable::Repeated(header)) = &self.grid.header { - // Add a header to the new region. - self.layout_header(header, engine, disambiguator)?; - } - // Ensure rows don't try to overrun the footer. - self.regions.size.y -= self.footer_height; + // Note that header layout will only subtract this again if it has + // to skip regions to fit headers, so there is no risk of + // subtracting this twice. + self.regions.size.y -= self.current.footer_height; + self.current.initial_after_repeats = self.regions.size.y; + + if !self.repeating_headers.is_empty() || !self.pending_headers.is_empty() { + // Add headers to the new region. + self.layout_active_headers(engine)?; + } } Ok(()) @@ -1541,11 +1766,26 @@ impl<'a> GridLayouter<'a> { &mut self, output: Frame, resolved_rows: Vec, + header_row_info: FinishedHeaderRowInfo, ) { self.finished.push(output); self.rrows.push(resolved_rows); self.regions.next(); - self.initial = self.regions.size; + self.current.initial = self.regions.size; + + // Repeats haven't been laid out yet, so in the meantime, this will + // represent the initial height after repeats laid out so far, and will + // be gradually updated when preparing footers and repeating headers. + self.current.initial_after_repeats = self.current.initial.y; + + self.current.could_progress_at_top = self.regions.may_progress(); + + if !self.grid.headers.is_empty() { + self.finished_header_rows.push(header_row_info); + } + + // Ensure orphan prevention is handled before resolving rows. + debug_assert!(self.current.lrows_orphan_snapshot.is_none()); } } @@ -1560,13 +1800,3 @@ pub(super) fn points( offset }) } - -/// Checks if the first region of a sequence of regions is the last usable -/// region, assuming that the last region will always be occupied by some -/// specific offset height, even after calling `.next()`, due to some -/// additional logic which adds content automatically on each region turn (in -/// our case, headers). -pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool { - regions.backlog.is_empty() - && regions.last.is_none_or(|height| regions.size.y + offset == height) -} diff --git a/crates/typst-layout/src/grid/lines.rs b/crates/typst-layout/src/grid/lines.rs index 7549673f1..d5da7e263 100644 --- a/crates/typst-layout/src/grid/lines.rs +++ b/crates/typst-layout/src/grid/lines.rs @@ -391,10 +391,12 @@ pub fn vline_stroke_at_row( /// /// This function assumes columns are sorted by increasing `x`, and rows are /// sorted by increasing `y`. +#[allow(clippy::too_many_arguments)] pub fn hline_stroke_at_column( grid: &CellGrid, rows: &[RowPiece], local_top_y: Option, + header_end_above: Option, in_last_region: bool, y: usize, x: usize, @@ -499,17 +501,15 @@ pub fn hline_stroke_at_column( // Top border stroke and header stroke are generally prioritized, unless // they don't have explicit hline overrides and one or more user-provided // hlines would appear at the same position, which then are prioritized. - let top_stroke_comes_from_header = grid - .header - .as_ref() - .and_then(Repeatable::as_repeated) - .zip(local_top_y) - .is_some_and(|(header, local_top_y)| { - // Ensure the row above us is a repeated header. - // FIXME: Make this check more robust when headers at arbitrary - // positions are added. - local_top_y < header.end && y > header.end - }); + let top_stroke_comes_from_header = header_end_above.zip(local_top_y).is_some_and( + |(last_repeated_header_end, local_top_y)| { + // Check if the last repeated header row is above this line. + // + // Note that `y == last_repeated_header_end` is impossible for a + // strictly repeated header (not in its original position). + local_top_y < last_repeated_header_end && y > last_repeated_header_end + }, + ); // Prioritize the footer's top stroke as well where applicable. let bottom_stroke_comes_from_footer = grid @@ -637,7 +637,7 @@ mod test { }, vec![], vec![], - None, + vec![], None, entries, ) @@ -1175,7 +1175,7 @@ mod test { }, vec![], vec![], - None, + vec![], None, entries, ) @@ -1268,6 +1268,7 @@ mod test { grid, &rows, y.checked_sub(1), + None, true, y, x, @@ -1461,6 +1462,7 @@ mod test { grid, &rows, y.checked_sub(1), + None, true, y, x, @@ -1506,6 +1508,7 @@ mod test { grid, &rows, if y == 4 { Some(2) } else { y.checked_sub(1) }, + None, true, y, x, diff --git a/crates/typst-layout/src/grid/repeated.rs b/crates/typst-layout/src/grid/repeated.rs index 22d2a09ef..8db33df5e 100644 --- a/crates/typst-layout/src/grid/repeated.rs +++ b/crates/typst-layout/src/grid/repeated.rs @@ -1,57 +1,446 @@ +use std::ops::Deref; + use typst_library::diag::SourceResult; use typst_library::engine::Engine; use typst_library::layout::grid::resolve::{Footer, Header, Repeatable}; use typst_library::layout::{Abs, Axes, Frame, Regions}; -use super::layouter::GridLayouter; +use super::layouter::{GridLayouter, RowState}; use super::rowspans::UnbreakableRowGroup; -impl GridLayouter<'_> { - /// Layouts the header's rows. - /// Skips regions as necessary. - pub fn layout_header( +impl<'a> GridLayouter<'a> { + /// Checks whether a region break could help a situation where we're out of + /// space for the next row. The criteria are: + /// + /// 1. If we could progress at the top of the region, that indicates the + /// region has a backlog, or (if we're at the first region) a region break + /// is at all possible (`regions.last` is `Some()`), so that's sufficient. + /// + /// 2. Otherwise, we may progress if another region break is possible + /// (`regions.last` is still `Some()`) and non-repeating rows have been + /// placed, since that means the space they occupy will be available in the + /// next region. + #[inline] + pub fn may_progress_with_repeats(&self) -> bool { + // TODO(subfooters): check below isn't enough to detect non-repeating + // footers... we can also change 'initial_after_repeats' to stop being + // calculated if there were any non-repeating footers. + self.current.could_progress_at_top + || self.regions.last.is_some() + && self.regions.size.y != self.current.initial_after_repeats + } + + pub fn place_new_headers( + &mut self, + consecutive_header_count: &mut usize, + engine: &mut Engine, + ) -> SourceResult<()> { + *consecutive_header_count += 1; + let (consecutive_headers, new_upcoming_headers) = + self.upcoming_headers.split_at(*consecutive_header_count); + + if new_upcoming_headers.first().is_some_and(|next_header| { + consecutive_headers.last().is_none_or(|latest_header| { + !latest_header.short_lived + && next_header.range.start == latest_header.range.end + }) && !next_header.short_lived + }) { + // More headers coming, so wait until we reach them. + return Ok(()); + } + + self.upcoming_headers = new_upcoming_headers; + *consecutive_header_count = 0; + + let [first_header, ..] = consecutive_headers else { + self.flush_orphans(); + return Ok(()); + }; + + // Assuming non-conflicting headers sorted by increasing y, this must + // be the header with the lowest level (sorted by increasing levels). + let first_level = first_header.level; + + // Stop repeating conflicting headers, even if the new headers are + // short-lived or won't repeat. + // + // If we go to a new region before the new headers fit alongside their + // children (or in general, for short-lived), the old headers should + // not be displayed anymore. + let first_conflicting_pos = + self.repeating_headers.partition_point(|h| h.level < first_level); + self.repeating_headers.truncate(first_conflicting_pos); + + // Ensure upcoming rows won't see that these headers will occupy any + // space in future regions anymore. + for removed_height in + self.current.repeating_header_heights.drain(first_conflicting_pos..) + { + self.current.repeating_header_height -= removed_height; + } + + // Layout short-lived headers immediately. + if consecutive_headers.last().is_some_and(|h| h.short_lived) { + // No chance of orphans as we're immediately placing conflicting + // headers afterwards, which basically are not headers, for all intents + // and purposes. It is therefore guaranteed that all new headers have + // been placed at least once. + self.flush_orphans(); + + // Layout each conflicting header independently, without orphan + // prevention (as they don't go into 'pending_headers'). + // These headers are short-lived as they are immediately followed by a + // header of the same or lower level, such that they never actually get + // to repeat. + self.layout_new_headers(consecutive_headers, true, engine)?; + } else { + // Let's try to place pending headers at least once. + // This might be a waste as we could generate an orphan and thus have + // to try to place old and new headers all over again, but that happens + // for every new region anyway, so it's rather unavoidable. + let snapshot_created = + self.layout_new_headers(consecutive_headers, false, engine)?; + + // Queue the new headers for layout. They will remain in this + // vector due to orphan prevention. + // + // After the first subsequent row is laid out, move to repeating, as + // it's then confirmed the headers won't be moved due to orphan + // prevention anymore. + self.pending_headers = consecutive_headers; + + if !snapshot_created { + // Region probably couldn't progress. + // + // Mark new pending headers as final and ensure there isn't a + // snapshot. + self.flush_orphans(); + } + } + + Ok(()) + } + + /// Lays out rows belonging to a header, returning the calculated header + /// height only for that header. Indicates to the laid out rows that they + /// should inform their laid out heights if appropriate (auto or fixed + /// size rows only). + #[inline] + fn layout_header_rows( &mut self, header: &Header, engine: &mut Engine, disambiguator: usize, - ) -> SourceResult<()> { - let header_rows = - self.simulate_header(header, &self.regions, engine, disambiguator)?; - let mut skipped_region = false; - while self.unbreakable_rows_left == 0 - && !self.regions.size.y.fits(header_rows.height + self.footer_height) - && self.regions.may_progress() - { - // Advance regions without any output until we can place the - // header and the footer. - self.finish_region_internal(Frame::soft(Axes::splat(Abs::zero())), vec![]); - skipped_region = true; + as_short_lived: bool, + ) -> SourceResult { + let mut header_height = Abs::zero(); + for y in header.range.clone() { + header_height += self + .layout_row_with_state( + y, + engine, + disambiguator, + RowState { + current_row_height: Some(Abs::zero()), + in_active_repeatable: !as_short_lived, + }, + )? + .current_row_height + .unwrap_or_default(); + } + Ok(header_height) + } + + /// This function should be called each time an additional row has been + /// laid out in a region to indicate that orphan prevention has succeeded. + /// + /// It removes the current orphan snapshot and flushes pending headers, + /// such that a non-repeating header won't try to be laid out again + /// anymore, and a repeating header will begin to be part of + /// `repeating_headers`. + pub fn flush_orphans(&mut self) { + self.current.lrows_orphan_snapshot = None; + self.flush_pending_headers(); + } + + /// Indicates all currently pending headers have been successfully placed + /// once, since another row has been placed after them, so they are + /// certainly not orphans. + pub fn flush_pending_headers(&mut self) { + if self.pending_headers.is_empty() { + return; } - // Reset the header height for this region. - // It will be re-calculated when laying out each header row. - self.header_height = Abs::zero(); - - if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { - if skipped_region { - // Simulate the footer again; the region's 'full' might have - // changed. - self.footer_height = self - .simulate_footer(footer, &self.regions, engine, disambiguator)? - .height; + for header in self.pending_headers { + if header.repeated { + // Vector remains sorted by increasing levels: + // - 'pending_headers' themselves are sorted, since we only + // push non-mutually-conflicting headers at a time. + // - Before pushing new pending headers in + // 'layout_new_pending_headers', we truncate repeating headers + // to remove anything with the same or higher levels as the + // first pending header. + // - Assuming it was sorted before, that truncation only keeps + // elements with a lower level. + // - Therefore, by pushing this header to the end, it will have + // a level larger than all the previous headers, and is thus + // in its 'correct' position. + self.repeating_headers.push(header); } } - // Header is unbreakable. + self.pending_headers = Default::default(); + } + + /// Lays out the rows of repeating and pending headers at the top of the + /// region. + /// + /// Assumes the footer height for the current region has already been + /// calculated. Skips regions as necessary to fit all headers and all + /// footers. + pub fn layout_active_headers(&mut self, engine: &mut Engine) -> SourceResult<()> { + // Generate different locations for content in headers across its + // repetitions by assigning a unique number for each one. + let disambiguator = self.finished.len(); + + let header_height = self.simulate_header_height( + self.repeating_headers + .iter() + .copied() + .chain(self.pending_headers.iter().map(Repeatable::deref)), + &self.regions, + engine, + disambiguator, + )?; + + // We already take the footer into account below. + // While skipping regions, footer height won't be automatically + // re-calculated until the end. + let mut skipped_region = false; + while self.unbreakable_rows_left == 0 + && !self.regions.size.y.fits(header_height) + && self.may_progress_with_repeats() + { + // Advance regions without any output until we can place the + // header and the footer. + self.finish_region_internal( + Frame::soft(Axes::splat(Abs::zero())), + vec![], + Default::default(), + ); + + // TODO(layout model): re-calculate heights of headers and footers + // on each region if 'full' changes? (Assuming height doesn't + // change for now...) + // + // Would remove the footer height update below (move it here). + skipped_region = true; + + self.regions.size.y -= self.current.footer_height; + self.current.initial_after_repeats = self.regions.size.y; + } + + if let Some(footer) = &self.grid.footer { + if footer.repeated && skipped_region { + // Simulate the footer again; the region's 'full' might have + // changed. + self.regions.size.y += self.current.footer_height; + self.current.footer_height = self + .simulate_footer(footer, &self.regions, engine, disambiguator)? + .height; + self.regions.size.y -= self.current.footer_height; + } + } + + let repeating_header_rows = + total_header_row_count(self.repeating_headers.iter().copied()); + + let pending_header_rows = + total_header_row_count(self.pending_headers.iter().map(Repeatable::deref)); + + // Group of headers is unbreakable. // Thus, no risk of 'finish_region' being recursively called from // within 'layout_row'. - self.unbreakable_rows_left += header.end; - for y in 0..header.end { - self.layout_row(y, engine, disambiguator)?; + self.unbreakable_rows_left += repeating_header_rows + pending_header_rows; + + self.current.last_repeated_header_end = + self.repeating_headers.last().map(|h| h.range.end).unwrap_or_default(); + + // Reset the header height for this region. + // It will be re-calculated when laying out each header row. + self.current.repeating_header_height = Abs::zero(); + self.current.repeating_header_heights.clear(); + + debug_assert!(self.current.lrows.is_empty()); + debug_assert!(self.current.lrows_orphan_snapshot.is_none()); + let may_progress = self.may_progress_with_repeats(); + + if may_progress { + // Enable orphan prevention for headers at the top of the region. + // Otherwise, we will flush pending headers below, after laying + // them out. + // + // It is very rare for this to make a difference as we're usually + // at the 'last' region after the first skip, at which the snapshot + // is handled by 'layout_new_headers'. Either way, we keep this + // here for correctness. + self.current.lrows_orphan_snapshot = Some(self.current.lrows.len()); } + + // Use indices to avoid double borrow. We don't mutate headers in + // 'layout_row' so this is fine. + let mut i = 0; + while let Some(&header) = self.repeating_headers.get(i) { + let header_height = + self.layout_header_rows(header, engine, disambiguator, false)?; + self.current.repeating_header_height += header_height; + + // We assume that this vector will be sorted according + // to increasing levels like 'repeating_headers' and + // 'pending_headers' - and, in particular, their union, as this + // vector is pushed repeating heights from both. + // + // This is guaranteed by: + // 1. We always push pending headers after repeating headers, + // as we assume they don't conflict because we remove + // conflicting repeating headers when pushing a new pending + // header. + // + // 2. We push in the same order as each. + // + // 3. This vector is also modified when pushing a new pending + // header, where we remove heights for conflicting repeating + // headers which have now stopped repeating. They are always at + // the end and new pending headers respect the existing sort, + // so the vector will remain sorted. + self.current.repeating_header_heights.push(header_height); + + i += 1; + } + + self.current.repeated_header_rows = self.current.lrows.len(); + self.current.initial_after_repeats = self.regions.size.y; + + let mut has_non_repeated_pending_header = false; + for header in self.pending_headers { + if !header.repeated { + self.current.initial_after_repeats = self.regions.size.y; + has_non_repeated_pending_header = true; + } + let header_height = + self.layout_header_rows(header, engine, disambiguator, false)?; + if header.repeated { + self.current.repeating_header_height += header_height; + self.current.repeating_header_heights.push(header_height); + } + } + + if !has_non_repeated_pending_header { + self.current.initial_after_repeats = self.regions.size.y; + } + + if !may_progress { + // Flush pending headers immediately, as placing them again later + // won't help. + self.flush_orphans(); + } + Ok(()) } + /// Lays out headers found for the first time during row layout. + /// + /// If 'short_lived' is true, these headers are immediately followed by + /// a conflicting header, so it is assumed they will not be pushed to + /// pending headers. + /// + /// Returns whether orphan prevention was successfully setup, or couldn't + /// due to short-lived headers or the region couldn't progress. + pub fn layout_new_headers( + &mut self, + headers: &'a [Repeatable
], + short_lived: bool, + engine: &mut Engine, + ) -> SourceResult { + // At first, only consider the height of the given headers. However, + // for upcoming regions, we will have to consider repeating headers as + // well. + let header_height = self.simulate_header_height( + headers.iter().map(Repeatable::deref), + &self.regions, + engine, + 0, + )?; + + while self.unbreakable_rows_left == 0 + && !self.regions.size.y.fits(header_height) + && self.may_progress_with_repeats() + { + // Note that, after the first region skip, the new headers will go + // at the top of the region, but after the repeating headers that + // remained (which will be automatically placed in 'finish_region'). + self.finish_region(engine, false)?; + } + + // Remove new headers at the end of the region if the upcoming row + // doesn't fit. + // TODO(subfooters): what if there is a footer right after it? + let should_snapshot = !short_lived + && self.current.lrows_orphan_snapshot.is_none() + && self.may_progress_with_repeats(); + + if should_snapshot { + // If we don't enter this branch while laying out non-short lived + // headers, that means we will have to immediately flush pending + // headers and mark them as final, since trying to place them in + // the next page won't help get more space. + self.current.lrows_orphan_snapshot = Some(self.current.lrows.len()); + } + + let mut at_top = self.regions.size.y == self.current.initial_after_repeats; + + self.unbreakable_rows_left += + total_header_row_count(headers.iter().map(Repeatable::deref)); + + for header in headers { + let header_height = self.layout_header_rows(header, engine, 0, false)?; + + // Only store this header height if it is actually going to + // become a pending header. Otherwise, pretend it's not a + // header... This is fine for consumers of 'header_height' as + // it is guaranteed this header won't appear in a future + // region, so multi-page rows and cells can effectively ignore + // this header. + if !short_lived && header.repeated { + self.current.repeating_header_height += header_height; + self.current.repeating_header_heights.push(header_height); + if at_top { + self.current.initial_after_repeats = self.regions.size.y; + } + } else { + at_top = false; + } + } + + Ok(should_snapshot) + } + + /// Calculates the total expected height of several headers. + pub fn simulate_header_height<'h: 'a>( + &self, + headers: impl IntoIterator, + regions: &Regions<'_>, + engine: &mut Engine, + disambiguator: usize, + ) -> SourceResult { + let mut height = Abs::zero(); + for header in headers { + height += + self.simulate_header(header, regions, engine, disambiguator)?.height; + } + Ok(height) + } + /// Simulate the header's group of rows. pub fn simulate_header( &self, @@ -66,8 +455,8 @@ impl GridLayouter<'_> { // assume that the amount of unbreakable rows following the first row // in the header will be precisely the rows in the header. self.simulate_unbreakable_row_group( - 0, - Some(header.end), + header.range.start, + Some(header.range.end - header.range.start), regions, engine, disambiguator, @@ -91,11 +480,22 @@ impl GridLayouter<'_> { { // Advance regions without any output until we can place the // footer. - self.finish_region_internal(Frame::soft(Axes::splat(Abs::zero())), vec![]); + self.finish_region_internal( + Frame::soft(Axes::splat(Abs::zero())), + vec![], + Default::default(), + ); skipped_region = true; } - self.footer_height = if skipped_region { + // TODO(subfooters): Consider resetting header height etc. if we skip + // region. (Maybe move that step to `finish_region_internal`.) + // + // That is unnecessary at the moment as 'prepare_footers' is only + // called at the start of the region, so header height is always zero + // and no headers were placed so far, but what about when we can have + // footers in the middle of the region? Let's think about this then. + self.current.footer_height = if skipped_region { // Simulate the footer again; the region's 'full' might have // changed. self.simulate_footer(footer, &self.regions, engine, disambiguator)? @@ -118,12 +518,22 @@ impl GridLayouter<'_> { // Ensure footer rows have their own height available. // Won't change much as we're creating an unbreakable row group // anyway, so this is mostly for correctness. - self.regions.size.y += self.footer_height; + self.regions.size.y += self.current.footer_height; + let repeats = self.grid.footer.as_ref().is_some_and(|f| f.repeated); let footer_len = self.grid.rows.len() - footer.start; self.unbreakable_rows_left += footer_len; + for y in footer.start..self.grid.rows.len() { - self.layout_row(y, engine, disambiguator)?; + self.layout_row_with_state( + y, + engine, + disambiguator, + RowState { + in_active_repeatable: repeats, + ..Default::default() + }, + )?; } Ok(()) @@ -144,10 +554,18 @@ impl GridLayouter<'_> { // in the footer will be precisely the rows in the footer. self.simulate_unbreakable_row_group( footer.start, - Some(self.grid.rows.len() - footer.start), + Some(footer.end - footer.start), regions, engine, disambiguator, ) } } + +/// The total amount of rows in the given list of headers. +#[inline] +pub fn total_header_row_count<'h>( + headers: impl IntoIterator, +) -> usize { + headers.into_iter().map(|h| h.range.end - h.range.start).sum() +} diff --git a/crates/typst-layout/src/grid/rowspans.rs b/crates/typst-layout/src/grid/rowspans.rs index 5ab0417d8..02ea14813 100644 --- a/crates/typst-layout/src/grid/rowspans.rs +++ b/crates/typst-layout/src/grid/rowspans.rs @@ -4,7 +4,7 @@ use typst_library::foundations::Resolve; use typst_library::layout::grid::resolve::Repeatable; use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing}; -use super::layouter::{in_last_with_offset, points, Row, RowPiece}; +use super::layouter::{points, Row}; use super::{layout_cell, Cell, GridLayouter}; /// All information needed to layout a single rowspan. @@ -90,10 +90,10 @@ pub struct CellMeasurementData<'layouter> { impl GridLayouter<'_> { /// Layout a rowspan over the already finished regions, plus the current - /// region's frame and resolved rows, if it wasn't finished yet (because - /// we're being called from `finish_region`, but note that this function is - /// also called once after all regions are finished, in which case - /// `current_region_data` is `None`). + /// region's frame and height of resolved header rows, if it wasn't + /// finished yet (because we're being called from `finish_region`, but note + /// that this function is also called once after all regions are finished, + /// in which case `current_region_data` is `None`). /// /// 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 @@ -101,7 +101,7 @@ impl GridLayouter<'_> { pub fn layout_rowspan( &mut self, rowspan_data: Rowspan, - current_region_data: Option<(&mut Frame, &[RowPiece])>, + current_region_data: Option<(&mut Frame, Abs)>, engine: &mut Engine, ) -> SourceResult<()> { let Rowspan { @@ -146,11 +146,31 @@ impl GridLayouter<'_> { // Push the layouted frames directly into the finished frames. let fragment = layout_cell(cell, engine, disambiguator, self.styles, pod)?; - let (current_region, current_rrows) = current_region_data.unzip(); - for ((i, finished), frame) in self + let (current_region, current_header_row_height) = current_region_data.unzip(); + + // Clever trick to process finished header rows: + // - If there are grid headers, the vector will be filled with one + // finished header row height per region, so, chaining with the height + // for the current one, we get the header row height for each region. + // + // - But if there are no grid headers, the vector will be empty, so in + // theory the regions and resolved header row heights wouldn't match. + // But that's fine - 'current_header_row_height' can only be either + // 'Some(zero)' or 'None' in such a case, and for all other rows we + // append infinite zeros. That is, in such a case, the resolved header + // row height is always zero, so that's our fallback. + let finished_header_rows = self + .finished_header_rows + .iter() + .map(|info| info.repeated_height) + .chain(current_header_row_height) + .chain(std::iter::repeat(Abs::zero())); + + for ((i, (finished, header_dy)), frame) in self .finished .iter_mut() .chain(current_region.into_iter()) + .zip(finished_header_rows) .skip(first_region) .enumerate() .zip(fragment) @@ -162,22 +182,9 @@ impl GridLayouter<'_> { } else { // The rowspan continuation starts after the header (thus, // at a position after the sum of the laid out header - // rows). - if let Some(Repeatable::Repeated(header)) = &self.grid.header { - let header_rows = self - .rrows - .get(i) - .map(Vec::as_slice) - .or(current_rrows) - .unwrap_or(&[]) - .iter() - .take_while(|row| row.y < header.end); - - header_rows.map(|row| row.height).sum() - } else { - // Without a header, start at the very top of the region. - Abs::zero() - } + // rows). Without a header, this is zero, so the rowspan can + // start at the very top of the region as usual. + header_dy }; finished.push_frame(Point::new(dx, dy), frame); @@ -231,15 +238,13 @@ impl GridLayouter<'_> { // current row is dynamic and depends on the amount of upcoming // unbreakable cells (with or without a rowspan setting). let mut amount_unbreakable_rows = None; - if let Some(Repeatable::NotRepeated(header)) = &self.grid.header { - if current_row < header.end { - // Non-repeated header, so keep it unbreakable. - amount_unbreakable_rows = Some(header.end); - } - } - if let Some(Repeatable::NotRepeated(footer)) = &self.grid.footer { - if current_row >= footer.start { + if let Some(footer) = &self.grid.footer { + if !footer.repeated && current_row >= footer.start { // Non-repeated footer, so keep it unbreakable. + // + // TODO(subfooters): This will become unnecessary + // once non-repeated footers are treated differently and + // have widow prevention. amount_unbreakable_rows = Some(self.grid.rows.len() - footer.start); } } @@ -254,10 +259,7 @@ impl GridLayouter<'_> { // Skip to fitting region. while !self.regions.size.y.fits(row_group.height) - && !in_last_with_offset( - self.regions, - self.header_height + self.footer_height, - ) + && self.may_progress_with_repeats() { self.finish_region(engine, false)?; } @@ -396,16 +398,29 @@ impl GridLayouter<'_> { // auto rows don't depend on the backlog, as they only span one // region. if breakable - && (matches!(self.grid.header, Some(Repeatable::Repeated(_))) - || matches!(self.grid.footer, Some(Repeatable::Repeated(_)))) + && (!self.repeating_headers.is_empty() + || !self.pending_headers.is_empty() + || matches!(&self.grid.footer, Some(footer) if footer.repeated)) { // Subtract header and footer height from all upcoming regions // when measuring the cell, including the last repeated region. // // This will update the 'custom_backlog' vector with the // updated heights of the upcoming regions. + // + // We predict that header height will only include that of + // repeating headers, as we can assume non-repeating headers in + // the first region have been successfully placed, unless + // something didn't fit on the first region of the auto row, + // but we will only find that out after measurement, and if + // that happens, we discard the measurement and try again. let mapped_regions = self.regions.map(&mut custom_backlog, |size| { - Size::new(size.x, size.y - self.header_height - self.footer_height) + Size::new( + size.x, + size.y + - self.current.repeating_header_height + - self.current.footer_height, + ) }); // Callees must use the custom backlog instead of the current @@ -459,6 +474,7 @@ impl GridLayouter<'_> { // Height of the rowspan covered by spanned rows in the current // region. let laid_out_height: Abs = self + .current .lrows .iter() .filter_map(|row| match row { @@ -506,7 +522,12 @@ impl GridLayouter<'_> { .iter() .copied() .chain(std::iter::once(if breakable { - self.initial.y - self.header_height - self.footer_height + // Here we are calculating the available height for a + // rowspan from the top of the current region, so + // we have to use initial header heights (note that + // header height can change in the middle of the + // region). + self.current.initial_after_repeats } else { // When measuring unbreakable auto rows, infinite // height is available for content to expand. @@ -518,11 +539,13 @@ impl GridLayouter<'_> { // rowspan's already laid out heights with the current // region's height and current backlog to ensure a good // level of accuracy in the measurements. - let backlog = self - .regions - .backlog - .iter() - .map(|&size| size - self.header_height - self.footer_height); + // + // Assume only repeating headers will survive starting at + // the next region. + let backlog = self.regions.backlog.iter().map(|&size| { + size - self.current.repeating_header_height + - self.current.footer_height + }); heights_up_to_current_region.chain(backlog).collect::>() } else { @@ -536,10 +559,10 @@ impl GridLayouter<'_> { height = *rowspan_height; backlog = None; full = rowspan_full; - last = self - .regions - .last - .map(|size| size - self.header_height - self.footer_height); + last = self.regions.last.map(|size| { + size - self.current.repeating_header_height + - self.current.footer_height + }); } else { // The rowspan started in the current region, as its vector // of heights in regions is currently empty. @@ -741,10 +764,11 @@ impl GridLayouter<'_> { simulated_regions.next(); disambiguator += 1; - // Subtract the initial header and footer height, since that's the - // height we used when subtracting from the region backlog's + // Subtract the repeating header and footer height, since that's + // the height we used when subtracting from the region backlog's // heights while measuring cells. - simulated_regions.size.y -= self.header_height + self.footer_height; + simulated_regions.size.y -= + self.current.repeating_header_height + self.current.footer_height; } if let Some(original_last_resolved_size) = last_resolved_size { @@ -876,12 +900,8 @@ impl GridLayouter<'_> { // which, when used and combined with upcoming spanned rows, covers all // of the requested rowspan height, we give up. for _attempt in 0..5 { - let rowspan_simulator = RowspanSimulator::new( - disambiguator, - simulated_regions, - self.header_height, - self.footer_height, - ); + let rowspan_simulator = + RowspanSimulator::new(disambiguator, simulated_regions, &self.current); let total_spanned_height = rowspan_simulator.simulate_rowspan_layout( y, @@ -963,7 +983,8 @@ impl GridLayouter<'_> { { extra_amount_to_grow -= simulated_regions.size.y.max(Abs::zero()); simulated_regions.next(); - simulated_regions.size.y -= self.header_height + self.footer_height; + simulated_regions.size.y -= + self.current.repeating_header_height + self.current.footer_height; disambiguator += 1; } simulated_regions.size.y -= extra_amount_to_grow; @@ -980,10 +1001,17 @@ struct RowspanSimulator<'a> { finished: usize, /// The state of regions during the simulation. regions: Regions<'a>, - /// The height of the header in the currently simulated region. + /// The total height of headers in the currently simulated region. header_height: Abs, - /// The height of the footer in the currently simulated region. + /// The total height of footers in the currently simulated region. footer_height: Abs, + /// Whether `self.regions.may_progress()` was `true` at the top of the + /// region, indicating we can progress anywhere in the current region, + /// even right after a repeated header. + could_progress_at_top: bool, + /// Available height after laying out repeated headers at the top of the + /// currently simulated region. + initial_after_repeats: Abs, /// The total spanned height so far in the simulation. total_spanned_height: Abs, /// Height of the latest spanned gutter row in the simulation. @@ -997,14 +1025,19 @@ impl<'a> RowspanSimulator<'a> { fn new( finished: usize, regions: Regions<'a>, - header_height: Abs, - footer_height: Abs, + current: &super::layouter::Current, ) -> Self { Self { finished, regions, - header_height, - footer_height, + // There can be no new headers or footers within a multi-page + // rowspan, since headers and footers are unbreakable, so + // assuming the repeating header height and footer height + // won't change is safe. + header_height: current.repeating_header_height, + footer_height: current.footer_height, + could_progress_at_top: current.could_progress_at_top, + initial_after_repeats: current.initial_after_repeats, total_spanned_height: Abs::zero(), latest_spanned_gutter_height: Abs::zero(), } @@ -1053,10 +1086,7 @@ impl<'a> RowspanSimulator<'a> { 0, )?; while !self.regions.size.y.fits(row_group.height) - && !in_last_with_offset( - self.regions, - self.header_height + self.footer_height, - ) + && self.may_progress_with_repeats() { self.finish_region(layouter, engine)?; } @@ -1078,10 +1108,7 @@ impl<'a> RowspanSimulator<'a> { let mut skipped_region = false; while unbreakable_rows_left == 0 && !self.regions.size.y.fits(height) - && !in_last_with_offset( - self.regions, - self.header_height + self.footer_height, - ) + && self.may_progress_with_repeats() { self.finish_region(layouter, engine)?; @@ -1127,23 +1154,37 @@ impl<'a> RowspanSimulator<'a> { // our simulation checks what happens AFTER the auto row, so we can // just use the original backlog from `self.regions`. let disambiguator = self.finished; - let header_height = - if let Some(Repeatable::Repeated(header)) = &layouter.grid.header { - layouter - .simulate_header(header, &self.regions, engine, disambiguator)? - .height - } else { - Abs::zero() - }; - let footer_height = - if let Some(Repeatable::Repeated(footer)) = &layouter.grid.footer { - layouter - .simulate_footer(footer, &self.regions, engine, disambiguator)? - .height - } else { - Abs::zero() - }; + let (repeating_headers, header_height) = if !layouter.repeating_headers.is_empty() + || !layouter.pending_headers.is_empty() + { + // Only repeating headers have survived after the first region + // break. + let repeating_headers = layouter.repeating_headers.iter().copied().chain( + layouter.pending_headers.iter().filter_map(Repeatable::as_repeated), + ); + + let header_height = layouter.simulate_header_height( + repeating_headers.clone(), + &self.regions, + engine, + disambiguator, + )?; + + (Some(repeating_headers), header_height) + } else { + (None, Abs::zero()) + }; + + let footer_height = if let Some(footer) = + layouter.grid.footer.as_ref().and_then(Repeatable::as_repeated) + { + layouter + .simulate_footer(footer, &self.regions, engine, disambiguator)? + .height + } else { + Abs::zero() + }; let mut skipped_region = false; @@ -1156,19 +1197,24 @@ impl<'a> RowspanSimulator<'a> { skipped_region = true; } - if let Some(Repeatable::Repeated(header)) = &layouter.grid.header { + if let Some(repeating_headers) = repeating_headers { self.header_height = if skipped_region { // Simulate headers again, at the new region, as // the full region height may change. - layouter - .simulate_header(header, &self.regions, engine, disambiguator)? - .height + layouter.simulate_header_height( + repeating_headers, + &self.regions, + engine, + disambiguator, + )? } else { header_height }; } - if let Some(Repeatable::Repeated(footer)) = &layouter.grid.footer { + if let Some(footer) = + layouter.grid.footer.as_ref().and_then(Repeatable::as_repeated) + { self.footer_height = if skipped_region { // Simulate footers again, at the new region, as // the full region height may change. @@ -1185,6 +1231,7 @@ impl<'a> RowspanSimulator<'a> { // header or footer (as an invariant, any rowspans spanning any header // or footer rows are fully contained within that header's or footer's rows). self.regions.size.y -= self.header_height + self.footer_height; + self.initial_after_repeats = self.regions.size.y; Ok(()) } @@ -1201,8 +1248,18 @@ impl<'a> RowspanSimulator<'a> { self.regions.next(); self.finished += 1; + self.could_progress_at_top = self.regions.may_progress(); self.simulate_header_footer_layout(layouter, engine) } + + /// Similar to [`GridLayouter::may_progress_with_repeats`] but for rowspan + /// simulation. + #[inline] + fn may_progress_with_repeats(&self) -> bool { + self.could_progress_at_top + || self.regions.last.is_some() + && self.regions.size.y != self.initial_after_repeats + } } /// Subtracts some size from the end of a vector of sizes. diff --git a/crates/typst-library/src/foundations/int.rs b/crates/typst-library/src/foundations/int.rs index 83a89bf8a..f65641ff1 100644 --- a/crates/typst-library/src/foundations/int.rs +++ b/crates/typst-library/src/foundations/int.rs @@ -1,4 +1,6 @@ -use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError}; +use std::num::{ + NonZeroI64, NonZeroIsize, NonZeroU32, NonZeroU64, NonZeroUsize, ParseIntError, +}; use ecow::{eco_format, EcoString}; use smallvec::SmallVec; @@ -482,3 +484,16 @@ cast! { "number too large" })?, } + +cast! { + NonZeroU32, + self => Value::Int(self.get() as _), + v: i64 => v + .try_into() + .and_then(|v: u32| v.try_into()) + .map_err(|_| if v <= 0 { + "number must be positive" + } else { + "number too large" + })?, +} diff --git a/crates/typst-library/src/layout/grid/mod.rs b/crates/typst-library/src/layout/grid/mod.rs index 369df11ee..52621c647 100644 --- a/crates/typst-library/src/layout/grid/mod.rs +++ b/crates/typst-library/src/layout/grid/mod.rs @@ -1,6 +1,6 @@ pub mod resolve; -use std::num::NonZeroUsize; +use std::num::{NonZeroU32, NonZeroUsize}; use std::sync::Arc; use comemo::Track; @@ -468,6 +468,17 @@ pub struct GridHeader { #[default(true)] pub repeat: bool, + /// The level of the header. Must not be zero. + /// + /// This allows repeating multiple headers at once. Headers with different + /// levels can repeat together, as long as they have ascending levels. + /// + /// Notably, when a header with a lower level starts repeating, all higher + /// or equal level headers stop repeating (they are "replaced" by the new + /// header). + #[default(NonZeroU32::ONE)] + pub level: NonZeroU32, + /// The cells and lines within the header. #[variadic] pub children: Vec, diff --git a/crates/typst-library/src/layout/grid/resolve.rs b/crates/typst-library/src/layout/grid/resolve.rs index bad25b474..baf6b7383 100644 --- a/crates/typst-library/src/layout/grid/resolve.rs +++ b/crates/typst-library/src/layout/grid/resolve.rs @@ -1,5 +1,5 @@ -use std::num::NonZeroUsize; -use std::ops::Range; +use std::num::{NonZeroU32, NonZeroUsize}; +use std::ops::{Deref, DerefMut, Range}; use std::sync::Arc; use ecow::eco_format; @@ -48,6 +48,7 @@ pub fn grid_to_cellgrid<'a>( let children = elem.children.iter().map(|child| match child { GridChild::Header(header) => ResolvableGridChild::Header { repeat: header.repeat(styles), + level: header.level(styles), span: header.span(), items: header.children.iter().map(resolve_item), }, @@ -101,6 +102,7 @@ pub fn table_to_cellgrid<'a>( let children = elem.children.iter().map(|child| match child { TableChild::Header(header) => ResolvableGridChild::Header { repeat: header.repeat(styles), + level: header.level(styles), span: header.span(), items: header.children.iter().map(resolve_item), }, @@ -426,8 +428,20 @@ pub struct Line { /// A repeatable grid header. Starts at the first row. #[derive(Debug)] pub struct Header { - /// The index after the last row included in this header. - pub end: usize, + /// The range of rows included in this header. + pub range: Range, + /// The header's level. + /// + /// Higher level headers repeat together with lower level headers. If a + /// lower level header stops repeating, all higher level headers do as + /// well. + pub level: u32, + /// Whether this header cannot be repeated nor should have orphan + /// prevention because it would be about to cease repetition, either + /// because it is followed by headers of conflicting levels, or because + /// it is at the end of the table (possibly followed by some footers at the + /// end). + pub short_lived: bool, } /// A repeatable grid footer. Stops at the last row. @@ -435,32 +449,56 @@ pub struct Header { pub struct Footer { /// The first row included in this footer. pub start: usize, + /// The index after the last row included in this footer. + pub end: usize, + /// The footer's level. + /// + /// Used similarly to header level. + pub level: u32, } -/// A possibly repeatable grid object. +impl Footer { + /// The footer's range of included rows. + #[inline] + pub fn range(&self) -> Range { + self.start..self.end + } +} + +/// A possibly repeatable grid child (header or footer). +/// /// 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 enum Repeatable { - Repeated(T), - NotRepeated(T), +pub struct Repeatable { + inner: T, + + /// Whether the user requested the child to repeat. + pub repeated: bool, +} + +impl Deref for Repeatable { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Repeatable { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } } impl Repeatable { - /// Gets the value inside this repeatable, regardless of whether - /// it repeats. - pub fn unwrap(&self) -> &T { - match self { - Self::Repeated(repeated) => repeated, - Self::NotRepeated(not_repeated) => not_repeated, - } - } - /// Returns `Some` if the value is repeated, `None` otherwise. + #[inline] pub fn as_repeated(&self) -> Option<&T> { - match self { - Self::Repeated(repeated) => Some(repeated), - Self::NotRepeated(_) => None, + if self.repeated { + Some(&self.inner) + } else { + None } } } @@ -617,7 +655,7 @@ impl<'a> Entry<'a> { /// Any grid child, which can be either a header or an item. pub enum ResolvableGridChild { - Header { repeat: bool, span: Span, items: I }, + Header { repeat: bool, level: NonZeroU32, span: Span, items: I }, Footer { repeat: bool, span: Span, items: I }, Item(ResolvableGridItem), } @@ -638,8 +676,8 @@ pub struct CellGrid<'a> { /// Gutter rows are not included. /// Contains up to 'rows_without_gutter.len() + 1' vectors of lines. pub hlines: Vec>, - /// The repeatable header of this grid. - pub header: Option>, + /// The repeatable headers of this grid. + pub headers: Vec>, /// The repeatable footer of this grid. pub footer: Option>, /// Whether this grid has gutters. @@ -654,7 +692,7 @@ impl<'a> CellGrid<'a> { cells: impl IntoIterator>, ) -> Self { let entries = cells.into_iter().map(Entry::Cell).collect(); - Self::new_internal(tracks, gutter, vec![], vec![], None, None, entries) + Self::new_internal(tracks, gutter, vec![], vec![], vec![], None, entries) } /// Generates the cell grid, given the tracks and resolved entries. @@ -663,7 +701,7 @@ impl<'a> CellGrid<'a> { gutter: Axes<&[Sizing]>, vlines: Vec>, hlines: Vec>, - header: Option>, + headers: Vec>, footer: Option>, entries: Vec>, ) -> Self { @@ -717,7 +755,7 @@ impl<'a> CellGrid<'a> { entries, vlines, hlines, - header, + headers, footer, has_gutter, } @@ -852,6 +890,11 @@ impl<'a> CellGrid<'a> { self.cols.len() } } + + #[inline] + pub fn has_repeated_headers(&self) -> bool { + self.headers.iter().any(|h| h.repeated) + } } /// Resolves and positions all cells in the grid before creating it. @@ -937,6 +980,12 @@ struct RowGroupData { span: Span, kind: RowGroupKind, + /// Whether this header or footer may repeat. + repeat: bool, + + /// Level of this header or footer. + repeatable_level: NonZeroU32, + /// Start of the range of indices of hlines at the top of the row group. /// This is always the first index after the last hline before we started /// building the row group - any upcoming hlines would appear at least at @@ -984,14 +1033,17 @@ impl<'x> CellGridResolver<'_, '_, 'x> { let mut pending_vlines: Vec<(Span, Line)> = vec![]; let has_gutter = self.gutter.any(|tracks| !tracks.is_empty()); - let mut header: Option
= None; - let mut repeat_header = false; + let mut headers: Vec> = vec![]; // Stores where the footer is supposed to end, its span, and the // actual footer structure. let mut footer: Option<(usize, Span, Footer)> = None; let mut repeat_footer = false; + // If true, there has been at least one cell besides headers and + // footers. When false, footers at the end are forced to not repeat. + let mut at_least_one_cell = false; + // We can't just use the cell's index in the 'cells' vector to // determine its automatic position, since cells could have arbitrary // positions, so the position of a cell in 'cells' can differ from its @@ -1008,6 +1060,11 @@ impl<'x> CellGridResolver<'_, '_, 'x> { // automatically-positioned cell. let mut auto_index: usize = 0; + // The next header after the latest auto-positioned cell. This is used + // to avoid checking for collision with headers that were already + // skipped. + let mut next_header = 0; + // We have to rebuild the grid to account for fixed cell positions. // // Create at least 'children.len()' positions, since there could be at @@ -1028,12 +1085,13 @@ impl<'x> CellGridResolver<'_, '_, 'x> { columns, &mut pending_hlines, &mut pending_vlines, - &mut header, - &mut repeat_header, + &mut headers, &mut footer, &mut repeat_footer, &mut auto_index, + &mut next_header, &mut resolved_cells, + &mut at_least_one_cell, child, )?; } @@ -1049,13 +1107,13 @@ impl<'x> CellGridResolver<'_, '_, 'x> { row_amount, )?; - let (header, footer) = self.finalize_headers_and_footers( + let footer = self.finalize_headers_and_footers( has_gutter, - header, - repeat_header, + &mut headers, footer, repeat_footer, row_amount, + at_least_one_cell, )?; Ok(CellGrid::new_internal( @@ -1063,7 +1121,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> { self.gutter, vlines, hlines, - header, + headers, footer, resolved_cells, )) @@ -1083,12 +1141,13 @@ impl<'x> CellGridResolver<'_, '_, 'x> { columns: usize, pending_hlines: &mut Vec<(Span, Line, bool)>, pending_vlines: &mut Vec<(Span, Line)>, - header: &mut Option
, - repeat_header: &mut bool, + headers: &mut Vec>, footer: &mut Option<(usize, Span, Footer)>, repeat_footer: &mut bool, auto_index: &mut usize, + next_header: &mut usize, resolved_cells: &mut Vec>>, + at_least_one_cell: &mut bool, child: ResolvableGridChild, ) -> SourceResult<()> where @@ -1112,7 +1171,32 @@ impl<'x> CellGridResolver<'_, '_, 'x> { // position than it would usually be if it would be in a non-empty // row, so we must step a local index inside headers and footers // instead, and use a separate counter outside them. - let mut local_auto_index = *auto_index; + let local_auto_index = if matches!(child, ResolvableGridChild::Item(_)) { + auto_index + } else { + // Although 'usize' is Copy, we need to be explicit here that we + // aren't reborrowing the original auto index but rather making a + // mutable copy of it using 'clone'. + &mut (*auto_index).clone() + }; + + // NOTE: usually, if 'next_header' were to be updated inside a row + // group (indicating a header was skipped by a cell), that would + // indicate a collision between the row group and that header, which + // is an error. However, the exception is for the first auto cell of + // the row group, which may skip headers while searching for a position + // where to begin the row group in the first place. + // + // Therefore, we cannot safely share the counter in the row group with + // the counter used by auto cells outside, as it might update it in a + // valid situation, whereas it must not, since its auto cells use a + // different auto index counter and will have seen different headers, + // so we copy the next header counter while inside a row group. + let local_next_header = if matches!(child, ResolvableGridChild::Item(_)) { + next_header + } else { + &mut (*next_header).clone() + }; // The first row in which this table group can fit. // @@ -1123,23 +1207,19 @@ impl<'x> CellGridResolver<'_, '_, 'x> { let mut first_available_row = 0; let (header_footer_items, simple_item) = match child { - ResolvableGridChild::Header { repeat, span, items, .. } => { - if header.is_some() { - bail!(span, "cannot have more than one header"); - } - + ResolvableGridChild::Header { repeat, level, span, items, .. } => { row_group_data = Some(RowGroupData { range: None, span, kind: RowGroupKind::Header, + repeat, + repeatable_level: level, top_hlines_start: pending_hlines.len(), top_hlines_end: None, }); - *repeat_header = repeat; - first_available_row = - find_next_empty_row(resolved_cells, local_auto_index, columns); + find_next_empty_row(resolved_cells, *local_auto_index, columns); // If any cell in the header is automatically positioned, // have it skip to the next empty row. This is to avoid @@ -1150,7 +1230,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> { // latest auto-position cell, since each auto-position cell // always occupies the first available position after the // previous one. Therefore, this will be >= auto_index. - local_auto_index = first_available_row * columns; + *local_auto_index = first_available_row * columns; (Some(items), None) } @@ -1162,21 +1242,27 @@ impl<'x> CellGridResolver<'_, '_, 'x> { row_group_data = Some(RowGroupData { range: None, span, + repeat, kind: RowGroupKind::Footer, + repeatable_level: NonZeroU32::ONE, top_hlines_start: pending_hlines.len(), top_hlines_end: None, }); - *repeat_footer = repeat; - first_available_row = - find_next_empty_row(resolved_cells, local_auto_index, columns); + find_next_empty_row(resolved_cells, *local_auto_index, columns); - local_auto_index = first_available_row * columns; + *local_auto_index = first_available_row * columns; (Some(items), None) } - ResolvableGridChild::Item(item) => (None, Some(item)), + ResolvableGridChild::Item(item) => { + if matches!(item, ResolvableGridItem::Cell(_)) { + *at_least_one_cell = true; + } + + (None, Some(item)) + } }; let items = header_footer_items.into_iter().flatten().chain(simple_item); @@ -1191,7 +1277,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> { // gutter. skip_auto_index_through_fully_merged_rows( resolved_cells, - &mut local_auto_index, + local_auto_index, columns, ); @@ -1266,7 +1352,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> { // automatically positioned cell. Same for footers. local_auto_index .checked_sub(1) - .filter(|_| local_auto_index > first_available_row * columns) + .filter(|_| *local_auto_index > first_available_row * columns) .map_or(0, |last_auto_index| last_auto_index % columns + 1) }); if end.is_some_and(|end| end.get() < start) { @@ -1295,10 +1381,11 @@ impl<'x> CellGridResolver<'_, '_, 'x> { cell_y, colspan, rowspan, - header.as_ref(), + headers, footer.as_ref(), resolved_cells, - &mut local_auto_index, + local_auto_index, + local_next_header, first_available_row, columns, row_group_data.is_some(), @@ -1350,7 +1437,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> { ); if top_hlines_end.is_none() - && local_auto_index > first_available_row * columns + && *local_auto_index > first_available_row * columns { // Auto index was moved, so upcoming auto-pos hlines should // no longer appear at the top. @@ -1437,7 +1524,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> { None => { // Empty header/footer: consider the header/footer to be // at the next empty row after the latest auto index. - local_auto_index = first_available_row * columns; + *local_auto_index = first_available_row * columns; let group_start = first_available_row; let group_end = group_start + 1; @@ -1454,8 +1541,8 @@ impl<'x> CellGridResolver<'_, '_, 'x> { // 'find_next_empty_row' will skip through any existing headers // and footers without having to loop through them each time. // Cells themselves, unfortunately, still have to. - assert!(resolved_cells[local_auto_index].is_none()); - resolved_cells[local_auto_index] = + assert!(resolved_cells[*local_auto_index].is_none()); + resolved_cells[*local_auto_index] = Some(Entry::Cell(self.resolve_cell( T::default(), 0, @@ -1483,21 +1570,38 @@ impl<'x> CellGridResolver<'_, '_, 'x> { match row_group.kind { RowGroupKind::Header => { - if group_range.start != 0 { - bail!( - row_group.span, - "header must start at the first row"; - hint: "remove any rows before the header" - ); - } - - *header = Some(Header { - // Later on, we have to correct this number in case there + let data = Header { + // Later on, we have to correct this range in case there // is gutter. But only once all cells have been analyzed // and the header has fully expanded in the fixup loop // below. - end: group_range.end, - }); + range: group_range, + + level: row_group.repeatable_level.get(), + + // This can only change at a later iteration, if we + // find a conflicting header or footer right away. + short_lived: false, + }; + + // Mark consecutive headers right before this one as short + // lived if they would have a higher or equal level, as + // then they would immediately stop repeating during + // layout. + let mut consecutive_header_start = data.range.start; + for conflicting_header in + headers.iter_mut().rev().take_while(move |h| { + let conflicts = h.range.end == consecutive_header_start + && h.level >= data.level; + + consecutive_header_start = h.range.start; + conflicts + }) + { + conflicting_header.short_lived = true; + } + + headers.push(Repeatable { inner: data, repeated: row_group.repeat }); } RowGroupKind::Footer => { @@ -1514,15 +1618,14 @@ impl<'x> CellGridResolver<'_, '_, 'x> { // before the footer might not be included as part of // the footer if it is contained within the header. start: group_range.start, + end: group_range.end, + level: 1, }, )); + + *repeat_footer = row_group.repeat; } } - } else { - // The child was a single cell outside headers or footers. - // Therefore, 'local_auto_index' for this table child was - // simply an alias for 'auto_index', so we update it as needed. - *auto_index = local_auto_index; } Ok(()) @@ -1689,47 +1792,64 @@ impl<'x> CellGridResolver<'_, '_, 'x> { fn finalize_headers_and_footers( &self, has_gutter: bool, - header: Option
, - repeat_header: bool, + headers: &mut [Repeatable
], footer: Option<(usize, Span, Footer)>, repeat_footer: bool, row_amount: usize, - ) -> SourceResult<(Option>, Option>)> { - let header = header - .map(|mut header| { - // Repeat the gutter below a header (hence why we don't - // subtract 1 from the gutter case). - // Don't do this if there are no rows under the header. - if has_gutter { - // - 'header.end' is always 'last y + 1'. The header stops - // before that row. - // - Therefore, '2 * header.end' will be 2 * (last y + 1), - // which is the adjusted index of the row before which the - // header stops, meaning it will still stop right before it - // even with gutter thanks to the multiplication below. - // - This means that it will span all rows up to - // '2 * (last y + 1) - 1 = 2 * last y + 1', which equates - // to the index of the gutter row right below the header, - // which is what we want (that gutter spacing should be - // repeated across pages to maintain uniformity). - header.end *= 2; + at_least_one_cell: bool, + ) -> SourceResult>> { + // Mark consecutive headers right before the end of the table, or the + // final footer, as short lived, given that there are no normal rows + // after them, so repeating them is pointless. + // + // It is important to do this BEFORE we update header and footer ranges + // due to gutter below as 'row_amount' doesn't consider gutter. + // + // TODO(subfooters): take the last footer if it is at the end and + // backtrack through consecutive footers until the first one in the + // sequence is found. If there is no footer at the end, there are no + // haeders to turn short-lived. + let mut consecutive_header_start = + footer.as_ref().map(|(_, _, f)| f.start).unwrap_or(row_amount); + for header_at_the_end in headers.iter_mut().rev().take_while(move |h| { + let at_the_end = h.range.end == consecutive_header_start; - // If the header occupies the entire grid, ensure we don't - // include an extra gutter row when it doesn't exist, since - // the last row of the header is at the very bottom, - // therefore '2 * last y + 1' is not a valid index. - let row_amount = (2 * row_amount).saturating_sub(1); - header.end = header.end.min(row_amount); - } - header - }) - .map(|header| { - if repeat_header { - Repeatable::Repeated(header) - } else { - Repeatable::NotRepeated(header) - } - }); + consecutive_header_start = h.range.start; + at_the_end + }) { + header_at_the_end.short_lived = true; + } + + // Repeat the gutter below a header (hence why we don't + // subtract 1 from the gutter case). + // Don't do this if there are no rows under the header. + if has_gutter { + for header in &mut *headers { + // Index of first y is doubled, as each row before it + // receives a gutter row below. + header.range.start *= 2; + + // - 'header.end' is always 'last y + 1'. The header stops + // before that row. + // - Therefore, '2 * header.end' will be 2 * (last y + 1), + // which is the adjusted index of the row before which the + // header stops, meaning it will still stop right before it + // even with gutter thanks to the multiplication below. + // - This means that it will span all rows up to + // '2 * (last y + 1) - 1 = 2 * last y + 1', which equates + // to the index of the gutter row right below the header, + // which is what we want (that gutter spacing should be + // repeated across pages to maintain uniformity). + header.range.end *= 2; + + // If the header occupies the entire grid, ensure we don't + // include an extra gutter row when it doesn't exist, since + // the last row of the header is at the very bottom, + // therefore '2 * last y + 1' is not a valid index. + let row_amount = (2 * row_amount).saturating_sub(1); + header.range.end = header.range.end.min(row_amount); + } + } let footer = footer .map(|(footer_end, footer_span, mut footer)| { @@ -1737,8 +1857,17 @@ impl<'x> CellGridResolver<'_, '_, 'x> { bail!(footer_span, "footer must end at the last row"); } - let header_end = - header.as_ref().map(Repeatable::unwrap).map(|header| header.end); + // TODO(subfooters): will need a global slice of headers and + // footers for when we have multiple footers + // Alternatively, never include the gutter in the footer's + // range and manually add it later on layout. This would allow + // laying out the gutter as part of both the header and footer, + // and, if the page only has headers, the gutter row below the + // header is automatically removed (as it becomes the last), so + // only the gutter above the footer is kept, ensuring the same + // gutter row isn't laid out two times in a row. When laying + // out the footer for real, the mechanism can be disabled. + let last_header_end = headers.last().map(|header| header.range.end); if has_gutter { // Convert the footer's start index to post-gutter coordinates. @@ -1747,23 +1876,38 @@ impl<'x> CellGridResolver<'_, '_, 'x> { // Include the gutter right before the footer, unless there is // none, or the gutter is already included in the header (no // rows between the header and the footer). - if header_end != Some(footer.start) { + if last_header_end != Some(footer.start) { footer.start = footer.start.saturating_sub(1); } + + // Adapt footer end but DO NOT include the gutter below it, + // if it exists. Calculation: + // - Starts as 'last y + 1'. + // - The result will be + // 2 * (last_y + 1) - 1 = 2 * last_y + 1, + // which is the new index of the last footer row plus one, + // meaning we do exclude any gutter below this way. + // + // It also keeps us within the total amount of rows, so we + // don't need to '.min()' later. + footer.end = (2 * footer.end).saturating_sub(1); } Ok(footer) }) .transpose()? .map(|footer| { - if repeat_footer { - Repeatable::Repeated(footer) - } else { - Repeatable::NotRepeated(footer) + // Don't repeat footers when the table only has headers and + // footers. + // TODO(subfooters): Switch this to marking the last N + // consecutive footers as short lived. + Repeatable { + inner: footer, + repeated: repeat_footer && at_least_one_cell, } }); - Ok((header, footer)) + Ok(footer) } /// Resolves the cell's fields based on grid-wide properties. @@ -1934,28 +2078,28 @@ fn expand_row_group( /// Check if a cell's fixed row would conflict with a header or footer. fn check_for_conflicting_cell_row( - header: Option<&Header>, + headers: &[Repeatable
], footer: Option<&(usize, Span, Footer)>, cell_y: usize, rowspan: usize, ) -> HintedStrResult<()> { - if let Some(header) = header { - // TODO: check start (right now zero, always satisfied) - if cell_y < header.end { - bail!( - "cell would conflict with header spanning the same position"; - hint: "try moving the cell or the header" - ); - } + // NOTE: y + rowspan >, not >=, header.start, to check if the rowspan + // enters the header. For example, consider a rowspan of 1: if + // `y + 1 = header.start` holds, that means `y < header.start`, and it + // only occupies one row (`y`), so the cell is actually not in + // conflict. + if headers + .iter() + .any(|header| cell_y < header.range.end && cell_y + rowspan > header.range.start) + { + bail!( + "cell would conflict with header spanning the same position"; + hint: "try moving the cell or the header" + ); } - if let Some((footer_end, _, footer)) = footer { - // NOTE: y + rowspan >, not >=, footer.start, to check if the rowspan - // enters the footer. For example, consider a rowspan of 1: if - // `y + 1 = footer.start` holds, that means `y < footer.start`, and it - // only occupies one row (`y`), so the cell is actually not in - // conflict. - if cell_y < *footer_end && cell_y + rowspan > footer.start { + if let Some((_, _, footer)) = footer { + if cell_y < footer.end && cell_y + rowspan > footer.start { bail!( "cell would conflict with footer spanning the same position"; hint: "try reducing the cell's rowspan or moving the footer" @@ -1981,10 +2125,11 @@ fn resolve_cell_position( cell_y: Smart, colspan: usize, rowspan: usize, - header: Option<&Header>, + headers: &[Repeatable
], footer: Option<&(usize, Span, Footer)>, resolved_cells: &[Option], auto_index: &mut usize, + next_header: &mut usize, first_available_row: usize, columns: usize, in_row_group: bool, @@ -2005,12 +2150,14 @@ fn resolve_cell_position( // Note that the counter ignores any cells with fixed positions, // but automatically-positioned cells will avoid conflicts by // simply skipping existing cells, headers and footers. - let resolved_index = find_next_available_position::( - header, + let resolved_index = find_next_available_position( + headers, footer, resolved_cells, columns, *auto_index, + next_header, + false, )?; // Ensure the next cell with automatic position will be @@ -2046,7 +2193,7 @@ fn resolve_cell_position( // footer (but only if it isn't already in one, otherwise there // will already be a separate check). if !in_row_group { - check_for_conflicting_cell_row(header, footer, cell_y, rowspan)?; + check_for_conflicting_cell_row(headers, footer, cell_y, rowspan)?; } cell_index(cell_x, cell_y) @@ -2063,12 +2210,28 @@ fn resolve_cell_position( // requested column ('Some(None)') or an out of bounds position // ('None'), in which case we'd create a new row to place this // cell in. - find_next_available_position::( - header, + find_next_available_position( + headers, footer, resolved_cells, columns, initial_index, + // Make our own copy of the 'next_header' counter, since it + // should only be updated by auto cells. However, we cannot + // start with the same value as we are searching from the + // start, and not from 'auto_index', so auto cells might + // have skipped some headers already which this cell will + // also need to skip. + // + // We could, in theory, keep a separate 'next_header' + // counter for cells with fixed columns. But then we would + // need one for every column, and much like how there isn't + // an index counter for each column either, the potential + // speed gain seems less relevant for a less used feature. + // Still, it is something to consider for the future if + // this turns out to be a bottleneck in important cases. + &mut 0, + true, ) } } @@ -2078,7 +2241,7 @@ fn resolve_cell_position( // footer (but only if it isn't already in one, otherwise there // will already be a separate check). if !in_row_group { - check_for_conflicting_cell_row(header, footer, cell_y, rowspan)?; + check_for_conflicting_cell_row(headers, footer, cell_y, rowspan)?; } // Let's find the first column which has that row available. @@ -2110,13 +2273,18 @@ fn resolve_cell_position( /// Finds the first available position after the initial index in the resolved /// grid of cells. Skips any non-absent positions (positions which already /// have cells specified by the user) as well as any headers and footers. +/// +/// When `skip_rows` is true, one row is skipped on each iteration, preserving +/// the column. That is used to find a position for a fixed column cell. #[inline] -fn find_next_available_position( - header: Option<&Header>, +fn find_next_available_position( + headers: &[Repeatable
], footer: Option<&(usize, Span, Footer)>, resolved_cells: &[Option>], columns: usize, initial_index: usize, + next_header: &mut usize, + skip_rows: bool, ) -> HintedStrResult { let mut resolved_index = initial_index; @@ -2126,7 +2294,7 @@ fn find_next_available_position( // determine where this cell will be placed. An out of // bounds position (thus `None`) is also a valid new // position (only requires expanding the vector). - if SKIP_ROWS { + if skip_rows { // Skip one row at a time (cell chose its column, so we don't // change it). resolved_index = @@ -2139,24 +2307,33 @@ fn find_next_available_position( // would become impractically large before this overflows. resolved_index += 1; } - } else if let Some(header) = - header.filter(|header| resolved_index < header.end * columns) + } else if let Some(header) = headers + .get(*next_header) + .filter(|header| resolved_index >= header.range.start * columns) { // Skip header (can't place a cell inside it from outside it). - resolved_index = header.end * columns; + // No changes needed if we already passed this header (which + // also triggers this branch) - in that case, we only update the + // counter. + if resolved_index < header.range.end * columns { + resolved_index = header.range.end * columns; - if SKIP_ROWS { - // Ensure the cell's chosen column is kept after the - // header. - resolved_index += initial_index % columns; + if skip_rows { + // Ensure the cell's chosen column is kept after the + // header. + resolved_index += initial_index % columns; + } } + + // From now on, only check the headers afterwards. + *next_header += 1; } else if let Some((footer_end, _, _)) = footer.filter(|(end, _, footer)| { resolved_index >= footer.start * columns && resolved_index < *end * columns }) { // Skip footer, for the same reason. resolved_index = *footer_end * columns; - if SKIP_ROWS { + if skip_rows { resolved_index += initial_index % columns; } } else { diff --git a/crates/typst-library/src/model/table.rs b/crates/typst-library/src/model/table.rs index 373230897..dcc77b0dc 100644 --- a/crates/typst-library/src/model/table.rs +++ b/crates/typst-library/src/model/table.rs @@ -1,4 +1,4 @@ -use std::num::NonZeroUsize; +use std::num::{NonZeroU32, NonZeroUsize}; use std::sync::Arc; use typst_utils::NonZeroExt; @@ -292,16 +292,61 @@ fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content { elem(tag::tr, Content::sequence(row)) }; + // TODO(subfooters): similarly to headers, take consecutive footers from + // the end for 'tfoot'. let footer = grid.footer.map(|ft| { - let rows = rows.drain(ft.unwrap().start..); + let rows = rows.drain(ft.start..); elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row)))) }); - let header = grid.header.map(|hd| { - let rows = rows.drain(..hd.unwrap().end); - elem(tag::thead, Content::sequence(rows.map(|row| tr(tag::th, row)))) - }); - let mut body = Content::sequence(rows.into_iter().map(|row| tr(tag::td, row))); + // Store all consecutive headers at the start in 'thead'. All remaining + // headers are just 'th' rows across the table body. + let mut consecutive_header_end = 0; + let first_mid_table_header = grid + .headers + .iter() + .take_while(|hd| { + let is_consecutive = hd.range.start == consecutive_header_end; + consecutive_header_end = hd.range.end; + + is_consecutive + }) + .count(); + + let (y_offset, header) = if first_mid_table_header > 0 { + let removed_header_rows = + grid.headers.get(first_mid_table_header - 1).unwrap().range.end; + let rows = rows.drain(..removed_header_rows); + + ( + removed_header_rows, + Some(elem(tag::thead, Content::sequence(rows.map(|row| tr(tag::th, row))))), + ) + } else { + (0, None) + }; + + // TODO: Consider improving accessibility properties of multi-level headers + // inside tables in the future, e.g. indicating which columns they are + // relative to and so on. See also: + // https://www.w3.org/WAI/tutorials/tables/multi-level/ + let mut next_header = first_mid_table_header; + let mut body = + Content::sequence(rows.into_iter().enumerate().map(|(relative_y, row)| { + let y = relative_y + y_offset; + if let Some(current_header) = + grid.headers.get(next_header).filter(|h| h.range.contains(&y)) + { + if y + 1 == current_header.range.end { + next_header += 1; + } + + tr(tag::th, row) + } else { + tr(tag::td, row) + } + })); + if header.is_some() || footer.is_some() { body = elem(tag::tbody, body); } @@ -492,6 +537,17 @@ pub struct TableHeader { #[default(true)] pub repeat: bool, + /// The level of the header. Must not be zero. + /// + /// This allows repeating multiple headers at once. Headers with different + /// levels can repeat together, as long as they have ascending levels. + /// + /// Notably, when a header with a lower level starts repeating, all higher + /// or equal level headers stop repeating (they are "replaced" by the new + /// header). + #[default(NonZeroU32::ONE)] + pub level: NonZeroU32, + /// The cells and lines within the header. #[variadic] pub children: Vec, diff --git a/crates/typst-syntax/src/span.rs b/crates/typst-syntax/src/span.rs index 3618b8f2f..b383ec27f 100644 --- a/crates/typst-syntax/src/span.rs +++ b/crates/typst-syntax/src/span.rs @@ -71,10 +71,7 @@ impl Span { /// Create a span that does not point into any file. pub const fn detached() -> Self { - match NonZeroU64::new(Self::DETACHED) { - Some(v) => Self(v), - None => unreachable!(), - } + Self(NonZeroU64::new(Self::DETACHED).unwrap()) } /// Create a new span from a file id and a number. @@ -111,11 +108,9 @@ impl Span { /// Pack a file ID and the low bits into a span. const fn pack(id: FileId, low: u64) -> Self { let bits = ((id.into_raw().get() as u64) << Self::FILE_ID_SHIFT) | low; - match NonZeroU64::new(bits) { - Some(v) => Self(v), - // The file ID is non-zero. - None => unreachable!(), - } + + // The file ID is non-zero. + Self(NonZeroU64::new(bits).unwrap()) } /// Whether the span is detached. diff --git a/crates/typst-utils/src/lib.rs b/crates/typst-utils/src/lib.rs index b346a8096..abe6423df 100644 --- a/crates/typst-utils/src/lib.rs +++ b/crates/typst-utils/src/lib.rs @@ -26,7 +26,7 @@ pub use once_cell; use std::fmt::{Debug, Formatter}; use std::hash::Hash; use std::iter::{Chain, Flatten, Rev}; -use std::num::NonZeroUsize; +use std::num::{NonZeroU32, NonZeroUsize}; use std::ops::{Add, Deref, Div, Mul, Neg, Sub}; use std::sync::Arc; @@ -66,10 +66,11 @@ pub trait NonZeroExt { } impl NonZeroExt for NonZeroUsize { - const ONE: Self = match Self::new(1) { - Some(v) => v, - None => unreachable!(), - }; + const ONE: Self = Self::new(1).unwrap(); +} + +impl NonZeroExt for NonZeroU32 { + const ONE: Self = Self::new(1).unwrap(); } /// Extra methods for [`Arc`]. diff --git a/crates/typst-utils/src/pico.rs b/crates/typst-utils/src/pico.rs index 2c80d37de..ce43667e9 100644 --- a/crates/typst-utils/src/pico.rs +++ b/crates/typst-utils/src/pico.rs @@ -95,10 +95,7 @@ impl PicoStr { } }; - match NonZeroU64::new(value) { - Some(value) => Ok(Self(value)), - None => unreachable!(), - } + Ok(Self(NonZeroU64::new(value).unwrap())) } /// Resolve to a decoded string. diff --git a/tests/ref/grid-footer-non-repeatable-unbreakable.png b/tests/ref/grid-footer-non-repeatable-unbreakable.png new file mode 100644 index 0000000000000000000000000000000000000000..59d72201f664f253d4ead37d769a26c148473b66 GIT binary patch literal 365 zcmV-z0h0cSP)959>O9UARv13v5=HY8(@bcbh zJ{?svs%BKpsG3nVqiU$Bh6pekhNEq4daL=mwBaHlHD_;JO(CS_&4h*Cgw)jSzUxj% z&6TE|MK}Ksr#5zV>1$+gPIgfEtEY#snnU@7)ttzG_M4y@$MtS62%7VMdOu+`6IKyW zbIj>b>JI{HKE1j0Z*ZiatdJGRV6qq%HDNm8$iSCx#FbJ1H_h8lNX>!v|J{Vt6mKDH zSAWU>mxR>(%&XWlbi&YI&ITEna$`|*e9d8Guy^5TTVhnrkW&KyP3_2YTbJIr00000 LNkvXXu0mjf3g)NX literal 0 HcmV?d00001 diff --git a/tests/ref/grid-footer-repeatable-unbreakable.png b/tests/ref/grid-footer-repeatable-unbreakable.png new file mode 100644 index 0000000000000000000000000000000000000000..0fa30f773390598874caadec81d03a8bfbd76820 GIT binary patch literal 340 zcmV-a0jvIrP)qr%<|VX4`=$T z**EnaAvK5hohT)w=I@5NK7`avSpU|GVEV~h;&^k&gkh*w2r}^E#iAxVbTu-VCO#E8*d$^GB9n6O>*rV~;V|2^pip>ku@ynBNq{Zy5_KnAB} zv8Zw55l051?8MkLywp6|@RX36+j;L|38|^yOxUja9>QuK+chmE?5}TwBn%TA8ju0j mw#0<^dSp-$FxqCLq8b39-U1g;aIg&k0000*Rn_wHat8+oR#w)fOP7B6@`aa|S4c=G zCMM?beXcEiGkXVG$P>&&$hWV`J;@@9*mB3JwnT_V(VgWy_2iGt$!1 zu3o(=CMK4Zm8GJhlAoU+9UZ-M=gyfkXYSasLqFIg) z>{&@kiI%F6BS?NwD( zdU|?HOibUteXFglJ#pg1zJ2@H+1Yt`cwWAISy53@Q&Z#N;lanp$HBn?3<`dJ{tq8M z?8w%d1`Hh&PZ!6Kid%2*oXoo%Akp^lKASqLRMfML0vexf&bMr2nKflu;p5-?MJ8-A zv99tHsJ-`QQ>BXcJ(-_1ik|iH%cI3oBmewcy84yN?pL=z96EaM)4StJvF^JsuG85! zr@O-bhrmm%Zu@^bYs!j0-+l0#>4EX$i^mEC3-psr?QLGpwYX^#-#&j{5epzAT`)$SaMX@^zAD7Lss?(|KDcbL6 zo`3Y|)T=Mt@_w9s@N4V8smsrAI-D zg9!!_JZ#NS>bu;$9P`hc=7(S3TzPKwRZR%+@~f_Pl-Fer>|1>v(>ZGGp}b{H(dYNPa4IT4G_s1D|Wa8(9JfG z$N`z6^4V$iO*t$)1d)*e2%4gcgH)bZ_* sfBSBoywAP8zxT0CRq>nzMqan{StqxdENxr^%v}r&p00i_>zopr0GIBM6951J literal 0 HcmV?d00001 diff --git a/tests/ref/grid-header-and-rowspan-contiguous-1.png b/tests/ref/grid-header-and-rowspan-contiguous-1.png new file mode 100644 index 0000000000000000000000000000000000000000..7cf2cb9ca2f1398eae307c4d89639794495ce82c GIT binary patch literal 815 zcmeAS@N?(olHy`uVBq!ia0vp^6$}iFM>v>)tfNQeqJSKx0G|-o|NsBj*Vi{RH2ilo zQ&(4aaBy&TcGlC=Q&v_kEiH|Wjg5$iC@U)q4Gk3+7thPfD=#lsRaITQcyVK6U#Y6F)uIgmoHzIE?wH*-X0SZBP1lm$;o;7@?|C_rf=WAef|2?)zuZ~PDVyX9v&V( zK0bDKb~ZLPX=&-dfB$aTvSr4M8EI*0SFc`;j*jNy;z~+NI&c)oRUAs4ZT;g$kV%-+)?#Jp%=X&Ja zcb!T)uOIHWZrwat+bD*bRo8FJ=`p=?Tl2hq`Q6{&XFYrOA#w7$yO~>VN3MQ-wlLOy z#?saII+?GWm-qdoXL9k~_3vv>R&2AB_E&qH(*G-Ci}v>YwY|~X-yhi-_ip~>=lX@k zxwgM1O8+yd`ugeE*X;1khvn_})P1aZ{b}l(f<5(@ZK|s@KOKI5&rL*JoZ&ok^T7lI zi9i1jUcQ?a_wDw|d1s5QRrk;3OU*ccTa!Je0VK=A23CZV`pp(@`9SLy?=RO+t3JoA zoW6xWS4YD?Zc?S}XRBwWXI{^^Zn!=`tkZg~Iz literal 0 HcmV?d00001 diff --git a/tests/ref/grid-header-and-rowspan-contiguous-2.png b/tests/ref/grid-header-and-rowspan-contiguous-2.png new file mode 100644 index 0000000000000000000000000000000000000000..29bc411d1cbf611682cb463daed918d201d3b788 GIT binary patch literal 815 zcmeAS@N?(olHy`uVBq!ia0vp^6$}iFM>v>)tfNQeqJY%@0G|-o|NsAg`0$~=zP_QM z;lHEV?Af#V`1oGFeCh1$oRE+(VZwy?_;_}9_I>;I)z;RYIB~+k!C~prrBzi`OiWDQ zzI|(NZ`ae)`|{%4_Vn~Td-m+kojcy% z-oe4aSFc`8OG}$EV}^{3OmuW~e}6w07gu3nVNz1knKNg$Y}ry$QsU+1B`q!e_wQd7 z6_xz_{H&}jF)^{Ot}Zqlq7s;Vw7E+0RBeDUH14-Zd8MMX_bjfaN^2L}f*0QmX&uh%jg0>kCC zr;B4q#jUq@j^_ynO0+$!Pdo5HO6D@}!teJKzsRwB%;|jpKg!`#(I&@>mcp9nU(Yq~ zPdV8gKkKK{q;K)-uck=0&wbA9>i=5tBe7bSVDPS(A8d~r=w?O&sPHMYWw6W{H>yjuKYTC&XF z6TI>B?(Hdk{A;WB&4=FQ@9x#@`S&EN%tG#t{yh6?sh!%IgAj#%~U^VzCd7FxP%-?o7Nx$7TB~-xkS?QVA zGp-x1PmVjjr>|1>vsD=j#GcvL3$15fXT)a|Ozt~ldGUd}8P{GC;Al3Q%+@jgci60mKVf4DH z{%QVSIf-uB>$~?#NXY-6f6v}Q%s;|ZKuo^aYRa!<1)YCio?Y5MG1T+V-8-LC9CYl; zUe(-di1?HI&}Kg4#vQf4wa+tdtf+r^>cQ;Ax$EFgn8U!3yk*+FZN_PRAP0N8`njxg HN@xNA-aTI_ literal 0 HcmV?d00001 diff --git a/tests/ref/grid-header-non-repeating-orphan-prevention.png b/tests/ref/grid-header-non-repeating-orphan-prevention.png new file mode 100644 index 0000000000000000000000000000000000000000..d0dbc59748ce0e8462187568367c72b122387107 GIT binary patch literal 453 zcmeAS@N?(olHy`uVBq!ia0vp^6+k?L14uA%c^CCCFfjIbx;TbZ+y*RqZXeQh$X>hg>89Mje8Hk~*2HhJV$}T*Br<=6ar@>~ z4-S5wvi*x}6oaau``TSA=D#^mdr;<-JHPh5wC=B8{arsa*>~p7|F5`7s)pr|a_9g3 z4tb02D}DG_bVi#=y7Q-)poP)YJdsBBIo(skg+8RP+bJ;4|H@!gaQMs)MW*eCg~hw7 zj;b@2A1u{nRBu~W{`t++%zHDHJ`_K%+syIiwu>9fx0V*GeJlF%Iu3ZuPx~3I0`{N{ z?}lWccmIo>d$XkF!7^q^iFe9^7E8iA4JNHQEdv@jsG0~9}=u6{1-oD!MmDLvn{Rnn3D?czT5o{NOk@`w`ewV;>U%57`?8l zf13YSPNG}(`tH3F67v7&-?MiR^N%nU5R)&qn(`}ILFeC>XP5S0yp;(xpGl0F;ZSmDLvn{Rnn3D?czT5o{NOk@`w`ewV;>U%57`?8l zf13YSPNG}(`tH3F67v7&-?MiR^N%nU5R)&qn(`}ILFeC>XP5S0yp;(xpGl0F;ZS&ZD-1Lkf5%VV8QpMfvfojI4J8JW2Ml8^|Gccd!?Qs2 z|0~90w#ly^yt(j?zhT;}{ni&sSG{%c5PjoYTWcnGp`_T~HH?cXfc1Vv^Yv%#%iNQq z^W~22(u(kxSh;>OfBK81U78L|KV_^` zuvVJi^}!pfg<;d5M`YToCTxDtdwT2UM{7-0nf25Dm3JJF|Gesw52wX_U6aS}F7JyH zl(_7hnAZ&nkL8T#K!G#6eX+y(&7QV7^*lPVTA_UZHy@wfuuQwXm_0Yn!Fy+dx$^eU zmvlovWL&!bgU8|W^W^&JF&nv*S3O9Vy`2Ajt<&X=*C*En#;#76J#b{*d5eFW{vGkE zj7jy45R55&e)q2Z(WR5;Wj*Kluy}=hSAs>ti92G#Qi@Fc^XE@$be^ur%&yOQ@T&W* m)CUC?rxYBJV(VcpZ$5)_#*EtAZ*=B^(v7F9pUXO@geCx~>kM50 literal 0 HcmV?d00001 diff --git a/tests/ref/grid-header-replace-doesnt-fit.png b/tests/ref/grid-header-replace-doesnt-fit.png new file mode 100644 index 0000000000000000000000000000000000000000..a06ce0e965d7a7984fd2985dc2af298d7eb4e287 GIT binary patch literal 559 zcmV+~0?_@5P)W-(+^l8q*YA-5)S&7GO~TG;pl^giPGuHN0Jp0~a|PY{R_#4y7QGt4l<48Jz` z9$4vdiic(8!1*t|aM&&j@d1)SIIP+DUcdQ6FSrk(Ewf=4y`P82Zw0}F`fb>>`hByh=Kz>!cD?-q zS)Y0!f#*jpZ6vVYHIP99n>$ZaN#I)5Q1}`J+);=tP{0di01ced-R>uW3s5;tG-{)k z1TIC1n*w$zI7_n5nBVX@V^{q@SOKTC>;2q^7sE) zJ^Lv)8WC9_4z}rh8bpjp9X6Jn_~fUk4Ze0C^vY&v$ya4?GWidEO8ZC4^yy84fo19$4vd ziic(8!1)ioaM&&j^8u1UIIP)tX9XlhjLdhJU!@F)g13`L!c>4{IDS8^OA0jJiGu5v zyltN^AsO(=tc({o>cdQ6FSHNgEwleFdOr@2-wJ{U_1my%_4{U1&jB#e>$ZaN#I)5(El|GxT6qPpnw<302(-@yWLL$7oc*QXw*h630#U2 zHwEl!=$hg4mSKh&W|(1y8D==BV5;zr8D^N_mw*F!9-D4e6m0tt*^+O^Q%p%CO`>4@ zt%B#P_35op3*50(4WwnmqL$kif5%OVg|nh>zH};2v^7sE* zJ^L;<8WC9_4z}r0_&fjr literal 0 HcmV?d00001 diff --git a/tests/ref/grid-header-replace.png b/tests/ref/grid-header-replace.png new file mode 100644 index 0000000000000000000000000000000000000000..dafecd7f48f6e81d378d703d92d3ec8f7d558f09 GIT binary patch literal 692 zcmV;l0!#ggP)000003QKZ~0007eNklFkL?*3O2qTwXFILl>^zbcE7k+JzffYRkUEM$-gbuyz<_IxFERQ9)Z_t z|0Z`fjR6lE1~Pwuyzlx;0{6SREhKPipzSybEITKTlE6;g-5o;|Fk+bEMSx+38D@Af z!c>QQ%rL_Ya|Xg02xlORDg)6-L>b77YN7!Ws($%{20rjc`bhJ{Xum-OTY4zq{)28q z>VNgzIV=k9%2t~y19QbAsr9NT7`YelPWf#1UZMkp!K#Zuu@)Zv3ID6j`yPvhPwtAZ zX(=tn-b)Y*=avN(eV-=8!t2h!wK4otSt8$ux=I}EF&owOQ$YBNGafQGj)>g3Y62u% zX7uT45;&s!UO)n0>xG^KHa6qDp8|fn=gyHz3K)sW7=JA?%rL_YGt9pRxW^1L%<#hC zH0B?#ZBP{KU6E(1@XZySg&o($r9K7>D?PJ!O|9tErwrY-MLvqu=g(@V=9u`@XUnOe zA}|^g3a8hWr&>1+POdLul%Wa|_f8KfSCJBtXu$cxv43NM*+hrPfx&TWIj_xLc<*FdBw+u7PFvAQp%rL_l a1^)wiJV+UqAlozm0000*0w!K~a1rE=Xd_Jeo?N!f@o}Q-yOs_(~0v51<1^f$mN%g5aSyjG8hV!&= zS-E$JTzx@GP>1@VQU<2BogjwG*035#SB0rz|N4tA1gMy*`rStlXRm;iGEWXit;;Jc zeVk3Nb+f?X>8DN>*kanOz2$%>`ij>j4){z5fCcVx1bEU+W)cVtcz1$K&aDzdvWqp^BMfln`UJ1W7cPKXMKRzqU8T0L*-1P|cgoFg) ziZ6xH(+}KC%8Y0F{)f?5Wl#NEQ_h^&nzsU+KxyR)p}qN@54MC_OgeBlAt9LQ{!y!0 z9)ITV%NGVo8(aL#@qQ3;$ZFz&$NqsTa{J$wa{hVDsSlO>du#fElM%0CrX1a_%lSv> zWtNg)Vf(ZW76Su=4>pHZJ?w96Y)of-t}FP)UgV%;KUdt>*nZUy^NPhpZA)J`GRa%K oYhzk>@5iJVJ)m8$dxbmpplQm-Q84I7?mo>NAxj@vkZr&Oz~6Sx2tT}Kkd!L1I0h5^KsUQ+s>CaZ2iKQnE==`mIld~3)bnG7Zp^+buOb9=OXGw4Z$7PN(W^3lEcC-V z^SjrBqjz?z2`vxa&FsP?Az>jgC;q!?!NW7NryK}%7wNp}yg!;rKJsOm5+e_f43A!3 zt!ILH_zc0+!rlH%`6+fL6Hl}R=ejJ?`*44f?8F1svyV+ZFikmX>*e^JQw}^+_~tRU bA=~`F-L1AqubdV?0R^BM;3C~2?nKBx(AtD}@E?hz4f|DFox2AN< z@O3R(VAuDK|Izt}pMAF1K9H+BU;ndAWZ`mV9=7I#st;P-uDh`O`}Ns9oh9%7`}Hml zq?POW+#Z~~wcC6D8BQLy>5bFlOZ;6QoEN@--sQng_pe?o>-PSy*I?SdZokb_kA{N} zftt(WgSu)RIq$o#uQ~&AiCY06vuHlHFq%6G%r?j3By8)h!Q0%*hnHGFQJqtN}_~?P~s^`C=x3+da@uz5%=&< z{WiaI1D`sb(^vHXjfyZ~!h{J6z~{Vc0)a=1xds9&!R!wNcDB}^5P0Tkn3sGf!h{Kn zz}KR81%Zc?sSW}kV`)qzJk8w~1eP7;e@22ZVZuW2z3SgW;J!RIK;TO@{R@G8^*tsM zZL9RhNDwAWSPX836$sqx$QB5ED}f>rs3_kMxMs}1GZKUe6BdK}$rA+b)_ZnP7E@{n z94#C$k+7+AZ;(hx2GCUxj41dcV!A?L{lhJRR-Fk!VC_w_MZlXxe8xHve%(9)Jo+!- Tc1)(d00000NkvXXu0mjf6li}x literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-alone-no-orphan-prevention.png b/tests/ref/grid-subheaders-alone-no-orphan-prevention.png new file mode 100644 index 0000000000000000000000000000000000000000..17bf3fe4f5ff3f740c0251c94b008db961dde494 GIT binary patch literal 254 zcmeAS@N?(olHy`uVBq!ia0vp^6+oQL0VEh^`bt^@siU4Qjv*Dd-rg|eI^-bYdQsTjvu%utTLj)*j)>gF?s!4Xdc&n-PB|t^Z)m(?YVnLS6MQ!L$v)mWNe>R+@o?*I zXhZ_pjQLg8p1bOt4!`mWy0xFRoqf`hANRC=2(kw?^KOQ;hXz};- z+4B#S&dY!KE&cA;`GPwi3*z0UJY!&H&KI2H`QzBTeYyuai_hPPyv6jUI!Wg%>z>+z zZ$I?+nI7AgXxXK|bMm&*A0dxat*u$ln?86Epj57TUG(;c{8yJhFZuIl;}d44J%&G$ z{vWt;Z!N3EoBrOf%VfSaG{V7I%c#ea%*@Qei9Zgq?OAq+=YO}BsG8R)-vvU>Zl5B8 z8dW`Bhs-#*dqv2MqqPyIGec%{{(kc9X9GiUqQx6VphgG?PF!Jd;J|?p2`wIj4Yn22 zzP{Ef*m}7tpz-jT^HaJ0M7@Yxb>QswKi&skv-q?=+`!HQ1ACG`aIGn}4SZr|y9X4g Mp00i_>zopr0A*vRQ~&?~ literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-alone-with-footer.png b/tests/ref/grid-subheaders-alone-with-footer.png new file mode 100644 index 0000000000000000000000000000000000000000..41bf88bc4050ab6cf3efb38110bbfca682df2040 GIT binary patch literal 319 zcmeAS@N?(olHy`uVBq!ia0vp^6+rC90VEid>iI7-Ffg)tx;TbZ+)Dmo@6eEVDc1KY*VFH;MfoNbK^v=>f4%KKGA$Z|4Jc5B6qh(D#7hmSID{PCuDwI*}Y z$Jw0Rd)bdX+I6@5E|AjpweHUAOIse;y)t{F_H+)T2R)0XAHBMC{X|Ev`x!F)!QItW zY$qN$|71@2*!d@YKl`zy<6rai*^V9mzx+G@grmY?ye&tM&*5A8PlBV{e(%mN`CFby ztrJUcUE{#^uJp?$3mzhY<29|TbYb1Z)(Ex>v Mr>mdKI;Vst0B7=#3IG5A literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-alone-with-gutter-and-footer-no-orphan-prevention.png b/tests/ref/grid-subheaders-alone-with-gutter-and-footer-no-orphan-prevention.png new file mode 100644 index 0000000000000000000000000000000000000000..7d6cc45e08422ec2e97decd1c56ca13a6738b0d7 GIT binary patch literal 382 zcmeAS@N?(olHy`uVBq!ia0vp^6+pb414uCZPe1;Jfq~K0)5S5Q;?~=nhTcqx5(hp; zXXj}|3|Jfzv#slZI+++9Im-ynGfsg?y|$1^fdxjMRDlx8{V`tis~M~hmQ z>*WO(zc+sV&-1=_|HB>A+kaaxGT}2YDCl|MbIAPJtGi5dEWY(vdPO(X`uBM5WjxM* z?p^&^`KkMIBfdRzf43k{)-Gnw$zyH@lX)#}O+7L9ROLalKQpVo+MK+<-)E;oLpa;~ zoc9u&OEVeyZr3J9IZv}qKmvm9*%ikR95_&-UZ=^V|I+X7d;6*HL{y(D1)Q|E-T61X zQPtyh$?|iXSClM2SsHOFvt)T=_492&V_*8|R0H+FK#BS;VFLq$yPfY?c=p&;O!<0R zt6*c`?!d;wXO7S0`t#$qO=#n4e|vt$`NBX&sr(XfVDan&&*?s{bs0Za_JIP{)78&q Iol`;+0M_TLTmS$7 literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-alone-with-gutter-no-orphan-prevention.png b/tests/ref/grid-subheaders-alone-with-gutter-no-orphan-prevention.png new file mode 100644 index 0000000000000000000000000000000000000000..17bf3fe4f5ff3f740c0251c94b008db961dde494 GIT binary patch literal 254 zcmeAS@N?(olHy`uVBq!ia0vp^6+oQL0VEh^`bt^@siU4Qjv*Dd-rg|eI^-bYdQsTjvu%utTLj)*j)>gF?s!4Xdc&n-PB|t^Z)m(?YVnLS6MQ!L$v)mWNe>R+@o?*I zXhZ_pjQLg8p1bOt4!`mWy0xFRoqf`hANRC=g*KIEGZ*O8#N*(2#iLQDC>b zuAG~gzek1x+r`f>Qwy7%ZH)}H7fwIQ`&C28axzeMYsHL+Kc$(6k1}rj@uqjRCUerq z*__;a*^fNhb+`O3kka~|sU-R_YjvfEM{5$`IqrzdlEk}>f;amDof}`7h@6IpzTb@X* z6H9Mh_kNXKe4~+t(9ill5`M(dH{j43l5lALX)`G6Cc@Pgg&ebxsLQ E0Q2l`5C8xG literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-basic-non-consecutive-with-footer.png b/tests/ref/grid-subheaders-basic-non-consecutive-with-footer.png new file mode 100644 index 0000000000000000000000000000000000000000..6f2a57beb3810ea4a91f861eee59c1ddd837d9b0 GIT binary patch literal 279 zcmeAS@N?(olHy`uVBq!ia0vp^6+mpt0VEjKpDfq|r0#jTIEGZ*O8#N*(2#iLQDC>b zuH4p&D@jH=|8{M8_^2VG=G(EWQy4e?INQ>Ck2$IE?z`RZn3FzET{hR%+&yGfUDfyV zd#^Jmew11}|0A>T74F0HczjNlo_hJFA!3jD=hkam8#h>(zdOFw9jK-V)2!xokcfC zRH}Y@_s_l817YzRt)KA&6VwaCo(xRMb;{}NfLOdY^M%&6fv`9??#H!917Y!rd%i^a_bKQ_ky8vu*{8Gl*!Wk7Tnl9y5j29i=DFC~jXl4OvQ@>Yo9p+qSRilUyL z;~ospFZrJv_|*5D|2ap(Kuba}!2}ab@E7p25?Vmu$!2ARz}Hk!4S|DQP6dIN;f`zj zSEu^K@w*EGZphwdOzNWH$RO}oIf1}E+gY|ozz)-EeL%n?Cup2Tx=bAd8<}MQN?neF zV9yi*&+O?k{kcSeBl1_%EC66Z@u)r^sgs!kW{>aE0|sV(hJo8d47>}JRuFjM;T;gz z*8*K2(HMI};G4$VphrR!xE4*gA@E$#?lGyeIZSF!&$1A>pD*NToCqeE;2(fL8PQ`V Tbe%SA00000NkvXXu0mjfM4^Wm literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-basic-with-footer.png b/tests/ref/grid-subheaders-basic-with-footer.png new file mode 100644 index 0000000000000000000000000000000000000000..5216561464d16b3123ef947a8c3bb6017e1176c7 GIT binary patch literal 256 zcmeAS@N?(olHy`uVBq!ia0vp^6+o=V0VEiX-Dr9Oq>g*KIEGZ*O8#N*(2#iLQDC>b zuH4p&D@jH=|8{M8_^2VG=G(EWQy4e?INQ>Ck2$IE?z`RZfK=!2bBks(Cw^S`htcb* z`ltDSN{ylpKG5-it0WtYvt0}*d6?Fc6d3I^PV3>98tnIZ9I(8TS z?XPyw(Yt5l;CMIw)BmRp5r2%=E4;eJu=LfL!_3Ja*G~f47kxBnF5|`>^>_b20#X$} z|2J~Jl8t*l_Y}KI;oLiAPyc0zr$OEOM~s=_W=GUd;ai98Kwk57^>bP0l+XkK)WLR} literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-basic.png b/tests/ref/grid-subheaders-basic.png new file mode 100644 index 0000000000000000000000000000000000000000..5a64680757e4120bc89ebd153bf23a5593c53c8c GIT binary patch literal 210 zcmeAS@N?(olHy`uVBq!ia0vp^6+o=P0VEg%8WVp4shOTGjv*Ddl7HAcG$dYm6xi*q zE4Q`cN|KSzzg=4%K5B@l`F8B;6vmA|&bGAPV@@i(`)>C;Al3Q%+@jgci60mKVf4DH z{%QVSIf-uB>$~?#NXY-6f6v}Q%s;|ZKuo^aYRa!<1)YCio?Y567-pS2YkRGOj@^ZS z`>P#v^zIosINpu_^#5r?#2@4JN#`?WXTuyIVa~u{{%wZoLVd6IAjf*T`njxgN@xNA Dt|(dA literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-colorful.png b/tests/ref/grid-subheaders-colorful.png new file mode 100644 index 0000000000000000000000000000000000000000..38bb4ad8b88a2f3735b6f7eac0863b2e9102df58 GIT binary patch literal 11005 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 JY8kVz{{uIvZ$tn9 literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-demo.png b/tests/ref/grid-subheaders-demo.png new file mode 100644 index 0000000000000000000000000000000000000000..ec1cd998c9aa4e74c7ecc16758a7c172d998cebd GIT binary patch literal 5064 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 literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-multi-page-row-right-after-with-footer.png b/tests/ref/grid-subheaders-multi-page-row-right-after-with-footer.png new file mode 100644 index 0000000000000000000000000000000000000000..119a2c22b0e9484962bc258edd4a7fb488f092b5 GIT binary patch literal 1207 zcmV;o1W5adP)FMd#*498kKp7bsczAegYirNX&+F^!$;rvSzP>LnFH%xc zPEJnZ;^I0wI&E!jhlhu>w6vL-ne_DZ;Nalf+uMPGfni}`_xJZ9At5$4Hqg+}jg5`_ z`}^kR=0-+FudlCJSy{os!L_xue0+R4IXQWGdCbhrn3$McTwLep=dG=+*VosMj*i^i z+}hgOS65eqgM+xZxNK}}u&}U7N=k-?hU7Xhuq-7}QBlgu%Duh4X=!Qm^Yg2#tIp2O zh=_>l>gq#7Ln$dK^*%H4@$r(9k`WOR;o;$;qN3m5-!wEdUteG4<>e?SC}w76V`F0` zCMGd4F(4oyH#ax#?(SV(T`VjtJUl!}NlB5Bk!WaWv9Yo6@bIjxta^HSq@<*jl$4*J zpMrvdg@uKqqoZ+gao*nE{QUftm6g=g)KgPaDk>^gR#vB{r#m}4etv$tyStZ{mu_xu zv$M0+)zyiKiE?ssxw*OB-QD^5`RM5A`1tr`Wo1W4M@vgfp`oFVkB_RVs*sS7*x12L?Ck8#&CNkULH73crKP3W+1d5=_5S|;00000_NVdy00MVOL_t(|+U?p^ zS6e|Ch2h;O5~RTzS^~64i%Y%K-QA74QN!J(1q#FejAScQbip_SzXQpr2V_*f|f^iDh9CLqB(V}gh~ zMNvq&d(Sm(gMjLMx2+Nabb83pO-=Y^!r$;w5xVOBM?zuiLkO^!4D=(#j6+1yI+70w z6LFXzVRQsCj6c%SAz|D97-pDZh8bpj{B9(oE#a>a-aHWzhh!X*ZK@$zcvWafW)@q%=no!{39lbMkz4#_wq(<1!r z)=h~p;Jj^yjIaU;1B0Ix_$)aFW8-{&E^L-17qk$6g#~4C2{jG)5(;|oq@+%HnuEgXRjusSNcK>VKl86?OFvXrObbY8b{q9J$u)vwg1< z)|%S#qtJw2frMmPmV%~}2E>9*Q56`AKoe*5=c<%Yqy31ztoeM~W$a%GFG33q#}HOt zSdjh!Z%}zjZHX_$TN4|fUyf5Fikm|p^?_(4aJ_v*RPe>>@VAUzn4~*_$Ox{ zKm%=8uByPrBud+#yXtCjA%uea(7+mGEWY=PedDHBxMvPBW@ifd*hfS}L_|cik$;(T VJ^Ny!_+9`2002ovPDHLkV1fl)U(5gi literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-multi-page-row-right-after.png b/tests/ref/grid-subheaders-multi-page-row-right-after.png new file mode 100644 index 0000000000000000000000000000000000000000..c9e30869c4111d653e4a9045c70bd03108b62d61 GIT binary patch literal 1127 zcmV-t1ep7YP)StYpW@=;US3|&(b0f_fU>f(o}Qkgqoaq1hu`1dy}i9@X=$mcsc~^} zQ&Ur2U0qsQT5xc1=H}+x+uO>@%0WRvudlDRwzkjD&&kQju&}T^JUo_xJbx{ryc%O(Y~F zsHmu1TwIKdjE;_u@bK^(8ylRQob>ec`#&|xGb-HN+?$)5>pL+X9v)y|V5_UE$H&K@ zprA}lOlD?gjg5_hf`T+OG?|&1XlQ7LhKA?o=T=r$`uh63yu9k_>ZhltuCA_XYHE^_ zlEJ~j_V)HSH#aOSEJj8~Zf*(m{>FMd0mzQ&MbB~XYbaZsX!^8Rc`K6_$&CSjA z_4WMx{Mp&r|3NnM%CzeM00KWrL_t(|+U?rMQ(93R#qkrGf=U-qBm%Z*VlU~v_uhLq zz1oc?LShIO1SyZ2KQ%M%;y&+ZhVxmx?RVbY-MI%uMD#x?!WFNVcJdWSK>wAz6jk@j!N~0d-#oJ(l1jA-jK|*Q{GSV5WLPGXyXoQm+h8bpcev_W?*ZPGC(n)Cxtl%Ed-d8-yL0apRM6e&Y(zYG79Hay tWuLnc^rJn>`vwsa5fKp)5fKr|!yiHa)bcA|uw4KE002ovPDHLkV1i+!I$i(( literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-multi-page-row-with-footer.png b/tests/ref/grid-subheaders-multi-page-row-with-footer.png new file mode 100644 index 0000000000000000000000000000000000000000..a440eac40523ab7dc093fe540dee131366f606b1 GIT binary patch literal 1345 zcmV-H1-|-;P)8SpI={J?d|Qbu&~q9)1soH zm6esfy}fO1ZR_jndwY9`h=^ljW6H|PprD|Ff`X=|raL=3<>loW85yjstYTtft*xy< zKtSs1>RMV_tE;QJy1I#piC$h_X=!PwsHiF`Dr#zKzP`RMFE8HS-qzOE*VorsSy_#Z zjq~&KaBy(q;^LN;mU?=6wzjr1GBTW;oEsY(adB}pG&DXwK2A9R(PfScq zl9H0e#l_Cf&QnuUii(O>R#sC@3hDl$3*mgP53@+uPfH zeSKYBU14Ei#Kgo+O-)KlN;Ne#q@<*qo13+@wRU!P;NakfhK6fvYq+?$zrVlZINJvQJfPg|mLYJ49 zrKP2di;K6nx6RGX*x1FM?L_4fAmK|w*n z!otSJ#&2(L$jHb5002(Q6gmI^0~Sd{K~#9!?b=maR8bfO;N`$Dz%UX+t8_Qmq=Jgw z-GzY?N=r*9-5oRjQ!mf3f57(vv);RPwVrb>_Ql={L_~CuY)()7g@(ELg(YZMUJO49 z4UrKpCp1JA9zPZUe^TNKtp^R- zQgPdJD0ti1&C`#;lm9n3q@nF#CJl+u1Buwc0DZK z^+ziZu`?Te!?*a!Sd&n5L`>7(qdUD160TQ`HSaB+=!S%mJeKjO=IF*KE zJ09rpdScx=#oNC}Ms>azQ^88bi$PGZJGrxNhk`%LD>nF-{()I&=kzBI1yYLoy;F4#_wq zk*|Va}C?m{aMj2~P`*|~pI9HeyZ^pwdB-9pYMiEoF ziR)$*F>ZIRnNdb!#HtxZeE-(&*b}oI5Has*Yxa$^gU=x2F?0-m3{un zM$XSbGm6O1+ln`%h_q8dnPxoPqGF0t%_t%+_DZ@LWn8*!Mj4?;H=@iaqM-4$(>LZN zB|}E%W)d{mMxf&zAFicrv_MB{2OqA9h=_=Yh)CCeRs*%|C#X8f00000NkvXXu0mjf D`7EVb literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-multi-page-row.png b/tests/ref/grid-subheaders-multi-page-row.png new file mode 100644 index 0000000000000000000000000000000000000000..637ca3fb10f78af61372f9fdc88a74a7b3a19d0a GIT binary patch literal 1173 zcmV;G1Zw+U3^`T5=5-TeIg!otFrmzSBDnMX%Qxw*NCiHUM@a#U1Q`1ttJ($fC^ z{+7AJoyy9} z)6>(Yrl$7x_NS+(goK3SuUGVVmYinzil$2>{X zDk>^!YHF30l}JcPq@<*Fc6Qg-*I8Lvjg5`7v$Jk)Zg_Zjyu7^c@9$SvSG2UWNl8h> z#KgmX?;}FMc9OG}H3i-(7Y)YR1b`}?D#qvq!3+1c6s{r&$zHit9?(EtDfi%CR5RCwC$+C^7e zVHAbo-56eiyIZMHv`XFGixg^5>PV5|9vqSz>F@3ECf}TM+GIboJ$hF%xGQo%L`46S ziSeo5&@lO{a2p!d^9%N&VJp{@1`UCd^rK-Uk_Voo&MJF0dLmUGNO;)oUI^Q>__kGA z2@wU)XAtF1Rk;NbHS?2*QcxL$h|<=T?k?EeSLPt%W}CO`6{2K7hQo_f~7RLfHG*8rg$F7+b^DPzi^g)#`-N1D$Z^4V|#(8o&n# z!wfUbFvAQpEF0!OfJ8*5BTD$r6A|%{jE7|Z)FIhon$jUzvJ*NS&XA=O{yF=7MduYG zWl1TVp9cl5`Q#!m6#Q7v%0^<@EY5+3RX+}(Vfiz3EG=RW8r&ZT24Uk*iVQQ%FvAQp z%rL_YGt4l<3^U9y!@O=HA|f7=@sNy&h=*i6B;z3&56O5)#zQh5lJSs?hh*_CVooXN z&JS#xQ$*v1H$HR9sJs|4r-+)0`Z9Bhs6C!nYR>o;sID6^r-)$a{ir!*ynSa*8AyO7=9ICyWlkA$U#yen n6frxtTVhOpIwB$>qQmePlc&co57D5?00000NkvXXu0mjfH6mT? literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-multi-page-rowspan-gutter.png b/tests/ref/grid-subheaders-multi-page-rowspan-gutter.png new file mode 100644 index 0000000000000000000000000000000000000000..53beeb02e4183db6499534946ce75b5180d79e52 GIT binary patch literal 1560 zcmV+z2Iu*SP)wlL_|b3Ha6DQ)<{T5aBy(N#l_p(+s@9; zK|w($CnrEaK!SpT#KgqCy}gW#jP331@bK_7G&Fg6d2MZNTU%TD`uduhnj#`17#J9t znVGS%v1w^(?(Xh|g@x+kRH^z`&SJw4*$;x#ojr>CbQBO^02GtA7)O-)T185uD# zF-l5G!NI|-tgJXVIBIHYVq#*Ro}QMLmXni{;NakVe0*A3TBD<*=jZ49`}=iub==(C zj*gDW$;syC=AE6LqN1XEdwa03u-Dhu;o;%3va)k?b98icrlzLx@$rw3kByCuSy@@j z%gY7^231v6>FMd{=;(@yit_UE)6>(bsi}j5gIHKt-{0SagoL!TwD|b={QUgn_3U)YQ~yXlR$0m(bAAFE1|#2M4IAs8?54|3Nl-+>KQL00XE=L_t(|+U?qDQ&VRc z#__Ahh)D<`2|GwC$|8shwHBo;uDEOWec$(eb!(Mc!78GtAjlS3k{nBlCHV0=sWXX> zhj;QkH__`i!<+lfeV@XFwe8A<-KC+H}}82Hb1~;C~Hs z9_V4+QCWcHIo#upwS;DFH6A|nO2mQ|6SJy`|4&1+$)CGieB}@k5z)eUOb9=DA|g&; zIe}eN6WGuip$TmLI)MqS?4E0VZEb}}xD0hgQhTm3q435PFfU%RbxT9y{t0YCXacJk z=kGHjT6C`bUJMBZStx;ot5L|fehniK;3=-09OKOS(Pj0czf)o(Q040VL1diZk6>=f zP&l=U?HDgKK2AukVmk+|Gv`f*yK;bIM-zP)Oox-J*p3r`b;*U{bB4mHRqWoTK=90} zz7t(%4TbF!SVLhuh8bSK@S*M7M8f&S!8VaFin@Om37<-QepDo!IC=5AYCcX3GrSPt zz(%3)3tpjcc4+4gnedSpTla~Cy~x`y6#k-EAZ!)3p6ZFESjNH=L$gD(X)eCGGCb^9 zrgGR$JE27|OBJ-8^7%(7BH{#=6Idc5;sllxSWaL$f#n326Idc5PGC8KZNxJG0000< KMNUMnLSTZPN*i4O literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-multi-page-rowspan-right-after.png b/tests/ref/grid-subheaders-multi-page-rowspan-right-after.png new file mode 100644 index 0000000000000000000000000000000000000000..5fe1eacf2221bbddcc5933baeb8cf9ad3ab4767c GIT binary patch literal 1421 zcmV;81#+7khsnpce*x1;Mi;K6nx1OG!va+&hXlVWY{o>-{qobpEczAbrcW!QO ze}8{*adB&FYw79f+S=MyR#u0Hhu`1d%*@P*iHSTsJeir9yu7^d@bImzt;)*Ewzjs< z&(C9HW3H~QK|w)aU|_$$zley4q@<*Fc6OAMlpzLXJ=&tW@b!GOgT9@ z#KgoiGc(}e;Cp*}_xJZCBqZ(a?JX@WJv}}9KQ;99^p1{>oSdA5goL`fy2>*uQBhH* zrl#vVF|4et<>lo~O-=9b?>;_0jEsyXCMHi$Pgz-6(9qCIN=hjyDdXegFE1}HE-uBz z#jC5UprD|$v$MXwz9J$bH#aw2TwGUISA~Uz$H&Lp+uNO;oisEwEG#U7f`YHFuW)d1 zT3TALu&{=PhPb%6o12??d3nLX!A3?#l9G~|nwqDlr)6bjYHDiL)zxHVWWBw;fPjF} z(b1}^s`&W$US3`*Dk@!FT~kw2_V)JZ=;)W1myeH+b8~Y`OG}}lp^%V}=H}+Kw6w{| z$!Te6xw*M=a&mrten&?~ySuyH-QD{7`uX|!pP!#}bacbR!`a!{`}_O+{QUnxHZdQB zasU7W3rR#lRCwC$+E-H(VE~2UV-g_-goHq%lu!h0pkOZu_JY0l-h1zr4$`YAy>}ud zBv}%Fn={-vk{R#z`*1kV#ooTN^9TC-Aj9amV+=A_ zi++wwVm5i;@S4Lg-Jrv{!|a8c?zcG)Ok%pB&z;t&38<=nX$XAvEwT7RU8_Tb;H%v3 zg1Q>)U2S^!+UkJ1bgeD=X!uHo$~0QQmpA{N>fsJkGkm0egzDjTtWzL?MvJt~lEPbd zLxQ!5>1mi{ypSSzOH3OWQ2!x&}l@5 zm&61ib(J6H>Yx=F-Wz30L{^J-Ty%IK0?53Qc2F-mT(Jlfmo5Vsu7}P~V7PN>!X8w% z*S^kv8~DmD-~=Y4B|1pf040hnDPBf=p= z;4&c6tBM`ZABnTq(tVgfnO2>OG3Sn-?X=B_jh|pUHx01XMxf`c`4P3YgLCRmQ z4UJn6dhUEm>LF*+3fsf{JnN?_2r#_Hx*TDa6Ii`|Qdt!pY-vEAgYO$eL_|bHL_|bH bBp-eQltzz?4j@AQ00000NkvXXu0mjf^hMJa literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-multi-page-rowspan-with-footer.png b/tests/ref/grid-subheaders-multi-page-rowspan-with-footer.png new file mode 100644 index 0000000000000000000000000000000000000000..b91046675c4e7b691ee56ce05c30bed9dd747eb5 GIT binary patch literal 1190 zcmV;X1X=ruP)+1bLv!tUFMdaySsjVespwnw6wH@goKZekNNres;a7Ta&o!3xr2j) z+S=M*Utg7#l`1MKtgNiOyu6>EpOTW2-rnAGb92DJz=wy2ZfKPvGcz;b;NT)6B2-jVA0HpJwY9}EDa*^t@bK{Q@$vNZ^oWRv&d$!1l$2|0Yb7Nm z!^6XyCm?ej80zZk#KgpAW@b4#Ia5LnFYoW~c6N5-*J3Bk=?d|gN^8Nk&V`F2WprE9rq{ha^LPA2Orlxy)dl?xS`}_NXf`aDe=J@#d z($dnmx3`Oni<_I9{QUf~va;*z>p(z2o}QlM$Y8y4#?L_6!U6AVAZ>c~dAnehUId z?%Z}F&jcYNIc^+ptQGCGf~tU^S&+km`M^xbXBK9w-~KOM|I)tLF5eX0?x zs4B`nl~`QZ)AX-%^3lWR2K-0A{)Ru=eSudU65-mN_!(CiPOU}E7=CXKs16&~QK*07 zcD+^|*0loFk1-F`;now$t2gdi^r7LC80c3RhRM+g&1^!rtM)>j#8Yi@;9^{5*f2eY zvL%D^|M>jS;2RvDm^!UG>^K693^>jGs>2=|kmv$%%Aq!F@3A=%_C-QNM`v1^6@~!| z0DlUGvDmfiO2e5LW;iFq*jO)xh$)mog7)%C zh5WJ+S%rkoeOQBpy|s{0SA!}@sNa8}4QYHs&oILbGt4l<3^U9y!wfUbFvA>@aY)7? z8HZ#Xk`WPyWE_$a5phVyAsL5c9FlQJ#vvJpWE_%lNX9qx3^UB|mJeUK|3D;+Ug#kq z-qQ#XiF3tbAH4D`eECu%M05}8`C5dCh=_=Yh$uh&22a+>M5^?6$^ZZW07*qoM6N<$ Eg8dy_>;M1& literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-multi-page-rowspan.png b/tests/ref/grid-subheaders-multi-page-rowspan.png new file mode 100644 index 0000000000000000000000000000000000000000..342b05695b9a8dd07766cad52ab259497b9c76da GIT binary patch literal 1048 zcmV+z1n2vSP)&HKtN4RO-xKosHmvplp8R#uyvo8sc)ZEbC0 zVq*IG`p(YImzS4DMn;sBlxu5ih=_>8!^7Iz+9Dz%R8&;U%geH|vWJI<^z`)H+}u)9 zQY9rN?d|Ou85w3~W;r=IKR-X5oSgCT@$m5QiHV8o>gvSA#NpxLRaI3}Q&U}CU1@1) z%F4>Iv9aLb;Ip%{YHDgoNJw~icy4ZPJ3BivGBR;-aX~>rJUl#eb92DJz~|@ZH#awB zWo56gue`jxl9H14_V(}Z@1mljjg5_2Sy|7|&(YD*WMpKyxw&$3a&&ZbpP!%U>FHx* zV@pd*M@L6~etx^VyV%&+p`oGG)zzk^rb0qO{r&xrkdVE-y?c9m`}_OC!otnX&Ft*# zprD}L-QDZ!>)F}aSXfx!-{1WF{BLh>$jHb500175wWk080uf0>K~#9!?b=sck^vaT z@jEsM1SUfi_ipdK_ukufS-C13=ExYb1rzA|cGLxNb=vco+`qH?=zlSe-uD3!5&cgb z4VI74ko_M01P!@FWDXj>E#&8+!5I4dWf}EkTfW#-sLa9qVj&L+{(!G>xiYQK(#aG= zM7DRL%G1J98Y1q+v#1hDnIIy0*BR(Wh41`1h}hNT>fVVe*C3-@LbZwUs8slNuoV@r z#~>BHgW1DSP%goKNGaUB7Ye2h99BvZ$3-? SQMQf%0000NFDhq1R)ee zK7tJh1r3S_ZBY!Rfnr0Yv{l+>`ZdS3OI`4i%$Yy%WX`!a_ujd0CW$~#62lBL%rL|M z0f)NpEkoeNZ}vP0?A5ksLg4C2hXMlIY9{Wi$!b*paKtxu5paKUM2RVN`_*lNzzb=M z5ZGL>le~(6i*v-)Ed>0bgoq&UWnCWzzDc>Wpi*}|0Kw&-5%ALOCtY&ebHy=msJt@= z0_(bI8v>Vhf_glCZeHY!0{9N7?`vKfqm6Z4E*StXx2!GH%_I=?~ooBO@J1OwPXthrYyq1zg}VB zrL0JB4*@r)M8{PKn9le+A`rN>zMUX&_^1a1QyoFzsIY{AU4wj^$}qzW{}YUQxW^1L z%rL_oB;z32p$d|fDL_H88CdeA#2v%H=LgewKmine6A0G!A>jG6nOh0RQa6>62LeY_nzy{?IuN)vMI2H}09Z;58iSBhH|Ye>Wyp~&3`~`T zfyd@Ba7#7^$v8;HK{5`KagdCIWE>=uagglz^ezOZhw1M?L9()01njR9PC;N(D-}ZE z(h3VvFv$%DYIXoe30Q{|fk1YXDu z--W=|fj+amrjyNJG(q4_cVh?8wHVFJOS5Wf7{6OHIx&}1_{vus+LSWKA zBM9ucLV5^1)h4v@bYhrch8bp-+gE2>2D~)H{$2{2VR`dbKsoAc`6L53c-Q}3l{tj@UQ;U zuMv3iZRP+1FLuUvAaFF1tw-RgGvlo@>uOhj$*$z`1UwKZH8WAi_kK16eo;4xz{&8} zz%&6L-RDiu67aL*W`@AKyYDe@lkI;3McsD}f=|98;Pm0keY$h43VgQG8*X+0?1F{c z1tjW@?p{OS zOSC!w{!!&dKO#||cn~uL&TW0pz}938oF8Q1)E+;dCE)AZ%E<@;TW$VE8G+-GTZX`; zO-l@H=M902T^RvHhf&~i}ELgB$!GZ+~7Q9B_KRD@F&Bi%_rvLx|07*qoM6N<$f+}+q ARR910 literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-non-repeating-header-before-multi-page-row.png b/tests/ref/grid-subheaders-non-repeating-header-before-multi-page-row.png new file mode 100644 index 0000000000000000000000000000000000000000..3db97f782ed52b337bcbaf7a0ddc7354d76feee8 GIT binary patch literal 410 zcmeAS@N?(olHy`uVBq!ia0vp^6+m3c0VEi%2k%EaktaqI0hTW_aCiDQNP zoxOvd9a97ZMe_7`9bKF~{KQ(I7rnmHf z?b46{E}Nb+TX-z2J8akUROD{k@aFQXmklBBN|?{{SA=)7etrGXO=3L^J_i>0FS5tlc=`#}>EAxj`_Xnh<|}veYTmTU z0~>aAYj;n5mgOYOc9?NX#0QQ1bIqM8EQcQS^BtJ#w!YKze(>{7i;C=50_(XS1}n^V k?0M~iVg=N$hw&`+%$ENz`o#vX`vnSgPgg&ebxsLQ0F=zDPyhe` literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-non-repeating-orphan-prevention.png b/tests/ref/grid-subheaders-non-repeating-orphan-prevention.png new file mode 100644 index 0000000000000000000000000000000000000000..0f37c5d1714e88a400f8e326333124a79d7daec6 GIT binary patch literal 347 zcmeAS@N?(olHy`uVBq!ia0vp^6+pa@14uAv+Nr!`U|>}8ba4!+xb^nhelMp&iDQMw z7f5I5axBW`SY)_j<-(TB-1T>3!D zZqky4@4VGEG^I-(DE=DvRI2a0NOkw@74yn?eoyz`Xa7-d&jrIfw`DzoO0;J`|7Eqg zF?b>G9bZ5HWab@Gb#*e&WDifBd*^&-h@G5uoAueS&AMBqFK)Z?Na^!E^G5Hhw-$5S xc-O_xIFNty;!e((il2MAB4!@I3Z!}U8CPDIc=*0T)i+S6dAjqjx4+B z7``jhEjUP8kN(688Xk0Mvb?eRzo7U2&N7yLa~FLP z&0vVQ+`2~bM9_8f8LUj~<+_OpI~64=3V4{!pJ}c4m0~D+JH4EF!cx0ud14$n_P4ql zUB!Jk-+49wqCUH4lE zeCQ6zeIv&-?Mv!pw#MDwRxC`btLu{z3^`ctt%*5uV5?=<-(77#j#zE1ZF}>vHSxlP z8UAGn2`7bkWNNw^=SEJvz{a$1zL4y$jt14U_oW&S_?qf6HEu8aZLlGPo275oT#1UE z?f;hvsrmfqPq=c}R?TOIL1J49&&hwQC%joDCo-w`(5dN0@6FikZWL#TiY$TYa1vUv6>kF9JCdsiC0YSHBesF{dgKa(3A_glz3vBBqr@d!9 z%DK(pyN9kIHq-S<(G zS}e!PA2> zEc1SDn!(jrt|gtMSa5JdBy*#0tMm5ormR)_Xa3E$7bpl{p)R@k3h(r7EKL5cXB`r1 zf0exuVmh~Dkxs7CgOfUfjR(Y6Ss4~gT2jNHTl(2qV2*KjL$CRKv(GbM2RvA0u+%(~ z^+48+)mK>~Uv+8Cb$GB>dA3BvCl==Y(#H>6?L2k7N|a%?->EMQDJ#$GUgKoBHvjFx z148eP9%zheJRqHEEwMwzP0}ZvaYNCe>&eY;3LUHWIwZW_$jYN*&ceK&H{roOi}$M- z8TWSv9G6#4=-hN=wZIPTy}k+?{{OIJo95Mcps%TvCuY`y7sa6rZ#bquTtC5fpDBx# zN|+O`M8qQ&=J$J#95`mtf4Z+>eoH00vET-mcV<-z1-YKh4hi1_rP-z>H6DnYE65zZ z>!;~dgTzDiEQ|Ht8G=I4SS8uxn}2IcxygbFIXq$eQX*?5euR{NG1G9k7}oIMlpl+Y zplO+};{p5fXICFssm!|{?$#a3kkdQCP&sAgdMStA2E!r==59BG4VRi4&vNFqrSP1T zm}Cev_Sl3TMM-BPGyyFYWA&<^Kih$>0LEXM$1z(qpBV;;AXPuyH#s-|Jzd90daCn-^RIrEa_HQWYCQ0)dr|8F<NV})yL!~XZw?`iG+8n9)Q!g+|n>~9`m`ux76ZV+4AhR$PZF{~edSb4< z>-YU{`c%1e-uwCof$#m_+f7ez)}F54@M&xQNAWzus@vf>O=5lMh_{6Wh*Y z$Nl@wwTzQCbKbt+ocr5jLzcRN)$+*;xZe4w>{H!vU-n#g*xq*?4<>G6*wfWwT0GmQ@xW`{?(3EnCj859@45M`&-cLb zl}#*mbBokfKU9QmdG)j7_o{~Z-1`t`629ftjKz(wr%o&Q_4u@nO>SiG zw5d<}8X@4nTo`XY4-oixxFNw~^)QiRvci)Dfw1s+OA0bP00Av2dkhVLKv5DI{s)Rd k_~j6DkI6ElfY*uN*$+Mp`BbOox(XDdp00i_>zopr0QZO5LI3~& literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-repeat-non-consecutive.png b/tests/ref/grid-subheaders-repeat-non-consecutive.png new file mode 100644 index 0000000000000000000000000000000000000000..2e0fe2364549f3c44f6b9758a32e010c55566fcf GIT binary patch literal 599 zcmV-d0;v6oP)c=3GC1Lxs6?;3+vjbOoo1q)sV{I~1U zTLd2YFm@P$OUd+31WsfMQ3M{nJalGaUA@)c3X7B93AiUzX=Fy7-uu-M_*G;CfwS?M z&^Q5~JmAKs2>4}_nIZ6=wr&RAZ-<^iqwc&4!ROu(aB%!)r@pyX1-?@2`Wrm}`*8Vw z85wnJTZbX=+=eLzwhP9eq85=uM@CErc)k) zd*TlaffqObVPH!d0{={WX5ecLE_>b+PaL+)M`Vvr3_*v)L@3X|7BdE(d&Iy&tWy3# zz&C2EgQp1C>VMs+6uXbJ0C*_5fV0 zvs{3T`d}Vv1cLeu16$M>*mW^*pV8Lq({txa)NE%S9;f4SJ002ovPDHLkV1j*%2t@z@ literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-repeat-replace-didnt-fit-once.png b/tests/ref/grid-subheaders-repeat-replace-didnt-fit-once.png new file mode 100644 index 0000000000000000000000000000000000000000..df984bd60952a61f1d96d34020c6b270b4d94416 GIT binary patch literal 877 zcmeAS@N?(olHy`uVBq!ia0vp^6$}iF&Ky7n!ivHFc{^#b(azRflv+zkgid@L+rE zW`P@V!sq6?AJ{te)E^_Jc`JnlIcm1laxjN`eAT`X^!*;|LEd7H8?_G;_4gO&a@MH( zORl`cuwj|^7j*^iyVvrBIArc`>}h=3mhj-CCeNM1pvv4U3>(VV?~!iS`6?Jc-67$3 zqBfgYtQ2#)6)RJ{t@P0YXW6HAO7SviSDURBJkcXPUy7Bfy{z`gfucBr5050+N!Q68E4w#K_lS00gKs+%x<-j0@rr3%+3HyxOJ_AXPSd6}ER1`|#ezu9*r zDvlni4+e#ryTPl6JwS%f3`I%jP5-r2c89TWot(4b(^9=HosHk4`C1$qyB6}Yylxlx zP_poIPg6s^i)~a{tWbZxR-YO#L;I(rFWYp!YKpHAED*n8ut9{AE$o*;!GfA*pm|1Z zDLf}7CK;+EdmNk4qj=N4jd}Xd({hR62=kd?0Ad&eCEY+wkjjs}DNXi%ubQ|Tm7k~a zIX;;7d{WZ^UbYK8w!02Ga_DewKXtp);Ne@o^+^vR-ez#JT;unYVY)8uXj~VZu)X0C zOZ24YGeGjIB`j_xtZO;&wpOU+K=_#jlGfiUk11~W@$35L*A|t%f5SL(`0l=LJg}+2 zNnt~j%txmO>Q|R(-SdlP;y2lP&H2IisFOl0VuzIjn5unv1b(DWU#Y#jtn`AZEqkN2 z%-XX87Q%Yx91=7?w<~PenX`q7X?ari{Yq;=U?>_m8|CSN{FesyGBgZ0gw@Uf0|3Z_ o`WO@(U^!6Ig@%Oizq6mYe_r3Eap7~te^5U1boFyt=akR{0GgD2aR2}S literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-repeat-replace-double-orphan.png b/tests/ref/grid-subheaders-repeat-replace-double-orphan.png new file mode 100644 index 0000000000000000000000000000000000000000..e340e6817a8f7fc875ed35f3cc6241b0980b44c3 GIT binary patch literal 950 zcmeAS@N?(olHy`uVBq!ia0vp^6$}iF&Ky7n!?ODXU(>hx(ZP{ovSFKC0 zG^>r>#ze;QdE(^ze*5$59mOjjF2C{oJO7>=-z$%8POsjqz-a5(!YQogQ^CJs^~ZA- zEOLMCrZP2N*Um~-END!$lVXbIs$O@V?aI1;mhYo~v^GRH-}Rgkczs^9D2I)VC0Ao_ zX7<6$92Sok8SU+9Xy4?*!Zd$n&cOqnlkV_Kl^(Vgm}A}6aQ6B*X0m~H8LN!-B!qs`QW53aHA?ii1~Er!2{f}5<8U78K*lP zcz^gxqdP~0Qh13LOP+7G7{?pyJ+(+b?@N;8E|zrw|G&o<-mfgl}$8;i}8A51m- zH_K~=!NdJcf&ABJ7$hEQk(^|x@_V_{ra2h_9@p5l<2S8|(}g+be$~SHs%GOX+Przb0{B;h1rlS0Z8&E3DlM+CKlRb`2=z$6Rn?B)8;xqB5;CO@DUDykX#2FwZ|CDduOT6F1&vbho&)kLs zOY@KLHWqJPU^79}i`g`9EtYfdn0kLXE?iRQa$wz* z-R50PuO@t0{9w6jrKUi|QQ^&ujjH=E^Eb9nF3)#*aPqqDu>-q2BT9VV3hcP|YXcwC z=`)p$2lhqGW@WPXzoz~%X`g;Uabctx%fDsw;#v+YTFWH*G~JcE(f<02Y^Ln+^|RQH zD{NS{eGLoK@9Rl_1tK1$uNBx~_b*n7rOf)x?8dKUuXe__-n=@0kEXIf#62+%mTmIi z%N!nT3#v^_c>79rcd{^3_Ll8NiXYmvPq8+t-fd~*^(>YYsK~rva!#8E7!#bvDvzHj v`2a%(Yw%%&Csg2aCny@h!D#T%-ex~zL|E^$9WSRbf-ZS_X}6cPwu~Q`45<{pMC$x?#Hv4FP<muvFlIYr&tcs(eUE3}9l< zO8cBHQt)Q6v2c8Fanui`rfmPN2M$ZQ?p-%q(a0ZpGx%cY_c)=0rt3uuRxH{&U7qp5 zRaWzyf;R71#y4V1@|LQE-@Wt9kn>M=Yz*^l$;$`$=Vv~Mu;xF(bd<#EiHQ~r2 zwtH3ex7V!b%E%WxsN2l-qjKT@6^k3hCk5--GaayOw9cNZE*`@;=SIrE2~Sq>`c-2<2QD&a^5*F2i-QdY84iUA5s7;En)Q7d%Rb}!bLx${5otmen``TWn5tLX zJUzo%QF`#Y^eb(Zf|ShX<(xUa+kKhZ`ER*$%6$07W2pXM&H?$F!;gh_DA#NaWxBn5 z@3O}3$AO%8_WpO{1epzTCCIH%$3p#-T(+ z@z?3)6ehoOR>8&6S#!mnyB0jQ6qH#TdXU}Lk+MYK@0~J4xL*HO;i(;XKa=`Rc|}7XK%o-s<(Cckj8?m)9|rZClrJX;Ps1 z*Pp&j%gy{V60DP@EG$-E&;SS&C4unfv6$u8!USbjPAyefDayO& z^UHOtfuav{#Sh9lvi{j;Q*GF&Z79E(|dv3N^IK%R7kt%AEUq1@Y_k3{U zn<8(H=(5JSa=ddaCS5-8tY|04NmRHarX>Xe!43d1K@5n{8eydUz>W%ih z+0#@vRKB0Z;wNLYo<;5(Yt0&AQ0N+Lc<`6`$%l)I+#PcLpakvd>gTe~DWM4f(wSPh literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-repeat-replace-gutter.png b/tests/ref/grid-subheaders-repeat-replace-gutter.png new file mode 100644 index 0000000000000000000000000000000000000000..e87e3b5bf5efeaf190d41a7a883f2ef7cd8fb0f0 GIT binary patch literal 782 zcmeAS@N?(olHy`uVBq!ia0vp^6$}iF&Ky7n!(gIh3llhEO1 zC0An3cw{wCV>xND=hd0&a=wEX8hlyvUtDfhtGGeF3M!h@jd4Q^<#E35niDj)9aZkD=n%9r_j-Gu{z^4uxS57x0g4_>U@aCrBd z;{GSA_GRpPFKd^^O=N&?uUTf1KN%8>Zg9G zZeZ1)&C(~X!lqL2`>ccHW0pB$7E7Dt!(#O(9w^TK?NS>2bM_P#IZeMi6AnC%o%{EU fp#cggusUylxa_j{+5DBaK?&W{)z4*}Q$iB}o+4uK literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-repeat-replace-multiple-levels.png b/tests/ref/grid-subheaders-repeat-replace-multiple-levels.png new file mode 100644 index 0000000000000000000000000000000000000000..d6f691e43d734335c994184c2bbf699a79f74ee7 GIT binary patch literal 877 zcmV-z1CsoSP)0E zc1N>{fNy8{S2q#x`+O0Bz?V#&7&udMra(j8@(2VME+F8=o6lO*cIFCW;KM>|76dl6 z$~FYPV+GCe#F?dG2rMgY`|4Uw-7oy_%nAT@B*@$lWT-E!GzbK?o$tlK5*Nh4k$Qo^ z->!Qwurb9yqE!H}|5T{g4Vhy@hgl%7>%=k!mK%bB*M~50Us~|19RUx^$K4oM$^-%jEsGdmR~|2@@tvm?0VGm@r|& zgc*`CB*T4?tV`1kdaHF)5IC@`g}}S{^YIY4Y)}dy@V!P5JeSasl!t)D52<%PvjYHs$2Nu1$jy@3-{JgX%7XohuQ}!XS6u*u@;MpACeF(hV z-Dy+Ta&nm4dLgjYRoM)I<C$wH2S#C2!Y>* za_1nh^cw^M@0|ndYl7SAln00000NkvXXu0mjf Du*`r4 literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-repeat-replace-orphan.png b/tests/ref/grid-subheaders-repeat-replace-orphan.png new file mode 100644 index 0000000000000000000000000000000000000000..c28e9d4ff5664d8c7384bdffc74f091d7fdd2265 GIT binary patch literal 939 zcmeAS@N?(olHy`uVBq!ia0vp^6$}iF&Ky7n!Sh*_Ws2yNNPb`<;Hmkg`($j5!C(wmI*Q z9$3Vg@StF?!-K7}Z%gdZ36wk&#JHhI>&EN0HyigI))2T+n#9L^I``lKZDxrb`j#7V zoDaNDzIt&vN5muZ9xax4=d#2(WUMM18ka^XeCVqaWuCrVYZ{a2$8OW4e|$F|?{;3` zoTPcHtx+}7;KRRco*e&g)wUczB7G7J*crX=Yq12?Hu*M`)YparV#3jkmqEKPh|&|N2OaMa+4wL&Eb^Lx%?s*@B%@e7`Qs&5>fd-WM{( z@xh0%APFY#QG1(fY8V^{vbGA3VG|H8@u_pRl+kx!lZIX6}1VHiNY{VxH3KC;HK?e#; n4qJ-QM*xC>MIV`njxgN@xNAjUttt literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-repeat-replace-short-lived.png b/tests/ref/grid-subheaders-repeat-replace-short-lived.png new file mode 100644 index 0000000000000000000000000000000000000000..d041888c8010e6fd37717a34b3404045ea65e90d GIT binary patch literal 795 zcmeAS@N?(olHy`uVBq!ia0vp^6+rxn14u9|R*Bodz`*p#)5S5Q;?~<+x!#i#C5{#D zcNM5@i1Tn%5fxd|5+>vrp{&|+MSSY6g2g7mhm_U|ZWg-k6uqltgGTA3d!- zcEu^dQqbJ-L5bdrz}wfO4GWZ_McCRmITXBi-G1ra&Lrk}b}TX>zrP<9+_2WsV7r}t z*V@GbKTaN;&CJOXv-j=UxmGMO`;=c6C?&kVwn<5z>EDjbd-qrx`8#)WF`j=jec@w{ z9e-a;pVno-;1}g6-m&l|}L6fU5B?WA@^@R(rW;t%&{9tvqLBWAJosEm{w%TXwFhu-uoh-}LxIMj9 zaYOjiJ6oF?cvr3WeYIs$-LEA9A9_DuT`KV7@~3wiEYohaYjXTqr4q>8sQxZ;zSe7% zhner+wH|o(V5%R7&gFJiru9$SZ3KRlb*`Mj{yDT!SG+`u1`1QCum`U1=> za#_Y(HM3XQ@4k~Pu%k5MI!9x0@lQ<-9j$l)rrDw2lm#j@#UewlU0?ZslA!U`b15&g zIdryFH6EB~d(w{MPVvnv3)ZasC)cU#^dRi;O)n0a-si^@H%!@W@FC%2I~Rw|>$2X@ z9L~THIK*?(BBq5?SnbS&9z{uKBT$G)OfpnS_Be(|V28CJIQ$NQbpC1oD*fz6vRS4n R>k3e+_jL7hS?83{1OS<_WHSH& literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-repeat-replace-with-footer-orphan.png b/tests/ref/grid-subheaders-repeat-replace-with-footer-orphan.png new file mode 100644 index 0000000000000000000000000000000000000000..b50fae716c252da72a6a60daa32ac8eb18113a49 GIT binary patch literal 961 zcmeAS@N?(olHy`uVBq!ia0vp^6$}iF&Ky7n!h6T9hoLBLtq^Kg($^A(*`qx0=Sn{_68Zv5Er ztnk=jm&HrWES4;@Cai2zO(Z= zB{<({de6eNuQG7v4A%p-AC;II&x>41eBjaflSS%P7rTN*CI=&be0|Kpa=rBjB-y7t z(-37?ZRI|d+2~3oYgOZc)wk0OH!R^`tGnLY$gQujcc~VGnf$cIuoD6ErSt_kax8AP zG`eo@Y_#=fWvbu3siSdo)70;US`1-)yJo$aVClD7QGjtHjbUZEU!1j2O%ZtP9xW%l0Z@ZjkdNuD{f3neCn9?`$%`CI&i%>4{z zTNRysZag`c?jApo_*IOf#T0W zj26jBhAO|;J8in{xM0E>?y!AFZpTPiEckeXNy9<&6!XEKr5rodH@x1|)KIV6xAIn% zht2NQC26Y|ZahDlM+DF zlfeQv?;kqFC(P#O>DzwbbhV>muVYaM&wjwwNflg{mnUj17$Vmff zQ=jzL_=fGvwgcZ**QzTNeAB+!aKO`bfu-E#tJjH3ma!dthlveo$E=~kq}GVeA|99!dh1a_`5^EsNIPUimiBFfFvc^_5vcfjLd##}ac7Fn#2!dI6!wAA@C?kLLk&y*;QlIEJVKrBsj<%+~!kF#Db%~y0vf9fpzF(b`Pbgonv z|ISR^S(9$vDu|lacQ*I?{_+cs?EhMRZ#Vytc%AwB(RKP~+#TfQ54CU#tNoGx5O()( zzd*&O&oMGA+r&d#9TR-#Te2`o%YRumBV_BU{j+VkS99#Rd0n(w^3^JCZKlTK&1#Ap z^bWqV?r+F-3-Lnk}rX1mGZrk-uki&1j(}ByK=d-SsJ^z&OVC|P# z85}V&>e>Q7%6K#S*!$0ab4ZxEIzIe*SytssnbiRd8w?%Pb}2g7maU6X*znFrnoVqf zF3+1R)>VscFb(kq?;PWcNrE`ANdw;%k=J3 zxZ6CX1kR{+0SX1hPR$Mp=l|JDM0Bw*t z-^g3HQ^MlWR!*k#Z&>f{ENQ=&nBcx(1&^F{!r=qQ42|!L0Yk~>N4&wS!{qA7GB5C) zu2X`ht}sUq-}E33n}Bt?;n!EiUF`RBd@$+Y9~+K0TiYrXH@M8w5!j)&$(W02ciptdN1OfL)(}>D$$+IyQb^%L)~6-*EazS_*=D7MG(G5SIQF1|MW^gD|MpdUoVAVH zB$&#rf=UG<+9t6u<+oX|DHJ@8&0V!?on4&B9~*09J|_MA_I1t)C#xQ}9(cU5&syNd zg-c6v%pdDC*X vZL+KfbNStWT8jf+3w9~U*_^^{hw2$)R<{)Guzvj=lyyB_{an^LB{Ts5mQ$mD literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-repeat-replace.png b/tests/ref/grid-subheaders-repeat-replace.png new file mode 100644 index 0000000000000000000000000000000000000000..9fe729401af3dd9e8c9fb298a5f7b7baadd159e9 GIT binary patch literal 953 zcmeAS@N?(olHy`uVBq!ia0vp^6$}iF&Ky7n!T@gj{Qkbx3x4Y(`~;q$q6Tgc=lLzH2xN@)mCHq zqvs`C-PE9(wmGo%fbC3KrbcGU@`KB+YxnD>~EM<CQ7u1hR}(l0Ck~x2Al5e)s;X+Ztd*7G5b$Sq$8_36U&$7c-z-M~qp##6JOwHlg zW0yDU>1J1;AB599jy+WJnPHF!vh+uPO8tSxr3@@(DL!+W4~XU;;cooAb%F7`?l-TR z4m>hrcX~a^AoOLH!iNU+^9+qgXDAEYkbN%McwnyUu00oKy;9(+i;J4T*Jxh*(OMva z>pr{Uhm={*?O5j3-_7M%yM6M~jeGf-w%=GaOL2ovtSEFRFXa@=%zRy&&rIF3t548L#QxNC-Cy9x!~MUx@9W{ejX-M~!wfUbFvEWU z3&z{S5cvJLGZ6wWmDbB4u%^ou4S^kbo$3imrFzEYom%ottpy#fIkXVk^(}58=rA4h zr4~+_0ef6t+8S94*kg}#k6%*(fN!QOlJ_S7?n?(9r@mNiGX{<)RSyJCr^X@>oMlJA zgUJ<*QhRgxF|d$jxCDWC?N3l z4$6eU`6ZOrjezG*+)-xoU>+2>tVs)jpQsvl_z`esOF$$9+#cf}gTRz?CJqBDXbl28 zLI(%}k4H>nVBZmv82mJ37z2CVg48YvQ&p)E0b2^`Fd^Vk#l%$z{7DZ=wRZOvl??$W z$w|0{fa`V{A1or^&$4`F3j~g#H`x&Q9NE+e_@!*U?$ibV9_n@5d%ZluFw8K+4F48J zE#6~>8D{vOh69Blm_!i#>M{rpgo9v;0KuDkL2#R@9|Kc0DqJ5yussBpYN^3O7zi%g zj)A8Q`!MiI(}ndM696XHMQ1%h;K=Yr2t2Orj`Aa5o$^OfdO%8#JrM<0MkRGET1)g=2;An&0M#+xrV#{Q zi}slz@Z(&41`!v@xJbrDGA@#Fk&KIETqNTn85ha8NX9V33^U9y!wfUbFvAQp%<%sV Z{{rJ)W=rgkmcRf2002ovPDHLkV1g23iL3ws literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-repeat-with-footer.png b/tests/ref/grid-subheaders-repeat-with-footer.png new file mode 100644 index 0000000000000000000000000000000000000000..39f8465e72743d1cd02666360d20a045f5c8518f GIT binary patch literal 584 zcmV-O0=NB%P)d4jWsoXrOhbLkk*-hhdAIjlSsu+Eecc~su6K9{~pDBl@ z{{$_iPqA+0a7|(8(Eog>{))XCXghgx23#1pNJ=&!uqI<&AlSN#fcGl<$ISN4^^SqV zR-+gK4>;u-0@oR!d%T|HTZF*k9}k*rJNZWa5P0AEUJZep7C@tJ?~n)te#mNc^T%8X z9PPH-Ah0nk9tf*Oofdn1(Cvht# z+fEL<0fFar8G*pHv!GEAmB|?dj%pd@5LgQ27y^fqLjr-<{x4(TmlQD>y!H1E10QS! zc(zEGFk!-9!KlT1OqeiX!VJk6lKrZX%#scY$vm*~MZ0;!z$sH{x1a!JuondDE(E-p z>M46gz)$zSsL9?=v-U*@yxu9@5V&C;H0oZ9+(F<-P2(yEaktaqI1^y3Z>(pqI-8r>T6bvt3=Lc=VQDc;LQr%Oa-}0WFU%I5@N}YMLUJ z#iE+A@yS=N7@3+Do9AqGPi&tH@cmI{iV{53!YQowhx>;9l2-yho?V;Ua6neqo~d!V zzEmK`8kh6W-wSM*%P{xVYnRX5%=~iv+YRD%q%8&3$SOQ|H9_{j%Yl4Zb&i|&SFV_@ zxMAu<9hNwaMz^`aOL!Z18^20D;kWgR!G`%J#}q$QInGo3Fo*G0a`pjD=Y+!Vdnd%I zr(bV9@HHi0ib;Q7qubt1TCp5|go1tT&irTL+f>EHBpsdP{Gf$XYOCbuY=H>BOWzfD z``x>$%~Cg~MymP1%is$urke=dd2#)l!}49KK)K4h4F^*a6?i47>JYD@<);T3K0RTMC B$)*4R literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-repeating-orphan-prevention.png b/tests/ref/grid-subheaders-repeating-orphan-prevention.png new file mode 100644 index 0000000000000000000000000000000000000000..0f37c5d1714e88a400f8e326333124a79d7daec6 GIT binary patch literal 347 zcmeAS@N?(olHy`uVBq!ia0vp^6+pa@14uAv+Nr!`U|>}8ba4!+xb^nhelMp&iDQMw z7f5I5axBW`SY)_j<-(TB-1T>3!D zZqky4@4VGEG^I-(DE=DvRI2a0NOkw@74yn?eoyz`Xa7-d&jrIfw`DzoO0;J`|7Eqg zF?b>G9bZ5HWab@Gb#*e&WDifBd*^&-h@G5uoAueS&AMBqFK)Z?Na^!E^G5Hhw-$5S xc-O_xIFNty;!e((il2MAB4!@I3Z!}U8CPDIc=*0T)i+S6dAjW|_HDUg$ zrM6O4E!P$-GE!QxSL$}q9G+QHSshhPF-PjVyPMDP{bE}5>9e9t-=Xrj2hGL$-^08# zL=7Z(*qRR}pi_HfZCnq2fzhV27KD_l`@#_2KuNq9#f8F)e-*VM_Eyo^-IF|>JTk8cH z=l?%d%Tkxo_Tb&a=NBWH^gf>~d1>8<=?0*o-?{EFxJu3spK@qX8p!9Ku6{1-oD!M< D=nr>z literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-too-large-non-repeating-orphan-before-auto.png b/tests/ref/grid-subheaders-too-large-non-repeating-orphan-before-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..c7d632adeabf46ee397803c8d0de13f6f6204999 GIT binary patch literal 460 zcmeAS@N?(olHy`uVBq!ia0vp^6+pa@14uAv+Nr!`U|^i=>EaktaqI0Ze{GgR3HA@Q z+NUI+uDWyOT~XrG4Nnx7-z~a0#Ur>ob<_5XTu;yEXYeOi&ilk-t4 z9eZci?&18o?eXtDjsE-V?>{fEU}9Ruka(zt^FP;|;|K25FlQajZTZi!&a^2(dEeLb z_crDxJ_yp<%hC9D`P4j)IsSWE6hCa)cCDnhA@=m%4>$Tbh1ccZs6KGu`K=zt#&~Tv zeHJ~xdu@svc1_9M>waLq^|OQQOm#kUSXmkGPg#2OAd5$#lfs9ma~vL&yMD`M*(2Fi zw!vCZ&1Z%I$PL0U#&2df(eofN8I{Khl8JniIy-i}UaiSdvq|?X-?O4nXQoEkr;^O8 zr|#BgnUg)A>zcraj4d^-lE)fEs`+W}mYualr zu;z$k0pm55U(?GC3y!?BW=q{Vk>%iHR+bu*70YMFwA8ZIH$T`Y!C{u@n`Dul_~2aD ubkj^xg-EdLqYP8NN;aKudx6iuG4elF{r5}E)(Vai(o literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-too-large-non-repeating-orphan-before-relative.png b/tests/ref/grid-subheaders-too-large-non-repeating-orphan-before-relative.png new file mode 100644 index 0000000000000000000000000000000000000000..324787b2506342de1630d9dc2c013971d0d701d8 GIT binary patch literal 542 zcmV+(0^$9MP)qoX7wB>epR($dmwY;5=U_t)3gg@uKNhK7oYib+XH?Ck7-fPl!z z$cc%G+1c6c?d{gq*5u^mRaI3>OG_>;E-)}Ke0+R_goL1=po4>h*x1(SBC z>FMd{=;-$L_Pe{g#>U3N!ov0S_2uQ|Gcz-soSc`JmzbECQc_ZElL;+mS8etv#O zM@O`@w9U=UwY9aOp`o9jpGHPT!NI}m>gunrukrEmc6N5|?(SP#TS`hwFE1~Rjg8LE z&fVSJ=H}+Iva%r|A&iWS^z`(-y}kYY{r^EW{n;FJ0002#Nkldd5Jb_o zg)E#%jyU0*bI!)bWFhW<8+IIlF2R2n_*Ju+O$z|httX45+mXpM;StHq$RnI>YqG>y zSv2a`H*$y%uWsegtL&HMu#nr?Xv!eUM)L`u5=ghCC5zSEVkn1|Lbxu6(&K$u4#kJN z%L^GCpPZhZ%iySTke9>Wt~LW%l(uBCyd;aCsy<^GeAoUy_!|HK00000<6+8#%L8CY z#*mCKLo!K<-rnnSczM--AdBCcEQ0EnEQ*D-IT<`ZZRayG=noMlOqeiX!h{JECQO(x gVZs0a0058{1Ky);tIJX_@c;k-07*qoM6N<$f*kc6MgRZ+ literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-too-large-repeating-orphan-before-auto.png b/tests/ref/grid-subheaders-too-large-repeating-orphan-before-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..de77beb29ac5d995e907af95d71ace020ada7054 GIT binary patch literal 525 zcmeAS@N?(olHy`uVBq!ia0vp^6+rxngBeI}ocJ*gNSz7r332@o1de83zI@rcckiW3 zm;U_u^Xu2IyLazizkXd$Pw(8hb0(}@8_D-KZ{pr)EU%!4eHa0$V=+Loa$CfW&K5N#jhK7c3-@fhMz1!2% z^Ww#ej~+d$tE+qY^5vX4b6&i7@$lip8#ivee*L<+xj85(XvT~gZ{ECl`}Xa%Yu6MN z6?=MmT3cJYy1Lrh+BR+4l#`R=?d=^J8v5+nGZht;fB*h1UcA`G#^&$ezd#RZNt}oP zQhz*M978H@y}jigD?y+a_?0%5P=PL^%sl{U+{u_5s=gkf+&`oGfWgk>{sC*=b*GkCiC KxvX zN_KX3@$vDmudh;4Qka;S>+9>{;^KaOewUY*oSdBL>FJG)jpgO#Gcz;w_4UHS!p6qN zr>Cd8yStj2nt*_S_V)JZ=;-0$;n>*NgM)+7(a}jsNiZ-le0+S_+1ZJSiR|p`?d|Q> z*4B!OiiL%R$jHdf&d%4@*WKOS=H}-5`uhC*{L<3Wy}iBk^z{EhHWgZ%)&Kwir%6OX zRCwC$)x{CQ002bM4Z+>r-Ccr3=)Wj#RKT#4VP6CP_#q)-s3I?lQOM+oC z9(Fq*n2rYh9tdX3)fx=*1vXDmSX)r+_Mj*hknuoJ%IC6Rczr0Wu)+!}tgylgE3B}> f3M;Izh + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FirstHeader
SecondHeader
Level 2Header
Level 3Header
BodyCells
YetMore
Level 2Header Inside
Level 3
EvenMore
BodyCells
One Last HeaderFor Good Measure
FooterRow
EndingTable
+ + diff --git a/tests/ref/html/multi-header-table.html b/tests/ref/html/multi-header-table.html new file mode 100644 index 000000000..8a34ac170 --- /dev/null +++ b/tests/ref/html/multi-header-table.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FirstHeader
SecondHeader
Level 2Header
Level 3Header
BodyCells
YetMore
FooterRow
EndingTable
+ + diff --git a/tests/ref/issue-5359-column-override-stays-inside-header.png b/tests/ref/issue-5359-column-override-stays-inside-header.png new file mode 100644 index 0000000000000000000000000000000000000000..8339a4090d6cc71b8eff6890a9f9d37453dd5fdc GIT binary patch literal 674 zcmV;T0$u%yP)JGgviH_kPLjn0MN)2TI4$-6e70 z>7#r6!I#YdY!&|F=x#<{5{GBKPh8?RU|WOV=CnXL)@Xvk zVZQp^pX3dWNIqccSSXSMeKih+W7cE=8*ZY*>Ye(}qEW)Owbf@^^B(}XZ~#~9&^1wfE1?2<*{-}#yy{#8}*&a7nw zgo;~69{9MzQimqm?C86sH28+;fU>o24v4 zETLyi;WXeMRr2sa6^6nTrZA8D4VwpGPtrTzGXMYp07*qo IM6N<$g0lNP@&Et; literal 0 HcmV?d00001 diff --git a/tests/suite/layout/grid/footers.typ b/tests/suite/layout/grid/footers.typ index f7f1deb0a..c0b03f50a 100644 --- a/tests/suite/layout/grid/footers.typ +++ b/tests/suite/layout/grid/footers.typ @@ -389,6 +389,29 @@ table.footer[a][b][c] ) +--- grid-footer-repeatable-unbreakable --- +#set page(height: 8em, width: auto) +#table( + [h], + table.footer( + [a], + [b], + [c], + ) +) + +--- grid-footer-non-repeatable-unbreakable --- +#set page(height: 8em, width: auto) +#table( + [h], + table.footer( + [a], + [b], + [c], + repeat: false, + ) +) + --- grid-footer-stroke-edge-cases --- // Test footer stroke priority edge case #set page(height: 10em) diff --git a/tests/suite/layout/grid/headers.typ b/tests/suite/layout/grid/headers.typ index 229bce614..ea222ee88 100644 --- a/tests/suite/layout/grid/headers.typ +++ b/tests/suite/layout/grid/headers.typ @@ -118,30 +118,81 @@ ) --- grid-header-not-at-first-row --- -// Error: 3:3-3:19 header must start at the first row -// Hint: 3:3-3:19 remove any rows before the header #grid( [a], grid.header([b]) ) --- grid-header-not-at-first-row-two-columns --- -// Error: 4:3-4:19 header must start at the first row -// Hint: 4:3-4:19 remove any rows before the header #grid( columns: 2, [a], grid.header([b]) ) ---- grow-header-multiple --- -// Error: 3:3-3:19 cannot have more than one header +--- grid-header-multiple --- #grid( grid.header([a]), grid.header([b]), [a], ) +--- grid-header-skip --- +#grid( + columns: 2, + [x], [y], + grid.header([a]), + grid.header([b]), + grid.cell(x: 1)[c], [d], + grid.header([e]), + [f], grid.cell(x: 1)[g] +) + +--- grid-header-too-large-non-repeating-orphan --- +#set page(height: 8em) +#grid( + grid.header( + [a\ ] * 5, + repeat: false, + ), + [b] +) + +--- grid-header-too-large-repeating-orphan --- +#set page(height: 8em) +#grid( + grid.header( + [a\ ] * 5, + repeat: true, + ), + [b] +) + +--- grid-header-too-large-repeating-orphan-with-footer --- +#set page(height: 8em) +#grid( + grid.header( + [a\ ] * 5, + repeat: true, + ), + [b], + grid.footer( + [c], + repeat: true, + ) +) + +--- grid-header-too-large-repeating-orphan-not-at-first-row --- +#set page(height: 8em) +#grid( + [b], + grid.header( + [a\ ] * 5, + repeat: true, + ), + [c], +) + --- table-header-in-grid --- // Error: 2:3-2:20 cannot use `table.header` as a grid header // Hint: 2:3-2:20 use `grid.header` instead @@ -228,6 +279,51 @@ table.cell(rowspan: 3, lines(15)) ) +--- grid-header-and-rowspan-contiguous-1 --- +// Block should occupy all space +#set page(height: 15em) + +#table( + rows: (auto, 2.5em, 2em, auto), + gutter: 3pt, + inset: 0pt, + table.header( + [*H*], + [*W*] + ), + table.cell(rowspan: 3, block(height: 2.5em + 2em + 20em, width: 100%, fill: red)) +) + +--- grid-header-and-rowspan-contiguous-2 --- +// Block should occupy all space +#set page(height: 15em) + +#table( + rows: (auto, 2.5em, 10em, 5em, auto), + gutter: 3pt, + inset: 0pt, + table.header( + [*H*], + [*W*] + ), + table.cell(rowspan: 3, block(height: 2.5em + 2em + 20em, width: 100%, fill: red)) +) + +--- grid-header-and-large-auto-contiguous --- +// Block should occupy all space +#set page(height: 15em) + +#table( + rows: (auto, 4.5em, auto), + gutter: 3pt, + inset: 0pt, + table.header( + [*H*], + [*W*] + ), + block(height: 2.5em + 2em + 20em, width: 100%, fill: red) +) + --- grid-header-lack-of-space --- // Test lack of space for header + text. #set page(height: 8em) @@ -255,6 +351,17 @@ ..([Test], [Test], [Test]) * 20 ) +--- grid-header-non-repeating-orphan-prevention --- +#set page(height: 5em) +#v(2em) +#grid( + grid.header(repeat: false)[*Abc*], + [a], + [b], + [c], + [d] +) + --- grid-header-empty --- // Empty header should just be a repeated blank row #set page(height: 12em) @@ -339,6 +446,56 @@ [a\ b] ) +--- grid-header-not-at-the-top --- +#set page(height: 5em) +#v(2em) +#grid( + [a], + [b], + grid.header[*Abc*], + [d], + [e], + [f], +) + +--- grid-header-replace --- +#set page(height: 5em) +#v(1.5em) +#grid( + grid.header[*Abc*], + [a], + [b], + grid.header[*Def*], + [d], + [e], + [f], +) + +--- grid-header-replace-orphan --- +#set page(height: 5em) +#grid( + grid.header[*Abc*], + [a], + [b], + grid.header[*Def*], + [d], + [e], + [f], +) + +--- grid-header-replace-doesnt-fit --- +#set page(height: 5em) +#v(0.8em) +#grid( + grid.header[*Abc*], + [a], + [b], + grid.header[*Def*], + [d], + [e], + [f], +) + --- grid-header-stroke-edge-cases --- // Test header stroke priority edge case (last header row removed) #set page(height: 8em) @@ -463,8 +620,6 @@ #table( columns: 3, [Outside], - // Error: 1:3-4:4 header must start at the first row - // Hint: 1:3-4:4 remove any rows before the header table.header( [A], table.cell(x: 1)[B], [C], table.cell(x: 1)[D], diff --git a/tests/suite/layout/grid/html.typ b/tests/suite/layout/grid/html.typ index 10345cb06..cf98d4bc5 100644 --- a/tests/suite/layout/grid/html.typ +++ b/tests/suite/layout/grid/html.typ @@ -57,3 +57,78 @@ [d], [e], [f], [g], [h], [i] ) + +--- multi-header-table html --- +#table( + columns: 2, + + table.header( + [First], [Header] + ), + table.header( + [Second], [Header] + ), + table.header( + [Level 2], [Header], + level: 2, + ), + table.header( + [Level 3], [Header], + level: 3, + ), + + [Body], [Cells], + [Yet], [More], + + table.footer( + [Footer], [Row], + [Ending], [Table], + ), +) + +--- multi-header-inside-table html --- +#table( + columns: 2, + + table.header( + [First], [Header] + ), + table.header( + [Second], [Header] + ), + table.header( + [Level 2], [Header], + level: 2, + ), + table.header( + [Level 3], [Header], + level: 3, + ), + + [Body], [Cells], + [Yet], [More], + + table.header( + [Level 2], [Header Inside], + level: 2, + ), + table.header( + [Level 3], + level: 3, + ), + + [Even], [More], + [Body], [Cells], + + table.header( + [One Last Header], + [For Good Measure], + repeat: false, + level: 4, + ), + + table.footer( + [Footer], [Row], + [Ending], [Table], + ), +) diff --git a/tests/suite/layout/grid/subheaders.typ b/tests/suite/layout/grid/subheaders.typ new file mode 100644 index 000000000..56bed6a57 --- /dev/null +++ b/tests/suite/layout/grid/subheaders.typ @@ -0,0 +1,602 @@ +--- grid-subheaders-demo --- +#set page(height: 15.2em) +#table( + columns: 2, + align: center, + table.header( + table.cell(colspan: 2)[*Regional User Data*], + ), + table.header( + level: 2, + table.cell(colspan: 2)[*Germany*], + [*Username*], [*Joined*] + ), + [john123], [2024], + [rob8], [2025], + [joe1], [2025], + [joe2], [2025], + [martha], [2025], + [pear], [2025], + table.header( + level: 2, + table.cell(colspan: 2)[*United States*], + [*Username*], [*Joined*] + ), + [cool4], [2023], + [roger], [2023], + [bigfan55], [2022] +) + +--- grid-subheaders-colorful --- +#set page(width: auto, height: 12em) +#let rows(n) = { + range(n).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten() +} +#table( + columns: 5, + align: center + horizon, + table.header( + table.cell(colspan: 5)[*Cool Zone*], + ), + table.header( + level: 2, + table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*], + table.hline(start: 2, end: 3, stroke: yellow) + ), + ..rows(2), + table.header( + level: 2, + table.cell(stroke: red)[*New Name*], table.cell(stroke: aqua, colspan: 4)[*Other Data*], + table.hline(start: 2, end: 3, stroke: yellow) + ), + ..rows(3) +) + +--- grid-subheaders-basic --- +#grid( + grid.header([a]), + grid.header(level: 2, [b]), + [c] +) + +--- grid-subheaders-basic-non-consecutive --- +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + [y], +) + +--- grid-subheaders-basic-replace --- +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + [y], + grid.header(level: 2, [c]), + [z], +) + +--- grid-subheaders-basic-with-footer --- +#grid( + grid.header([a]), + grid.header(level: 2, [b]), + [c], + grid.footer([d]) +) + +--- grid-subheaders-basic-non-consecutive-with-footer --- +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + [y], + grid.footer([f]) +) + +--- grid-subheaders-repeat --- +#set page(height: 8em) +#grid( + grid.header([a]), + grid.header(level: 2, [b]), + ..([c],) * 10, +) + +--- grid-subheaders-repeat-non-consecutive --- +#set page(height: 8em) +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + ..([y],) * 10, +) + +--- grid-subheaders-repeat-with-footer --- +#set page(height: 8em) +#grid( + grid.header([a]), + [m], + grid.header(level: 2, [b]), + ..([c],) * 10, + grid.footer([f]) +) + +--- grid-subheaders-repeat-gutter --- +// Gutter below the header is also repeated +#set page(height: 8em) +#grid( + inset: (bottom: 0.5pt), + stroke: (bottom: 1pt), + gutter: (1pt, 6pt, 1pt), + grid.header([a]), + grid.header(level: 2, [b]), + ..([c],) * 10, +) + +--- grid-subheaders-repeat-replace --- +#set page(height: 8em) +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + ..([y],) * 10, + grid.header(level: 2, [c]), + ..([z],) * 10, +) + +--- grid-subheaders-repeat-replace-multiple-levels --- +#set page(height: 8em) +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + grid.header(level: 3, [c]), + ..([y],) * 10, + grid.header(level: 2, [d]), + ..([z],) * 6, +) + +--- grid-subheaders-repeat-replace-gutter --- +#set page(height: 8em) +#grid( + gutter: 3pt, + grid.header([a]), + [x], + grid.header(level: 2, [b]), + ..([y],) * 8, + grid.header(level: 2, [c]), + ..([z],) * 4, +) + +--- grid-subheaders-repeat-replace-orphan --- +#set page(height: 8em) +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + ..([y],) * 12, + grid.header(level: 2, [c]), + ..([z],) * 10, +) + +--- grid-subheaders-repeat-replace-double-orphan --- +#set page(height: 8em) +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + ..([y],) * 11, + grid.header(level: 2, [c]), + grid.header(level: 3, [d]), + ..([z],) * 10, +) + +--- grid-subheaders-repeat-replace-gutter-orphan-at-child --- +#set page(height: 8em) +#grid( + gutter: 3pt, + grid.header([a]), + [x], + grid.header(level: 2, [b]), + ..([y],) * 9, + grid.header(level: 2, [c]), + [z \ z], + ..([z],) * 3, +) + +--- grid-subheaders-repeat-replace-gutter-orphan-at-gutter --- +#set page(height: 8em) +#grid( + gutter: 3pt, + grid.header([a]), + [x], + grid.header(level: 2, [b]), + ..([y],) * 9, + box(height: 3pt), + grid.header(level: 2, [c]), + ..([z],) * 4, +) + +--- grid-subheaders-repeat-replace-didnt-fit-once --- +#set page(height: 8em) +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + ..([y],) * 10, + grid.header(level: 2, [c\ c\ c]), + ..([z],) * 4, +) + +--- grid-subheaders-repeat-replace-with-footer --- +#set page(height: 8em) +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + grid.header(level: 3, [c]), + ..([y],) * 10, + grid.header(level: 2, [d]), + ..([z],) * 6, + grid.footer([f]) +) + +--- grid-subheaders-repeat-replace-with-footer-orphan --- +#set page(height: 8em) +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + ..([y],) * 10, + grid.header(level: 2, [c]), + ..([z],) * 10, + grid.footer([f]) +) + +--- grid-subheaders-repeat-replace-short-lived --- +// No orphan prevention for short-lived headers +// (followed by replacing headers). +#set page(height: 8em) +#grid( + grid.header([a]), + grid.header(level: 2, [b]), + grid.header(level: 2, [c]), + grid.header(level: 2, [d]), + grid.header(level: 2, [e]), + grid.header(level: 2, [f]), + grid.header(level: 2, [g]), + grid.header(level: 2, [h]), + grid.header(level: 2, [i]), + grid.header(level: 2, [j]), + grid.header(level: 3, [k]), + ..([z],) * 10, +) + +--- grid-subheaders-repeat-short-lived-also-replaces --- +// Short-lived subheaders must still replace their conflicting predecessors. +#set page(height: 8em) +#grid( + // This has to go + grid.header(level: 3, [a]), + [w], + grid.header(level: 2, [b]), + grid.header(level: 2, [c]), + grid.header(level: 2, [d]), + grid.header(level: 2, [e]), + grid.header(level: 2, [f]), + grid.header(level: 2, [g]), + grid.header(level: 2, [h]), + grid.header(level: 2, [i]), + grid.header(level: 2, [j]), + grid.header(level: 3, [k]), + ..([z],) * 10, +) + +--- grid-subheaders-multi-page-row --- +#set page(height: 8em) +#grid( + columns: 2, + grid.header([a]), + [x], + grid.header(level: 2, [b]), + [y], + grid.header(level: 3, [c]), + [a], [b], + grid.cell( + block(fill: red, width: 1.5em, height: 6.4em) + ), + [y], + ..([z],) * 10, +) + +--- grid-subheaders-non-repeat --- +#set page(height: 8em) +#grid( + grid.header(repeat: false, [a]), + [x], + grid.header(level: 2, repeat: false, [b]), + ..([y],) * 10, +) + +--- grid-subheaders-non-repeat-replace --- +#set page(height: 8em) +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + grid.header(level: 3, [c]), + ..([y],) * 9, + grid.header(level: 2, repeat: false, [d]), + ..([z],) * 6, +) + +--- grid-subheaders-non-repeating-replace-orphan --- +#set page(height: 8em) +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + ..([y],) * 12, + grid.header(level: 2, repeat: false, [c]), + ..([z],) * 10, +) + +--- grid-subheaders-non-repeating-replace-didnt-fit-once --- +#set page(height: 8em) +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + ..([y],) * 10, + grid.header(level: 2, repeat: false, [c\ c\ c]), + ..([z],) * 4, +) + +--- grid-subheaders-multi-page-rowspan --- +#set page(height: 8em) +#grid( + columns: 2, + grid.header([a]), + [x], + grid.header(level: 2, [b]), + [y], + grid.header(level: 3, [c]), + [z], [z], + grid.cell( + rowspan: 5, + block(fill: red, width: 1.5em, height: 6.4em) + ), + [cell], + [cell] +) + +--- grid-subheaders-multi-page-row-right-after --- +#set page(height: 8em) +#grid( + columns: 1, + grid.header([a]), + [x], + grid.header(level: 2, [b]), + grid.header(level: 3, [c]), + grid.cell( + block(fill: red, width: 1.5em, height: 6.4em) + ), + [done.], + [done.] +) + +--- grid-subheaders-multi-page-rowspan-right-after --- +#set page(height: 8em) +#grid( + columns: 2, + grid.header([a]), + [x], [y], + grid.header(level: 2, [b]), + grid.header(level: 3, [c]), + grid.cell( + rowspan: 5, + block(fill: red, width: 1.5em, height: 6.4em) + ), + [cell], + [cell], + grid.cell(x: 0)[done.], + grid.cell(x: 0)[done.] +) + +--- grid-subheaders-multi-page-row-with-footer --- +#set page(height: 8em) +#grid( + columns: 2, + grid.header([a]), + [x], + grid.header(level: 2, [b]), + [y], + grid.header(level: 3, [c]), + [a], [b], + grid.cell( + block(fill: red, width: 1.5em, height: 6.4em) + ), + [y], + ..([z],) * 10, + grid.footer([f]) +) + +--- grid-subheaders-multi-page-rowspan-with-footer --- +#set page(height: 8em) +#grid( + columns: 2, + grid.header([a]), + [x], + grid.header(level: 2, [b]), + [y], + grid.header(level: 3, [c]), + [z], [z], + grid.cell( + rowspan: 5, + block(fill: red, width: 1.5em, height: 6.4em) + ), + [cell], + [cell], + grid.footer([f]) +) + +--- grid-subheaders-multi-page-row-right-after-with-footer --- +#set page(height: 8em) +#grid( + columns: 1, + grid.header([a]), + [x], + grid.header(level: 2, [b]), + grid.header(level: 3, [c]), + grid.cell( + block(fill: red, width: 1.5em, height: 6.4em) + ), + [done.], + [done.], + grid.footer([f]) +) + +--- grid-subheaders-multi-page-rowspan-gutter --- +#set page(height: 9em) +#grid( + columns: 2, + column-gutter: 4pt, + row-gutter: (0pt, 4pt, 8pt, 4pt), + inset: (bottom: 0.5pt), + stroke: (bottom: 1pt), + grid.header([a]), + [x], + grid.header(level: 2, [b]), + [y], + grid.header(level: 3, [c]), + [z], [z], + grid.cell( + rowspan: 5, + block(fill: red, width: 1.5em, height: 6.4em) + ), + [cell], + [cell], + [a\ b], + grid.cell(x: 0)[end], +) + +--- grid-subheaders-non-repeating-header-before-multi-page-row --- +#set page(height: 6em) +#grid( + grid.header(repeat: false, [h]), + [row #colbreak() row] +) + + +--- grid-subheaders-short-lived-no-orphan-prevention --- +// No orphan prevention for short-lived headers. +#set page(height: 8em) +#v(5em) +#grid( + grid.header(level: 2, [b]), + grid.header(level: 2, [c]), + [d] +) + +--- grid-subheaders-repeating-orphan-prevention --- +#set page(height: 8em) +#v(4.5em) +#grid( + grid.header(repeat: true, level: 2, [L2]), + grid.header(repeat: true, level: 4, [L4]), + [a] +) + +--- grid-subheaders-non-repeating-orphan-prevention --- +#set page(height: 8em) +#v(4.5em) +#grid( + grid.header(repeat: false, level: 2, [L2]), + grid.header(repeat: false, level: 4, [L4]), + [a] +) + +--- grid-subheaders-alone --- +#table( + table.header([a]), + table.header(level: 2, [b]), +) + +--- grid-subheaders-alone-no-orphan-prevention --- +#set page(height: 5.3em) +#v(2em) +#grid( + grid.header([L1]), + grid.header(level: 2, [L2]), +) + +--- grid-subheaders-alone-with-gutter-no-orphan-prevention --- +#set page(height: 5.3em) +#v(2em) +#grid( + gutter: 3pt, + grid.header([L1]), + grid.header(level: 2, [L2]), +) + +--- grid-subheaders-alone-with-footer --- +#table( + table.header([a]), + table.header(level: 2, [b]), + table.footer([c]) +) + +--- grid-subheaders-alone-with-footer-no-orphan-prevention --- +#set page(height: 5.3em) +#table( + table.header([L1]), + table.header(level: 2, [L2]), + table.footer([a]) +) + +--- grid-subheaders-alone-with-gutter-and-footer-no-orphan-prevention --- +#set page(height: 5.5em) +#table( + gutter: 4pt, + table.header([L1]), + table.header(level: 2, [L2]), + table.footer([a]) +) + +--- grid-subheaders-too-large-non-repeating-orphan-before-auto --- +#set page(height: 8em) +#grid( + grid.header([1]), + grid.header([a\ ] * 2, level: 2, repeat: false), + grid.header([2], level: 3), + [b\ b\ b], +) + +--- grid-subheaders-too-large-repeating-orphan-before-auto --- +#set page(height: 8em) +#grid( + grid.header([1]), + grid.header([a\ ] * 2, level: 2, repeat: true), + grid.header([2], level: 3), + rect(width: 10pt, height: 3em, fill: red), +) + +--- grid-subheaders-too-large-repeating-orphan-before-relative --- +#set page(height: 8em) +#grid( + rows: (auto, auto, auto, 3em), + grid.header([1]), + grid.header([a\ ] * 2, level: 2, repeat: true), + grid.header([2], level: 3), + rect(width: 10pt, height: 3em, fill: red), +) + +--- grid-subheaders-too-large-non-repeating-orphan-before-relative --- +#set page(height: 8em) +#grid( + rows: (auto, auto, auto, 3em), + grid.header([1]), + grid.header([a\ ] * 2, level: 2, repeat: false), + grid.header([2], level: 3), + rect(width: 10pt, height: 3em, fill: red), +) From 44d410dd007569227e8eca41e39fde9a932f0d02 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 10 Jun 2025 14:44:38 +0000 Subject: [PATCH 151/172] Use the shaper in math (#6336) --- crates/typst-layout/src/inline/mod.rs | 1 + crates/typst-layout/src/inline/shaping.rs | 26 +- crates/typst-layout/src/math/accent.rs | 51 +- crates/typst-layout/src/math/attach.rs | 25 +- crates/typst-layout/src/math/frac.rs | 12 +- crates/typst-layout/src/math/fragment.rs | 796 +++++++++++++--------- crates/typst-layout/src/math/lr.rs | 33 +- crates/typst-layout/src/math/mat.rs | 32 +- crates/typst-layout/src/math/mod.rs | 56 +- crates/typst-layout/src/math/root.rs | 6 +- crates/typst-layout/src/math/shared.rs | 23 +- crates/typst-layout/src/math/stretch.rs | 300 +------- crates/typst-layout/src/math/text.rs | 73 +- crates/typst-layout/src/math/underover.rs | 8 +- crates/typst-library/src/text/font/mod.rs | 15 +- crates/typst-library/src/text/item.rs | 18 + crates/typst-library/src/text/mod.rs | 18 + crates/typst-pdf/src/text.rs | 10 +- crates/typst-render/src/text.rs | 9 +- crates/typst-svg/src/text.rs | 23 +- tests/ref/math-accent-dotless-greedy.png | Bin 0 -> 710 bytes tests/suite/math/accent.typ | 6 + 22 files changed, 731 insertions(+), 810 deletions(-) create mode 100644 tests/ref/math-accent-dotless-greedy.png diff --git a/crates/typst-layout/src/inline/mod.rs b/crates/typst-layout/src/inline/mod.rs index 5ef820d07..6cafb9b00 100644 --- a/crates/typst-layout/src/inline/mod.rs +++ b/crates/typst-layout/src/inline/mod.rs @@ -9,6 +9,7 @@ mod prepare; mod shaping; pub use self::box_::layout_box; +pub use self::shaping::create_shape_plan; use comemo::{Track, Tracked, TrackedMut}; use typst_library::diag::SourceResult; diff --git a/crates/typst-layout/src/inline/shaping.rs b/crates/typst-layout/src/inline/shaping.rs index ca723c0a5..935a86b38 100644 --- a/crates/typst-layout/src/inline/shaping.rs +++ b/crates/typst-layout/src/inline/shaping.rs @@ -1,18 +1,16 @@ use std::borrow::Cow; use std::fmt::{self, Debug, Formatter}; -use std::str::FromStr; use std::sync::Arc; 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, FontFamily, FontVariant, - Glyph, Lang, Region, TextEdgeBounds, TextElem, TextItem, + families, features, is_default_ignorable, language, variant, Font, FontFamily, + FontVariant, Glyph, Lang, Region, TextEdgeBounds, TextElem, TextItem, }; use typst_library::World; use typst_utils::SliceExt; @@ -295,6 +293,8 @@ impl<'a> ShapedText<'a> { + justification_left + justification_right, x_offset: shaped.x_offset + justification_left, + y_advance: Em::zero(), + y_offset: Em::zero(), range: (shaped.range.start - range.start).saturating_as() ..(shaped.range.end - range.start).saturating_as(), span, @@ -934,7 +934,7 @@ fn shape_segment<'a>( /// Create a shape plan. #[comemo::memoize] -fn create_shape_plan( +pub fn create_shape_plan( font: &Font, direction: rustybuzz::Direction, script: rustybuzz::Script, @@ -952,7 +952,7 @@ fn create_shape_plan( /// Shape the text with tofus from the given font. fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) { - let x_advance = font.advance(0).unwrap_or_default(); + let x_advance = font.x_advance(0).unwrap_or_default(); let add_glyph = |(cluster, c): (usize, char)| { let start = base + cluster; let end = start + c.len_utf8(); @@ -1044,20 +1044,8 @@ fn calculate_adjustability(ctx: &mut ShapingContext, lang: Lang, region: Option< /// Difference between non-breaking and normal space. fn nbsp_delta(font: &Font) -> Option { - let space = font.ttf().glyph_index(' ')?.0; let nbsp = font.ttf().glyph_index('\u{00A0}')?.0; - Some(font.advance(nbsp)? - font.advance(space)?) -} - -/// Process the language and region of a style chain into a -/// rustybuzz-compatible BCP 47 language. -fn language(styles: StyleChain) -> rustybuzz::Language { - let mut bcp: EcoString = TextElem::lang_in(styles).as_str().into(); - if let Some(region) = TextElem::region_in(styles) { - bcp.push('-'); - bcp.push_str(region.as_str()); - } - rustybuzz::Language::from_str(&bcp).unwrap() + Some(font.x_advance(nbsp)? - font.space_width()?) } /// Returns true if all glyphs in `glyphs` have ranges within the range `range`. diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs index 301606466..159703b8e 100644 --- a/crates/typst-layout/src/math/accent.rs +++ b/crates/typst-layout/src/math/accent.rs @@ -3,7 +3,10 @@ use typst_library::foundations::{Packed, StyleChain}; use typst_library::layout::{Em, Frame, Point, Size}; use typst_library::math::AccentElem; -use super::{style_cramped, FrameFragment, GlyphFragment, MathContext, MathFragment}; +use super::{ + style_cramped, style_dtls, style_flac, FrameFragment, GlyphFragment, MathContext, + MathFragment, +}; /// How much the accent can be shorter than the base. const ACCENT_SHORT_FALL: Em = Em::new(0.5); @@ -15,40 +18,40 @@ pub fn layout_accent( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let cramped = style_cramped(); - let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?; - let accent = elem.accent; let top_accent = !accent.is_bottom(); - // Try to replace base glyph with its dotless variant. - if top_accent && elem.dotless(styles) { - if let MathFragment::Glyph(glyph) = &mut base { - glyph.make_dotless_form(ctx); - } - } + // Try to replace the base glyph with its dotless variant. + let dtls = style_dtls(); + let base_styles = + if top_accent && elem.dotless(styles) { styles.chain(&dtls) } else { styles }; + + let cramped = style_cramped(); + let base = ctx.layout_into_fragment(&elem.base, base_styles.chain(&cramped))?; // Preserve class to preserve automatic spacing. let base_class = base.class(); let base_attach = base.accent_attach(); - let mut glyph = GlyphFragment::new(ctx, styles, accent.0, elem.span()); + // Try to replace the accent glyph with its flattened variant. + let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); + let flac = style_flac(); + let accent_styles = if top_accent && base.ascent() > flattened_base_height { + styles.chain(&flac) + } else { + styles + }; - // Try to replace accent glyph with its flattened variant. - if top_accent { - let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); - if base.ascent() > flattened_base_height { - glyph.make_flattened_accent_form(ctx); - } - } + let mut glyph = + GlyphFragment::new_char(ctx.font, accent_styles, accent.0, elem.span())?; - // Forcing the accent to be at least as large as the base makes it too - // wide in many case. + // Forcing the accent to be at least as large as the base makes it too wide + // in many cases. let width = elem.size(styles).relative_to(base.width()); - 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.0; + let short_fall = ACCENT_SHORT_FALL.at(glyph.item.size); + glyph.stretch_horizontal(ctx, width - short_fall); + let accent_attach = glyph.accent_attach.0; + let accent = glyph.into_frame(); let (gap, accent_pos, base_pos) = if top_accent { // Descent is negative because the accent's ink bottom is above the diff --git a/crates/typst-layout/src/math/attach.rs b/crates/typst-layout/src/math/attach.rs index 90aad941e..a7f3cad5f 100644 --- a/crates/typst-layout/src/math/attach.rs +++ b/crates/typst-layout/src/math/attach.rs @@ -66,7 +66,6 @@ pub fn layout_attach( let relative_to_width = measure!(t, width).max(measure!(b, width)); stretch_fragment( ctx, - styles, &mut base, Some(Axis::X), Some(relative_to_width), @@ -220,7 +219,6 @@ fn layout_attachments( // Calculate the distance each pre-script extends to the left of the base's // width. let (tl_pre_width, bl_pre_width) = compute_pre_script_widths( - ctx, &base, [tl.as_ref(), bl.as_ref()], (tx_shift, bx_shift), @@ -231,7 +229,6 @@ fn layout_attachments( // base's width. Also calculate each post-script's kerning (we need this for // its position later). let ((tr_post_width, tr_kern), (br_post_width, br_kern)) = compute_post_script_widths( - ctx, &base, [tr.as_ref(), br.as_ref()], (tx_shift, bx_shift), @@ -287,14 +284,13 @@ fn layout_attachments( /// post-script's kerning value. The first tuple is for the post-superscript, /// and the second is for the post-subscript. fn compute_post_script_widths( - ctx: &MathContext, base: &MathFragment, [tr, br]: [Option<&MathFragment>; 2], (tr_shift, br_shift): (Abs, Abs), space_after_post_script: Abs, ) -> ((Abs, Abs), (Abs, Abs)) { let tr_values = tr.map_or_default(|tr| { - let kern = math_kern(ctx, base, tr, tr_shift, Corner::TopRight); + let kern = math_kern(base, tr, tr_shift, Corner::TopRight); (space_after_post_script + tr.width() + kern, kern) }); @@ -302,7 +298,7 @@ fn compute_post_script_widths( // need to shift the post-subscript left by the base's italic correction // (see the kerning algorithm as described in the OpenType MATH spec). let br_values = br.map_or_default(|br| { - let kern = math_kern(ctx, base, br, br_shift, Corner::BottomRight) + let kern = math_kern(base, br, br_shift, Corner::BottomRight) - base.italics_correction(); (space_after_post_script + br.width() + kern, kern) }); @@ -317,19 +313,18 @@ fn compute_post_script_widths( /// extends left of the base's width and the second being the distance the /// pre-subscript extends left of the base's width. fn compute_pre_script_widths( - ctx: &MathContext, base: &MathFragment, [tl, bl]: [Option<&MathFragment>; 2], (tl_shift, bl_shift): (Abs, Abs), space_before_pre_script: Abs, ) -> (Abs, Abs) { let tl_pre_width = tl.map_or_default(|tl| { - let kern = math_kern(ctx, base, tl, tl_shift, Corner::TopLeft); + let kern = math_kern(base, tl, tl_shift, Corner::TopLeft); space_before_pre_script + tl.width() + kern }); let bl_pre_width = bl.map_or_default(|bl| { - let kern = math_kern(ctx, base, bl, bl_shift, Corner::BottomLeft); + let kern = math_kern(base, bl, bl_shift, Corner::BottomLeft); space_before_pre_script + bl.width() + kern }); @@ -471,13 +466,7 @@ fn compute_script_shifts( /// a negative value means shifting the script closer to the base. Requires the /// distance from the base's baseline to the script's baseline, as well as the /// script's corner (tl, tr, bl, br). -fn math_kern( - ctx: &MathContext, - base: &MathFragment, - script: &MathFragment, - shift: Abs, - pos: Corner, -) -> Abs { +fn math_kern(base: &MathFragment, script: &MathFragment, shift: Abs, pos: Corner) -> Abs { // This process is described under the MathKernInfo table in the OpenType // MATH spec. @@ -502,8 +491,8 @@ fn math_kern( // Calculate the sum of kerning values for each correction height. let summed_kern = |height| { - let base_kern = base.kern_at_height(ctx, pos, height); - let attach_kern = script.kern_at_height(ctx, pos.inv(), height); + let base_kern = base.kern_at_height(pos, height); + let attach_kern = script.kern_at_height(pos.inv(), height); base_kern + attach_kern }; diff --git a/crates/typst-layout/src/math/frac.rs b/crates/typst-layout/src/math/frac.rs index 2567349d0..091f328f6 100644 --- a/crates/typst-layout/src/math/frac.rs +++ b/crates/typst-layout/src/math/frac.rs @@ -109,14 +109,14 @@ fn layout_frac_like( frame.push_frame(denom_pos, denom); if binom { - let mut left = GlyphFragment::new(ctx, styles, '(', span) - .stretch_vertical(ctx, height - short_fall); - left.center_on_axis(ctx); + let mut left = GlyphFragment::new_char(ctx.font, styles, '(', span)?; + left.stretch_vertical(ctx, height - short_fall); + left.center_on_axis(); ctx.push(left); ctx.push(FrameFragment::new(styles, frame)); - let mut right = GlyphFragment::new(ctx, styles, ')', span) - .stretch_vertical(ctx, height - short_fall); - right.center_on_axis(ctx); + let mut right = GlyphFragment::new_char(ctx.font, styles, ')', span)?; + right.stretch_vertical(ctx, height - short_fall); + right.center_on_axis(); ctx.push(right); } else { frame.push( diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs index 01fa6be4b..eb85eeb5d 100644 --- a/crates/typst-layout/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -1,28 +1,32 @@ use std::fmt::{self, Debug, Formatter}; -use rustybuzz::Feature; -use ttf_parser::gsub::{AlternateSubstitution, SingleSubstitution, SubstitutionSubtable}; -use ttf_parser::opentype_layout::LayoutTable; -use ttf_parser::{GlyphId, Rect}; +use az::SaturatingAs; +use rustybuzz::{BufferFlags, UnicodeBuffer}; +use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart}; +use ttf_parser::GlyphId; +use typst_library::diag::{bail, warning, SourceResult}; use typst_library::foundations::StyleChain; use typst_library::introspection::Tag; use typst_library::layout::{ - Abs, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment, + Abs, Axes, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment, }; use typst_library::math::{EquationElem, MathSize}; -use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem}; -use typst_library::visualize::{FixedStroke, Paint}; +use typst_library::text::{features, language, Font, Glyph, TextElem, TextItem}; use typst_syntax::Span; -use typst_utils::default_math_class; +use typst_utils::{default_math_class, Get}; use unicode_math_class::MathClass; -use super::{stretch_glyph, MathContext, Scaled}; +use super::MathContext; +use crate::inline::create_shape_plan; use crate::modifiers::{FrameModifiers, FrameModify}; +/// Maximum number of times extenders can be repeated. +const MAX_REPEATS: usize = 1024; + +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone)] pub enum MathFragment { Glyph(GlyphFragment), - Variant(VariantFragment), Frame(FrameFragment), Spacing(Abs, bool), Space(Abs), @@ -33,13 +37,18 @@ pub enum MathFragment { impl MathFragment { pub fn size(&self) -> Size { - Size::new(self.width(), self.height()) + match self { + Self::Glyph(glyph) => glyph.size, + Self::Frame(fragment) => fragment.frame.size(), + Self::Spacing(amount, _) => Size::with_x(*amount), + Self::Space(amount) => Size::with_x(*amount), + _ => Size::zero(), + } } pub fn width(&self) -> Abs { match self { - Self::Glyph(glyph) => glyph.width, - Self::Variant(variant) => variant.frame.width(), + Self::Glyph(glyph) => glyph.size.x, Self::Frame(fragment) => fragment.frame.width(), Self::Spacing(amount, _) => *amount, Self::Space(amount) => *amount, @@ -49,8 +58,7 @@ impl MathFragment { pub fn height(&self) -> Abs { match self { - Self::Glyph(glyph) => glyph.height(), - Self::Variant(variant) => variant.frame.height(), + Self::Glyph(glyph) => glyph.size.y, Self::Frame(fragment) => fragment.frame.height(), _ => Abs::zero(), } @@ -58,17 +66,15 @@ impl MathFragment { pub fn ascent(&self) -> Abs { match self { - Self::Glyph(glyph) => glyph.ascent, - Self::Variant(variant) => variant.frame.ascent(), - Self::Frame(fragment) => fragment.frame.baseline(), + Self::Glyph(glyph) => glyph.ascent(), + Self::Frame(fragment) => fragment.frame.ascent(), _ => Abs::zero(), } } pub fn descent(&self) -> Abs { match self { - Self::Glyph(glyph) => glyph.descent, - Self::Variant(variant) => variant.frame.descent(), + Self::Glyph(glyph) => glyph.descent(), Self::Frame(fragment) => fragment.frame.descent(), _ => Abs::zero(), } @@ -85,7 +91,6 @@ impl MathFragment { pub fn class(&self) -> MathClass { match self { Self::Glyph(glyph) => glyph.class, - Self::Variant(variant) => variant.class, Self::Frame(fragment) => fragment.class, Self::Spacing(_, _) => MathClass::Space, Self::Space(_) => MathClass::Space, @@ -98,7 +103,6 @@ impl MathFragment { pub fn math_size(&self) -> Option { match self { Self::Glyph(glyph) => Some(glyph.math_size), - Self::Variant(variant) => Some(variant.math_size), Self::Frame(fragment) => Some(fragment.math_size), _ => None, } @@ -106,8 +110,7 @@ impl MathFragment { pub fn font_size(&self) -> Option { match self { - Self::Glyph(glyph) => Some(glyph.font_size), - Self::Variant(variant) => Some(variant.font_size), + Self::Glyph(glyph) => Some(glyph.item.size), Self::Frame(fragment) => Some(fragment.font_size), _ => None, } @@ -116,7 +119,6 @@ impl MathFragment { pub fn set_class(&mut self, class: MathClass) { match self { Self::Glyph(glyph) => glyph.class = class, - Self::Variant(variant) => variant.class = class, Self::Frame(fragment) => fragment.class = class, _ => {} } @@ -125,7 +127,6 @@ impl MathFragment { pub fn set_limits(&mut self, limits: Limits) { match self { Self::Glyph(glyph) => glyph.limits = limits, - Self::Variant(variant) => variant.limits = limits, Self::Frame(fragment) => fragment.limits = limits, _ => {} } @@ -149,7 +150,6 @@ impl MathFragment { pub fn is_text_like(&self) -> bool { match self { Self::Glyph(glyph) => !glyph.extended_shape, - Self::Variant(variant) => !variant.extended_shape, MathFragment::Frame(frame) => frame.text_like, _ => false, } @@ -158,7 +158,6 @@ impl MathFragment { pub fn italics_correction(&self) -> Abs { match self { Self::Glyph(glyph) => glyph.italics_correction, - Self::Variant(variant) => variant.italics_correction, Self::Frame(fragment) => fragment.italics_correction, _ => Abs::zero(), } @@ -167,7 +166,6 @@ impl MathFragment { pub fn accent_attach(&self) -> (Abs, Abs) { match self { Self::Glyph(glyph) => glyph.accent_attach, - Self::Variant(variant) => variant.accent_attach, Self::Frame(fragment) => fragment.accent_attach, _ => (self.width() / 2.0, self.width() / 2.0), } @@ -176,7 +174,6 @@ impl MathFragment { pub fn into_frame(self) -> Frame { match self { Self::Glyph(glyph) => glyph.into_frame(), - Self::Variant(variant) => variant.frame, Self::Frame(fragment) => fragment.frame, Self::Tag(tag) => { let mut frame = Frame::soft(Size::zero()); @@ -190,7 +187,6 @@ impl MathFragment { pub fn limits(&self) -> Limits { match self { MathFragment::Glyph(glyph) => glyph.limits, - MathFragment::Variant(variant) => variant.limits, MathFragment::Frame(fragment) => fragment.limits, _ => Limits::Never, } @@ -198,11 +194,31 @@ impl MathFragment { /// If no kern table is provided for a corner, a kerning amount of zero is /// assumed. - pub fn kern_at_height(&self, ctx: &MathContext, corner: Corner, height: Abs) -> Abs { + pub fn kern_at_height(&self, corner: Corner, height: Abs) -> Abs { match self { Self::Glyph(glyph) => { - kern_at_height(ctx, glyph.font_size, glyph.id, corner, height) - .unwrap_or_default() + // For glyph assemblies we pick either the start or end glyph + // depending on the corner. + let is_vertical = + glyph.item.glyphs.iter().all(|glyph| glyph.y_advance != Em::zero()); + let glyph_index = match (is_vertical, corner) { + (true, Corner::TopLeft | Corner::TopRight) => { + glyph.item.glyphs.len() - 1 + } + (false, Corner::TopRight | Corner::BottomRight) => { + glyph.item.glyphs.len() - 1 + } + _ => 0, + }; + + kern_at_height( + &glyph.item.font, + GlyphId(glyph.item.glyphs[glyph_index].id), + corner, + Em::from_length(height, glyph.item.size), + ) + .unwrap_or_default() + .at(glyph.item.size) } _ => Abs::zero(), } @@ -215,12 +231,6 @@ impl From for MathFragment { } } -impl From for MathFragment { - fn from(variant: VariantFragment) -> Self { - Self::Variant(variant) - } -} - impl From for MathFragment { fn from(fragment: FrameFragment) -> Self { Self::Frame(fragment) @@ -229,266 +239,282 @@ impl From for MathFragment { #[derive(Clone)] pub struct GlyphFragment { - pub id: GlyphId, - pub c: char, - pub font: Font, - pub lang: Lang, - pub region: Option, - pub fill: Paint, - pub stroke: Option, - pub shift: Abs, - pub width: Abs, - pub ascent: Abs, - pub descent: Abs, + // Text stuff. + pub item: TextItem, + pub base_glyph: Glyph, + // Math stuff. + pub size: Size, + pub baseline: Option, pub italics_correction: Abs, pub accent_attach: (Abs, Abs), - pub font_size: Abs, - pub class: MathClass, pub math_size: MathSize, - pub span: Span, - pub modifiers: FrameModifiers, + pub class: MathClass, pub limits: Limits, pub extended_shape: bool, + pub mid_stretched: Option, + // External frame stuff. + pub modifiers: FrameModifiers, + pub shift: Abs, + pub align: Abs, } impl GlyphFragment { - pub fn new(ctx: &MathContext, styles: StyleChain, c: char, span: Span) -> Self { - let id = ctx.ttf.glyph_index(c).unwrap_or_default(); - let id = Self::adjust_glyph_index(ctx, id); - Self::with_id(ctx, styles, c, id, span) - } - - pub fn try_new( - ctx: &MathContext, + /// Calls `new` with the given character. + pub fn new_char( + font: &Font, styles: StyleChain, c: char, span: Span, - ) -> Option { - let id = ctx.ttf.glyph_index(c)?; - let id = Self::adjust_glyph_index(ctx, id); - Some(Self::with_id(ctx, styles, c, id, span)) + ) -> SourceResult { + Self::new(font, styles, c.encode_utf8(&mut [0; 4]), span) } - pub fn with_id( - ctx: &MathContext, + /// Try to create a new glyph out of the given string. Will bail if the + /// result from shaping the string is not a single glyph or is a tofu. + #[comemo::memoize] + pub fn new( + font: &Font, styles: StyleChain, - c: char, - id: GlyphId, + text: &str, span: Span, - ) -> Self { + ) -> SourceResult { + let mut buffer = UnicodeBuffer::new(); + buffer.push_str(text); + buffer.set_language(language(styles)); + // TODO: Use `rustybuzz::script::MATH` once + // https://github.com/harfbuzz/rustybuzz/pull/165 is released. + buffer.set_script( + rustybuzz::Script::from_iso15924_tag(ttf_parser::Tag::from_bytes(b"math")) + .unwrap(), + ); + buffer.set_direction(rustybuzz::Direction::LeftToRight); + buffer.set_flags(BufferFlags::REMOVE_DEFAULT_IGNORABLES); + + let features = features(styles); + let plan = create_shape_plan( + font, + buffer.direction(), + buffer.script(), + buffer.language().as_ref(), + &features, + ); + + let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer); + if buffer.len() != 1 { + bail!(span, "did not get a single glyph after shaping {}", text); + } + + let info = buffer.glyph_infos()[0]; + let pos = buffer.glyph_positions()[0]; + + // TODO: add support for coverage and fallback, like in normal text shaping. + if info.glyph_id == 0 { + bail!(span, "current font is missing a glyph for {}", text); + } + + let cluster = info.cluster as usize; + let c = text[cluster..].chars().next().unwrap(); + let limits = Limits::for_char(c); let class = EquationElem::class_in(styles) .or_else(|| default_math_class(c)) .unwrap_or(MathClass::Normal); - let mut fragment = Self { - id, - c, - font: ctx.font.clone(), - lang: TextElem::lang_in(styles), - region: TextElem::region_in(styles), + let glyph = Glyph { + id: info.glyph_id as u16, + x_advance: font.to_em(pos.x_advance), + x_offset: font.to_em(pos.x_offset), + y_advance: font.to_em(pos.y_advance), + y_offset: font.to_em(pos.y_offset), + range: 0..text.len().saturating_as(), + span: (span, 0), + }; + + let item = TextItem { + font: font.clone(), + size: TextElem::size_in(styles), fill: TextElem::fill_in(styles).as_decoration(), stroke: TextElem::stroke_in(styles).map(|s| s.unwrap_or_default()), - shift: TextElem::baseline_in(styles), - font_size: TextElem::size_in(styles), + lang: TextElem::lang_in(styles), + region: TextElem::region_in(styles), + text: text.into(), + glyphs: vec![glyph.clone()], + }; + + let mut fragment = Self { + item, + base_glyph: glyph, + // Math math_size: EquationElem::size_in(styles), - width: Abs::zero(), - ascent: Abs::zero(), - descent: Abs::zero(), - limits: Limits::for_char(c), + class, + limits, + mid_stretched: None, + // Math in need of updating. + extended_shape: false, italics_correction: Abs::zero(), accent_attach: (Abs::zero(), Abs::zero()), - class, - span, + size: Size::zero(), + baseline: None, + // Misc + align: Abs::zero(), + shift: TextElem::baseline_in(styles), modifiers: FrameModifiers::get_in(styles), - extended_shape: false, }; - fragment.set_id(ctx, id); - fragment - } - - /// Apply GSUB substitutions. - fn adjust_glyph_index(ctx: &MathContext, id: GlyphId) -> GlyphId { - if let Some(glyphwise_tables) = &ctx.glyphwise_tables { - glyphwise_tables.iter().fold(id, |id, table| table.apply(id)) - } else { - id - } + fragment.update_glyph(); + Ok(fragment) } /// Sets element id and boxes in appropriate way without changing other /// styles. This is used to replace the glyph with a stretch variant. - pub fn set_id(&mut self, ctx: &MathContext, id: GlyphId) { - let advance = ctx.ttf.glyph_hor_advance(id).unwrap_or_default(); - let italics = italics_correction(ctx, id, self.font_size).unwrap_or_default(); - let bbox = ctx.ttf.glyph_bounding_box(id).unwrap_or(Rect { - x_min: 0, - y_min: 0, - x_max: 0, - y_max: 0, - }); + pub fn update_glyph(&mut self) { + let id = GlyphId(self.item.glyphs[0].id); - let mut width = advance.scaled(ctx, self.font_size); + let extended_shape = is_extended_shape(&self.item.font, id); + let italics = italics_correction(&self.item.font, id).unwrap_or_default(); + let width = self.item.width(); + if !extended_shape { + self.item.glyphs[0].x_advance += italics; + } + let italics = italics.at(self.item.size); + + let (ascent, descent) = + ascent_descent(&self.item.font, id).unwrap_or((Em::zero(), Em::zero())); // The fallback for accents is half the width plus or minus the italics // correction. This is similar to how top and bottom attachments are // shifted. For bottom accents we do not use the accent attach of the // base as it is meant for top acccents. - let top_accent_attach = - accent_attach(ctx, id, self.font_size).unwrap_or((width + italics) / 2.0); + let top_accent_attach = accent_attach(&self.item.font, id) + .map(|x| x.at(self.item.size)) + .unwrap_or((width + italics) / 2.0); let bottom_accent_attach = (width - italics) / 2.0; - let extended_shape = is_extended_shape(ctx, id); - if !extended_shape { - width += italics; - } - - self.id = id; - self.width = width; - self.ascent = bbox.y_max.scaled(ctx, self.font_size); - self.descent = -bbox.y_min.scaled(ctx, self.font_size); + self.baseline = Some(ascent.at(self.item.size)); + self.size = Size::new( + self.item.width(), + ascent.at(self.item.size) + descent.at(self.item.size), + ); self.italics_correction = italics; self.accent_attach = (top_accent_attach, bottom_accent_attach); self.extended_shape = extended_shape; } - pub fn height(&self) -> Abs { - self.ascent + self.descent + // Reset a GlyphFragment's text field and math properties back to its + // base_id's. This is used to return a glyph to its unstretched state. + pub fn reset_glyph(&mut self) { + self.align = Abs::zero(); + self.item.glyphs = vec![self.base_glyph.clone()]; + self.update_glyph(); } - pub fn into_variant(self) -> VariantFragment { - VariantFragment { - c: self.c, - font_size: self.font_size, - italics_correction: self.italics_correction, - accent_attach: self.accent_attach, - class: self.class, - math_size: self.math_size, - span: self.span, - limits: self.limits, - extended_shape: self.extended_shape, - frame: self.into_frame(), - mid_stretched: None, - } + pub fn baseline(&self) -> Abs { + self.ascent() + } + + /// The distance from the baseline to the top of the frame. + pub fn ascent(&self) -> Abs { + self.baseline.unwrap_or(self.size.y) + } + + /// The distance from the baseline to the bottom of the frame. + pub fn descent(&self) -> Abs { + self.size.y - self.ascent() } pub fn into_frame(self) -> Frame { - let item = TextItem { - font: self.font.clone(), - size: self.font_size, - fill: self.fill, - stroke: self.stroke, - lang: self.lang, - region: self.region, - text: self.c.into(), - glyphs: vec![Glyph { - id: self.id.0, - x_advance: Em::from_length(self.width, self.font_size), - x_offset: Em::zero(), - range: 0..self.c.len_utf8() as u16, - span: (self.span, 0), - }], - }; - let size = Size::new(self.width, self.ascent + self.descent); - let mut frame = Frame::soft(size); - frame.set_baseline(self.ascent); - frame.push(Point::with_y(self.ascent + self.shift), FrameItem::Text(item)); + let mut frame = Frame::soft(self.size); + frame.set_baseline(self.baseline()); + frame.push( + Point::with_y(self.ascent() + self.shift + self.align), + FrameItem::Text(self.item), + ); frame.modify(&self.modifiers); frame } - pub fn make_script_size(&mut self, ctx: &MathContext) { - let alt_id = - ctx.ssty_table.as_ref().and_then(|ssty| ssty.try_apply(self.id, None)); - if let Some(alt_id) = alt_id { - self.set_id(ctx, alt_id); - } - } - - pub fn make_script_script_size(&mut self, ctx: &MathContext) { - let alt_id = ctx.ssty_table.as_ref().and_then(|ssty| { - // We explicitly request to apply the alternate set with value 1, - // as opposed to the default value in ssty, as the former - // corresponds to second level scripts and the latter corresponds - // to first level scripts. - ssty.try_apply(self.id, Some(1)) - .or_else(|| ssty.try_apply(self.id, None)) - }); - if let Some(alt_id) = alt_id { - self.set_id(ctx, alt_id); - } - } - - pub fn make_dotless_form(&mut self, ctx: &MathContext) { - let alt_id = - ctx.dtls_table.as_ref().and_then(|dtls| dtls.try_apply(self.id, None)); - if let Some(alt_id) = alt_id { - self.set_id(ctx, alt_id); - } - } - - pub fn make_flattened_accent_form(&mut self, ctx: &MathContext) { - let alt_id = - ctx.flac_table.as_ref().and_then(|flac| flac.try_apply(self.id, None)); - if let Some(alt_id) = alt_id { - self.set_id(ctx, alt_id); - } - } - /// Try to stretch a glyph to a desired height. - pub fn stretch_vertical(self, ctx: &mut MathContext, height: Abs) -> VariantFragment { - stretch_glyph(ctx, self, height, Axis::Y) + pub fn stretch_vertical(&mut self, ctx: &mut MathContext, height: Abs) { + self.stretch(ctx, height, Axis::Y) } /// Try to stretch a glyph to a desired width. - pub fn stretch_horizontal( - self, - ctx: &mut MathContext, - width: Abs, - ) -> VariantFragment { - stretch_glyph(ctx, self, width, Axis::X) + pub fn stretch_horizontal(&mut self, ctx: &mut MathContext, width: Abs) { + self.stretch(ctx, width, Axis::X) + } + + /// Try to stretch a glyph to a desired width or height. + /// + /// The resulting frame may not have the exact desired width or height. + pub fn stretch(&mut self, ctx: &mut MathContext, target: Abs, axis: Axis) { + self.reset_glyph(); + + // If the base glyph is good enough, use it. + let mut advance = self.size.get(axis); + if axis == Axis::X && !self.extended_shape { + // For consistency, we subtract the italics correction from the + // glyph's width if it was added in `update_glyph`. + advance -= self.italics_correction; + } + if target <= advance { + return; + } + + let id = GlyphId(self.item.glyphs[0].id); + let font = self.item.font.clone(); + let Some(construction) = glyph_construction(&font, id, axis) else { return }; + + // Search for a pre-made variant with a good advance. + let mut best_id = id; + let mut best_advance = advance; + for variant in construction.variants { + best_id = variant.variant_glyph; + best_advance = + self.item.font.to_em(variant.advance_measurement).at(self.item.size); + if target <= best_advance { + break; + } + } + + // This is either good or the best we've got. + if target <= best_advance || construction.assembly.is_none() { + self.item.glyphs[0].id = best_id.0; + self.item.glyphs[0].x_advance = + self.item.font.x_advance(best_id.0).unwrap_or_default(); + self.item.glyphs[0].x_offset = Em::zero(); + self.item.glyphs[0].y_advance = + self.item.font.y_advance(best_id.0).unwrap_or_default(); + self.item.glyphs[0].y_offset = Em::zero(); + self.update_glyph(); + return; + } + + // Assemble from parts. + let assembly = construction.assembly.unwrap(); + let min_overlap = min_connector_overlap(&self.item.font) + .unwrap_or_default() + .at(self.item.size); + assemble(ctx, self, assembly, min_overlap, target, axis); + } + + /// Vertically adjust the fragment's frame so that it is centered + /// on the axis. + pub fn center_on_axis(&mut self) { + self.align_on_axis(VAlignment::Horizon); + } + + /// Vertically adjust the fragment's frame so that it is aligned + /// to the given alignment on the axis. + pub fn align_on_axis(&mut self, align: VAlignment) { + let h = self.size.y; + let axis = axis_height(&self.item.font).unwrap().at(self.item.size); + self.align += self.baseline(); + self.baseline = Some(align.inv().position(h + axis * 2.0)); + self.align -= self.baseline(); } } impl Debug for GlyphFragment { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "GlyphFragment({:?})", self.c) - } -} - -#[derive(Clone)] -pub struct VariantFragment { - pub c: char, - pub italics_correction: Abs, - pub accent_attach: (Abs, Abs), - pub frame: Frame, - pub font_size: Abs, - pub class: MathClass, - pub math_size: MathSize, - pub span: Span, - pub limits: Limits, - pub mid_stretched: Option, - pub extended_shape: bool, -} - -impl VariantFragment { - /// Vertically adjust the fragment's frame so that it is centered - /// on the axis. - pub fn center_on_axis(&mut self, ctx: &MathContext) { - self.align_on_axis(ctx, VAlignment::Horizon) - } - - /// Vertically adjust the fragment's frame so that it is aligned - /// to the given alignment on the axis. - pub fn align_on_axis(&mut self, ctx: &MathContext, align: VAlignment) { - let h = self.frame.height(); - let axis = ctx.constants.axis_height().scaled(ctx, self.font_size); - self.frame.set_baseline(align.inv().position(h + axis * 2.0)); - } -} - -impl Debug for VariantFragment { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "VariantFragment({:?})", self.c) + write!(f, "GlyphFragment({:?})", self.item.text) } } @@ -566,46 +592,47 @@ impl FrameFragment { } } +fn ascent_descent(font: &Font, id: GlyphId) -> Option<(Em, Em)> { + let bbox = font.ttf().glyph_bounding_box(id)?; + Some((font.to_em(bbox.y_max), -font.to_em(bbox.y_min))) +} + /// Look up the italics correction for a glyph. -fn italics_correction(ctx: &MathContext, id: GlyphId, font_size: Abs) -> Option { - Some( - ctx.table - .glyph_info? - .italic_corrections? - .get(id)? - .scaled(ctx, font_size), - ) +fn italics_correction(font: &Font, id: GlyphId) -> Option { + font.ttf() + .tables() + .math? + .glyph_info? + .italic_corrections? + .get(id) + .map(|value| font.to_em(value.value)) } /// Loop up the top accent attachment position for a glyph. -fn accent_attach(ctx: &MathContext, id: GlyphId, font_size: Abs) -> Option { - Some( - ctx.table - .glyph_info? - .top_accent_attachments? - .get(id)? - .scaled(ctx, font_size), - ) +fn accent_attach(font: &Font, id: GlyphId) -> Option { + font.ttf() + .tables() + .math? + .glyph_info? + .top_accent_attachments? + .get(id) + .map(|value| font.to_em(value.value)) } /// Look up whether a glyph is an extended shape. -fn is_extended_shape(ctx: &MathContext, id: GlyphId) -> bool { - ctx.table - .glyph_info - .and_then(|info| info.extended_shapes) - .and_then(|info| info.get(id)) +fn is_extended_shape(font: &Font, id: GlyphId) -> bool { + font.ttf() + .tables() + .math + .and_then(|math| math.glyph_info) + .and_then(|glyph_info| glyph_info.extended_shapes) + .and_then(|coverage| coverage.get(id)) .is_some() } /// Look up a kerning value at a specific corner and height. -fn kern_at_height( - ctx: &MathContext, - font_size: Abs, - id: GlyphId, - corner: Corner, - height: Abs, -) -> Option { - let kerns = ctx.table.glyph_info?.kern_infos?.get(id)?; +fn kern_at_height(font: &Font, id: GlyphId, corner: Corner, height: Em) -> Option { + let kerns = font.ttf().tables().math?.glyph_info?.kern_infos?.get(id)?; let kern = match corner { Corner::TopLeft => kerns.top_left, Corner::TopRight => kerns.top_right, @@ -614,11 +641,187 @@ fn kern_at_height( }?; let mut i = 0; - while i < kern.count() && height > kern.height(i)?.scaled(ctx, font_size) { + while i < kern.count() && height > font.to_em(kern.height(i)?.value) { i += 1; } - Some(kern.kern(i)?.scaled(ctx, font_size)) + Some(font.to_em(kern.kern(i)?.value)) +} + +fn axis_height(font: &Font) -> Option { + Some(font.to_em(font.ttf().tables().math?.constants?.axis_height().value)) +} + +pub fn stretch_axes(font: &Font, id: u16) -> Axes { + let id = GlyphId(id); + let horizontal = font + .ttf() + .tables() + .math + .and_then(|math| math.variants) + .and_then(|variants| variants.horizontal_constructions.get(id)) + .is_some(); + let vertical = font + .ttf() + .tables() + .math + .and_then(|math| math.variants) + .and_then(|variants| variants.vertical_constructions.get(id)) + .is_some(); + + Axes::new(horizontal, vertical) +} + +fn min_connector_overlap(font: &Font) -> Option { + font.ttf() + .tables() + .math? + .variants + .map(|variants| font.to_em(variants.min_connector_overlap)) +} + +fn glyph_construction(font: &Font, id: GlyphId, axis: Axis) -> Option { + font.ttf() + .tables() + .math? + .variants + .map(|variants| match axis { + Axis::X => variants.horizontal_constructions, + Axis::Y => variants.vertical_constructions, + })? + .get(id) +} + +/// Assemble a glyph from parts. +fn assemble( + ctx: &mut MathContext, + base: &mut GlyphFragment, + assembly: GlyphAssembly, + min_overlap: Abs, + target: Abs, + axis: Axis, +) { + // Determine the number of times the extenders need to be repeated as well + // as a ratio specifying how much to spread the parts apart + // (0 = maximal overlap, 1 = minimal overlap). + let mut full; + let mut ratio; + let mut repeat = 0; + loop { + full = Abs::zero(); + ratio = 0.0; + + let mut parts = parts(assembly, repeat).peekable(); + let mut growable = Abs::zero(); + + while let Some(part) = parts.next() { + let mut advance = base.item.font.to_em(part.full_advance).at(base.item.size); + if let Some(next) = parts.peek() { + let max_overlap = base + .item + .font + .to_em(part.end_connector_length.min(next.start_connector_length)) + .at(base.item.size); + if max_overlap < min_overlap { + // This condition happening is indicative of a bug in the + // font. + ctx.engine.sink.warn(warning!( + base.item.glyphs[0].span.0, + "glyph has assembly parts with overlap less than minConnectorOverlap"; + hint: "its rendering may appear broken - this is probably a font bug"; + hint: "please file an issue at https://github.com/typst/typst/issues" + )); + } + + advance -= max_overlap; + growable += max_overlap - min_overlap; + } + + full += advance; + } + + if full < target { + let delta = target - full; + ratio = (delta / growable).min(1.0); + full += ratio * growable; + } + + if target <= full || repeat >= MAX_REPEATS { + break; + } + + repeat += 1; + } + + let mut glyphs = vec![]; + let mut parts = parts(assembly, repeat).peekable(); + while let Some(part) = parts.next() { + let mut advance = base.item.font.to_em(part.full_advance).at(base.item.size); + if let Some(next) = parts.peek() { + let max_overlap = base + .item + .font + .to_em(part.end_connector_length.min(next.start_connector_length)) + .at(base.item.size); + advance -= max_overlap; + advance += ratio * (max_overlap - min_overlap); + } + let (x, y) = match axis { + Axis::X => (Em::from_length(advance, base.item.size), Em::zero()), + Axis::Y => (Em::zero(), Em::from_length(advance, base.item.size)), + }; + glyphs.push(Glyph { + id: part.glyph_id.0, + x_advance: x, + x_offset: Em::zero(), + y_advance: y, + y_offset: Em::zero(), + ..base.item.glyphs[0].clone() + }); + } + + match axis { + Axis::X => base.size.x = full, + Axis::Y => { + base.baseline = None; + base.size.y = full; + base.size.x = glyphs + .iter() + .map(|glyph| base.item.font.x_advance(glyph.id).unwrap_or_default()) + .max() + .unwrap_or_default() + .at(base.item.size); + } + } + + base.item.glyphs = glyphs; + base.italics_correction = base + .item + .font + .to_em(assembly.italics_correction.value) + .at(base.item.size); + if axis == Axis::X { + base.accent_attach = (full / 2.0, full / 2.0); + } + base.mid_stretched = None; + base.extended_shape = true; +} + +/// Return an iterator over the assembly's parts with extenders repeated the +/// specified number of times. +fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator + '_ { + assembly.parts.into_iter().flat_map(move |part| { + let count = if part.part_flags.extender() { repeat } else { 1 }; + std::iter::repeat_n(part, count) + }) +} + +pub fn has_dtls_feat(font: &Font) -> bool { + font.ttf() + .tables() + .gsub + .and_then(|gsub| gsub.features.index(ttf_parser::Tag::from_bytes(b"dtls"))) + .is_some() } /// Describes in which situation a frame should use limits for attachments. @@ -671,56 +874,3 @@ impl Limits { 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: Option>, feature: Feature) -> Option { - let gsub = gsub?; - let table = gsub - .features - .find(feature.tag) - .and_then(|feature| feature.lookup_indices.get(0)) - .and_then(|index| gsub.lookups.get(index))?; - let table = table.subtables.get::(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, - alt_value: Option, - ) -> Option { - 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(alt_value.unwrap_or(*value) as u16)), - } - } - - pub fn apply(&self, glyph_id: GlyphId) -> GlyphId { - self.try_apply(glyph_id, None).unwrap_or(glyph_id) - } -} diff --git a/crates/typst-layout/src/math/lr.rs b/crates/typst-layout/src/math/lr.rs index bf8235411..e0caf4179 100644 --- a/crates/typst-layout/src/math/lr.rs +++ b/crates/typst-layout/src/math/lr.rs @@ -45,20 +45,20 @@ pub fn layout_lr( // Scale up fragments at both ends. match inner_fragments { - [one] => scale(ctx, styles, one, relative_to, height, None), + [one] => scale(ctx, 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)); + scale(ctx, first, relative_to, height, Some(MathClass::Opening)); + scale(ctx, last, relative_to, height, Some(MathClass::Closing)); } _ => {} } - // Handle MathFragment::Variant fragments that should be scaled up. + // Handle MathFragment::Glyph fragments that should be scaled up. for fragment in inner_fragments.iter_mut() { - 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)); + if let MathFragment::Glyph(ref mut glyph) = fragment { + if glyph.mid_stretched == Some(false) { + glyph.mid_stretched = Some(true); + scale(ctx, fragment, relative_to, height, Some(MathClass::Large)); } } } @@ -95,18 +95,9 @@ pub fn layout_mid( 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; - } - _ => {} + if let MathFragment::Glyph(ref mut glyph) = fragment { + glyph.mid_stretched = Some(false); + glyph.class = MathClass::Fence; } } @@ -117,7 +108,6 @@ pub fn layout_mid( /// Scale a math fragment to a height. fn scale( ctx: &mut MathContext, - styles: StyleChain, fragment: &mut MathFragment, relative_to: Abs, height: Rel, @@ -132,7 +122,6 @@ fn scale( let short_fall = DELIM_SHORT_FALL.at(fragment.font_size().unwrap_or_default()); stretch_fragment( ctx, - styles, fragment, Some(Axis::Y), Some(relative_to), diff --git a/crates/typst-layout/src/math/mat.rs b/crates/typst-layout/src/math/mat.rs index e509cecc7..278b1343e 100644 --- a/crates/typst-layout/src/math/mat.rs +++ b/crates/typst-layout/src/math/mat.rs @@ -9,8 +9,8 @@ use typst_library::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape}; use typst_syntax::Span; use super::{ - alignments, delimiter_alignment, style_for_denominator, AlignmentResult, - FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL, + alignments, style_for_denominator, AlignmentResult, FrameFragment, GlyphFragment, + LeftRightAlternator, MathContext, DELIM_SHORT_FALL, }; const VERTICAL_PADDING: Ratio = Ratio::new(0.1); @@ -183,8 +183,12 @@ fn layout_body( // 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()); + let paren = GlyphFragment::new_char( + ctx.font, + styles.chain(&denom_style), + '(', + Span::detached(), + )?; for (column, col) in columns.iter().zip(&mut cols) { for (cell, (ascent, descent)) in column.iter().zip(&mut heights) { @@ -202,8 +206,8 @@ fn layout_body( )); } - ascent.set_max(cell.ascent().max(paren.ascent)); - descent.set_max(cell.descent().max(paren.descent)); + ascent.set_max(cell.ascent().max(paren.ascent())); + descent.set_max(cell.descent().max(paren.descent())); col.push(cell); } @@ -312,19 +316,19 @@ fn layout_delimiters( 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)); + if let Some(left_c) = left { + let mut left = GlyphFragment::new_char(ctx.font, styles, left_c, span)?; + left.stretch_vertical(ctx, target - short_fall); + left.center_on_axis(); ctx.push(left); } ctx.push(FrameFragment::new(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)); + if let Some(right_c) = right { + let mut right = GlyphFragment::new_char(ctx.font, styles, right_c, span)?; + right.stretch_vertical(ctx, target - short_fall); + right.center_on_axis(); ctx.push(right); } diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs index 708a4443d..5fd22e578 100644 --- a/crates/typst-layout/src/math/mod.rs +++ b/crates/typst-layout/src/math/mod.rs @@ -13,8 +13,6 @@ mod stretch; mod text; mod underover; -use rustybuzz::Feature; -use ttf_parser::Tag; use typst_library::diag::{bail, SourceResult}; use typst_library::engine::Engine; use typst_library::foundations::{ @@ -30,7 +28,7 @@ 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, + families, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem, }; use typst_library::World; use typst_syntax::Span; @@ -38,11 +36,11 @@ use typst_utils::Numeric; use unicode_math_class::MathClass; use self::fragment::{ - FrameFragment, GlyphFragment, GlyphwiseSubsts, Limits, MathFragment, VariantFragment, + has_dtls_feat, stretch_axes, FrameFragment, GlyphFragment, Limits, MathFragment, }; use self::run::{LeftRightAlternator, MathRun, MathRunFrameBuilder}; use self::shared::*; -use self::stretch::{stretch_fragment, stretch_glyph}; +use self::stretch::stretch_fragment; /// Layout an inline equation (in a paragraph). #[typst_macros::time(span = elem.span())] @@ -58,7 +56,7 @@ pub fn layout_equation_inline( 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 mut ctx = MathContext::new(engine, &mut locator, region, &font); let scale_style = style_for_script_scale(&ctx); let styles = styles.chain(&scale_style); @@ -113,7 +111,7 @@ pub fn layout_equation_block( 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 mut ctx = MathContext::new(engine, &mut locator, regions.base(), &font); let scale_style = style_for_script_scale(&ctx); let styles = styles.chain(&scale_style); @@ -374,14 +372,7 @@ struct MathContext<'a, 'v, 'e> { 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>, - dtls_table: Option>, - flac_table: Option>, - ssty_table: Option>, - glyphwise_tables: Option>>, - space_width: Em, // Mutable. fragments: Vec, } @@ -391,46 +382,20 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> { 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 feat = |tag: &[u8; 4]| { - GlyphwiseSubsts::new(gsub_table, Feature::new(Tag::from_bytes(tag), 0, ..)) - }; - - let features = features(styles); - let glyphwise_tables = Some( - features - .into_iter() - .filter_map(|feature| GlyphwiseSubsts::new(gsub_table, 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); + // These unwraps are safe as the font given is one returned by the + // find_math_font function, which only returns fonts that have a math + // constants table. + let constants = font.ttf().tables().math.unwrap().constants.unwrap(); Self { engine, locator, region: Region::new(base, Axes::splat(false)), font, - ttf, - table: math_table, constants, - dtls_table: feat(b"dtls"), - flac_table: feat(b"flac"), - ssty_table: feat(b"ssty"), - glyphwise_tables, - space_width, fragments: vec![], } } @@ -529,7 +494,8 @@ fn layout_realized( if let Some(elem) = elem.to_packed::() { ctx.push(MathFragment::Tag(elem.tag.clone())); } else if elem.is::() { - ctx.push(MathFragment::Space(ctx.space_width.resolve(styles))); + let space_width = ctx.font.space_width().unwrap_or(THICK); + ctx.push(MathFragment::Space(space_width.resolve(styles))); } else if elem.is::() { ctx.push(MathFragment::Linebreak); } else if let Some(elem) = elem.to_packed::() { diff --git a/crates/typst-layout/src/math/root.rs b/crates/typst-layout/src/math/root.rs index 32f527198..91b9b16af 100644 --- a/crates/typst-layout/src/math/root.rs +++ b/crates/typst-layout/src/math/root.rs @@ -49,9 +49,9 @@ pub fn layout_root( // Layout root symbol. let target = radicand.height() + thickness + gap; - let sqrt = GlyphFragment::new(ctx, styles, '√', span) - .stretch_vertical(ctx, target) - .frame; + let mut sqrt = GlyphFragment::new_char(ctx.font, styles, '√', span)?; + sqrt.stretch_vertical(ctx, target); + let sqrt = sqrt.into_frame(); // Layout the index. let sscript = EquationElem::set_size(MathSize::ScriptScript).wrap(); diff --git a/crates/typst-layout/src/math/shared.rs b/crates/typst-layout/src/math/shared.rs index 600c130d4..1f88d2dd7 100644 --- a/crates/typst-layout/src/math/shared.rs +++ b/crates/typst-layout/src/math/shared.rs @@ -1,7 +1,9 @@ use ttf_parser::math::MathValue; +use ttf_parser::Tag; use typst_library::foundations::{Style, StyleChain}; -use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size, VAlignment}; +use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size}; use typst_library::math::{EquationElem, MathSize}; +use typst_library::text::{FontFeatures, TextElem}; use typst_utils::LazyHash; use super::{LeftRightAlternator, MathContext, MathFragment, MathRun}; @@ -59,6 +61,16 @@ pub fn style_cramped() -> LazyHash