shows how adding CSS
+/// rules to `` can make it sensitive to whitespace. For this reason, we
+/// should also respect the `style` tag in the future.
+fn allows_pretty_inside(tag: HtmlTag) -> bool {
+ (tag::is_block_by_default(tag) && tag != tag::pre)
+ || tag::is_tabular_by_default(tag)
+ || tag == tag::li
+}
+
+/// Whether newlines should be added before and after the element if the parent
+/// allows it.
+///
+/// In contrast to `allows_pretty_inside`, which is purely spec-driven, this is
+/// more subjective and depends on preference.
+fn wants_pretty_around(tag: HtmlTag) -> bool {
+ allows_pretty_inside(tag) || tag::is_metadata(tag) || tag == tag::pre
}
/// Escape a character.
diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs
index ffd8e2505..aa769976e 100644
--- a/crates/typst-html/src/lib.rs
+++ b/crates/typst-html/src/lib.rs
@@ -14,9 +14,9 @@ use typst_library::html::{
use typst_library::introspection::{
Introspector, Locator, LocatorLink, SplitLocator, TagElem,
};
-use typst_library::layout::{Abs, Axes, BoxElem, Region, Size};
+use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size};
use typst_library::model::{DocumentInfo, ParElem};
-use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
+use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines};
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
use typst_library::World;
use typst_syntax::Span;
@@ -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 })
}
@@ -139,7 +139,9 @@ fn html_fragment_impl(
let arenas = Arenas::default();
let children = (engine.routines.realize)(
- RealizationKind::HtmlFragment,
+ // No need to know about the `FragmentKind` because we handle both
+ // uniformly.
+ RealizationKind::HtmlFragment(&mut FragmentKind::Block),
&mut engine,
&mut locator,
&arenas,
@@ -189,7 +191,8 @@ fn handle(
};
output.push(element.into());
} else if let Some(elem) = child.to_packed::() {
- let children = handle_list(engine, locator, elem.children.iter(&styles))?;
+ let children =
+ html_fragment(engine, &elem.body, locator.next(&elem.span()), styles)?;
output.push(
HtmlElement::new(tag::p)
.with_children(children)
@@ -197,13 +200,34 @@ fn handle(
.into(),
);
} else if let Some(elem) = child.to_packed::() {
- // FIXME: Very incomplete and hacky, but makes boxes kind fulfill their
- // purpose for now.
+ // TODO: This is rather incomplete.
if let Some(body) = elem.body(styles) {
let children =
html_fragment(engine, body, locator.next(&elem.span()), styles)?;
- output.extend(children);
+ output.push(
+ HtmlElement::new(tag::span)
+ .with_attr(attr::style, "display: inline-block;")
+ .with_children(children)
+ .spanned(elem.span())
+ .into(),
+ )
}
+ } else if let Some((elem, body)) =
+ child
+ .to_packed::()
+ .and_then(|elem| match elem.body(styles) {
+ Some(BlockBody::Content(body)) => Some((elem, body)),
+ _ => None,
+ })
+ {
+ // TODO: This is rather incomplete.
+ let children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
+ output.push(
+ HtmlElement::new(tag::div)
+ .with_children(children)
+ .spanned(elem.span())
+ .into(),
+ );
} else if child.is::() {
output.push(HtmlNode::text(' ', child.span()));
} else if let Some(elem) = child.to_packed::() {
@@ -283,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/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs
index c22ea7e40..91fa53f9a 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();
@@ -398,13 +401,31 @@ fn field_access_completions(
value: &Value,
styles: &Option,
) {
- for (name, value, _) in value.ty().scope().iter() {
- ctx.call_completion(name.clone(), value);
+ 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. We only complete
+ // those which have a `self` parameter.
+ for (name, binding) in scopes.flat_map(|scope| scope.iter()) {
+ 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() {
- 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());
}
}
@@ -414,7 +435,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 {
@@ -452,16 +473,6 @@ fn field_access_completions(
}
}
}
- Value::Plugin(plugin) => {
- for name in plugin.iter() {
- ctx.completions.push(Completion {
- kind: CompletionKind::Func,
- label: name.clone(),
- apply: None,
- detail: None,
- })
- }
- }
_ => {}
}
}
@@ -506,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 {
@@ -525,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 {
@@ -551,9 +562,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());
}
}
}
@@ -666,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 {
@@ -817,19 +828,8 @@ fn param_value_completions<'a>(
) {
if param.name == "font" {
ctx.font_completions();
- } else if param.name == "path" {
- ctx.file_completions_with_extensions(match func.name() {
- Some("image") => &["png", "jpg", "jpeg", "gif", "svg", "svgz"],
- Some("csv") => &["csv"],
- Some("plugin") => &["wasm"],
- Some("cbor") => &["cbor"],
- Some("json") => &["json"],
- Some("toml") => &["toml"],
- Some("xml") => &["xml"],
- Some("yaml") => &["yml", "yaml"],
- Some("bibliography") => &["bib", "yml", "yaml"],
- _ => &[],
- });
+ } else if let Some(extensions) = path_completion(func, param) {
+ ctx.file_completions_with_extensions(extensions);
} else if func.name() == Some("figure") && param.name == "body" {
ctx.snippet_completion("image", "image(\"${}\"),", "An image in a figure.");
ctx.snippet_completion("table", "table(\n ${}\n),", "A table in a figure.");
@@ -838,6 +838,28 @@ fn param_value_completions<'a>(
ctx.cast_completions(¶m.input);
}
+/// 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("csv"), "source") => &["csv"],
+ (Some("plugin"), "source") => &["wasm"],
+ (Some("cbor"), "source") => &["cbor"],
+ (Some("json"), "source") => &["json"],
+ (Some("toml"), "source") => &["toml"],
+ (Some("xml"), "source") => &["xml"],
+ (Some("yaml"), "source") => &["yml", "yaml"],
+ (Some("bibliography"), "sources") => &["bib", "yml", "yaml"],
+ (Some("bibliography"), "style") => &["csl"],
+ (Some("cite"), "style") => &["csl"],
+ (Some("raw"), "syntaxes") => &["sublime-syntax"],
+ (Some("raw"), "theme") => &["tmtheme"],
+ (Some("embed"), "path") => &[],
+ (None, "path") => &[],
+ _ => return None,
+ })
+}
+
/// Resolve a callee expression to a global function.
fn resolve_global_callee<'a>(
ctx: &CompletionContext<'a>,
@@ -845,13 +867,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,
@@ -1443,7 +1463,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());
}
@@ -1463,7 +1483,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);
}
@@ -1747,4 +1768,26 @@ mod tests {
.must_include(["this", "that"])
.must_exclude(["*", "figure"]);
}
+
+ #[test]
+ fn test_autocomplete_type_methods() {
+ test("#\"hello\".", -1).must_include(["len", "contains"]);
+ test("#table().", -1).must_exclude(["cell"]);
+ }
+
+ #[test]
+ fn test_autocomplete_content_methods() {
+ 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/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/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]";
diff --git a/crates/typst-ide/src/matchers.rs b/crates/typst-ide/src/matchers.rs
index d02eb2a95..93fdc5dd5 100644
--- a/crates/typst-ide/src/matchers.rs
+++ b/crates/typst-ide/src/matchers.rs
@@ -1,7 +1,7 @@
use ecow::EcoString;
use typst::foundations::{Module, Value};
use typst::syntax::ast::AstNode;
-use typst::syntax::{ast, LinkedNode, Span, SyntaxKind, SyntaxNode};
+use typst::syntax::{ast, LinkedNode, Span, SyntaxKind};
use crate::{analyze_import, IdeWorld};
@@ -30,38 +30,38 @@ pub fn named_items(
if let Some(v) = node.cast::() {
let imports = v.imports();
- let source = node
- .children()
- .find(|child| child.is::())
- .and_then(|source: LinkedNode| {
- Some((analyze_import(world, &source)?, source))
- });
- let source = source.as_ref();
+ let source = v.source();
+
+ let source_value = node
+ .find(source.span())
+ .and_then(|source| analyze_import(world, &source));
+ let source_value = source_value.as_ref();
+
+ let module = source_value.and_then(|value| match value {
+ Value::Module(module) => Some(module),
+ _ => None,
+ });
+
+ let name_and_span = match (imports, v.new_name()) {
+ // ```plain
+ // import "foo" as name
+ // import "foo" as name: ..
+ // ```
+ (_, Some(name)) => Some((name.get().clone(), name.span())),
+ // ```plain
+ // import "foo"
+ // ```
+ (None, None) => v.bare_name().ok().map(|name| (name, source.span())),
+ // ```plain
+ // import "foo": ..
+ // ```
+ (Some(..), None) => None,
+ };
// Seeing the module itself.
- if let Some((value, source)) = source {
- let site = match (imports, v.new_name()) {
- // ```plain
- // import "foo" as name;
- // import "foo" as name: ..;
- // ```
- (_, Some(name)) => Some(name.to_untyped()),
- // ```plain
- // import "foo";
- // ```
- (None, None) => Some(source.get()),
- // ```plain
- // import "foo": ..;
- // ```
- (Some(..), None) => None,
- };
-
- if let Some((site, value)) =
- site.zip(value.clone().cast::().ok())
- {
- if let Some(res) = recv(NamedItem::Module(&value, site)) {
- return Some(res);
- }
+ if let Some((name, span)) = name_and_span {
+ if let Some(res) = recv(NamedItem::Module(&name, span, module)) {
+ return Some(res);
}
}
@@ -75,9 +75,13 @@ pub fn named_items(
// import "foo": *;
// ```
Some(ast::Imports::Wildcard) => {
- if let Some(scope) = source.and_then(|(value, _)| value.scope()) {
- for (name, value, span) in scope.iter() {
- let item = NamedItem::Import(name, span, Some(value));
+ if let Some(scope) = source_value.and_then(Value::scope) {
+ 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,18 +93,26 @@ pub fn named_items(
// ```
Some(ast::Imports::Items(items)) => {
for item in items.iter() {
- let original = item.original_name();
- let bound = item.bound_name();
- let scope = source.and_then(|(value, _)| value.scope());
- let span = scope
- .and_then(|s| s.get_span(&original))
- .unwrap_or(Span::detached())
- .or(bound.span());
+ 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));
- let value = scope.and_then(|s| s.get(&original));
- if let Some(res) =
- recv(NamedItem::Import(bound.get(), span, value))
- {
+ 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 item = NamedItem::Import(bound.get(), span, value);
+ if let Some(res) = recv(item) {
return Some(res);
}
}
@@ -169,8 +181,8 @@ pub enum NamedItem<'a> {
Var(ast::Ident<'a>),
/// A function item.
Fn(ast::Ident<'a>),
- /// A (imported) module item.
- Module(&'a Module, &'a SyntaxNode),
+ /// A (imported) module.
+ Module(&'a EcoString, Span, Option<&'a Module>),
/// An imported item.
Import(&'a EcoString, Span, Option<&'a Value>),
}
@@ -180,7 +192,7 @@ impl<'a> NamedItem<'a> {
match self {
NamedItem::Var(ident) => ident.get(),
NamedItem::Fn(ident) => ident.get(),
- NamedItem::Module(value, _) => value.name(),
+ NamedItem::Module(name, _, _) => name,
NamedItem::Import(name, _, _) => name,
}
}
@@ -188,7 +200,7 @@ impl<'a> NamedItem<'a> {
pub(crate) fn value(&self) -> Option {
match self {
NamedItem::Var(..) | NamedItem::Fn(..) => None,
- NamedItem::Module(value, _) => Some(Value::Module((*value).clone())),
+ NamedItem::Module(_, _, value) => value.cloned().map(Value::Module),
NamedItem::Import(_, _, value) => value.cloned(),
}
}
@@ -196,7 +208,7 @@ impl<'a> NamedItem<'a> {
pub(crate) fn span(&self) -> Span {
match *self {
NamedItem::Var(name) | NamedItem::Fn(name) => name.span(),
- NamedItem::Module(_, site) => site.span(),
+ NamedItem::Module(_, span, _) => span,
NamedItem::Import(_, span, _) => span,
}
}
@@ -220,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)
}
@@ -269,16 +283,18 @@ mod tests {
use std::borrow::Borrow;
use ecow::EcoString;
+ use typst::foundations::Value;
use typst::syntax::{LinkedNode, Side};
use super::named_items;
- use crate::tests::{FilePos, WorldLike};
+ use crate::tests::{FilePos, TestWorld, WorldLike};
- type Response = Vec;
+ type Response = Vec<(EcoString, Option)>;
trait ResponseExt {
fn must_include<'a>(&self, includes: impl IntoIterator- ) -> &Self;
fn must_exclude<'a>(&self, excludes: impl IntoIterator
- ) -> &Self;
+ fn must_include_value(&self, name_value: (&str, Option<&Value>)) -> &Self;
}
impl ResponseExt for Response {
@@ -286,7 +302,7 @@ mod tests {
fn must_include<'a>(&self, includes: impl IntoIterator
- ) -> &Self {
for item in includes {
assert!(
- self.iter().any(|v| v == item),
+ self.iter().any(|v| v.0 == item),
"{item:?} was not contained in {self:?}",
);
}
@@ -297,12 +313,21 @@ mod tests {
fn must_exclude<'a>(&self, excludes: impl IntoIterator
- ) -> &Self {
for item in excludes {
assert!(
- !self.iter().any(|v| v == item),
+ !self.iter().any(|v| v.0 == item),
"{item:?} was wrongly contained in {self:?}",
);
}
self
}
+
+ #[track_caller]
+ fn must_include_value(&self, name_value: (&str, Option<&Value>)) -> &Self {
+ assert!(
+ self.iter().any(|v| (v.0.as_str(), v.1.as_ref()) == name_value),
+ "{name_value:?} was not contained in {self:?}",
+ );
+ self
+ }
}
#[track_caller]
@@ -314,7 +339,7 @@ mod tests {
let leaf = node.leaf_at(cursor, Side::After).unwrap();
let mut items = vec![];
named_items(world, leaf, |s| {
- items.push(s.name().clone());
+ items.push((s.name().clone(), s.value().clone()));
None::<()>
});
items
@@ -339,6 +364,21 @@ mod tests {
#[test]
fn test_named_items_import() {
- test("#import \"foo.typ\": a; #(a);", 2).must_include(["a"]);
+ test("#import \"foo.typ\"", 2).must_include(["foo"]);
+ test("#import \"foo.typ\" as bar", 2)
+ .must_include(["bar"])
+ .must_exclude(["foo"]);
+ }
+
+ #[test]
+ fn test_named_items_import_items() {
+ test("#import \"foo.typ\": a; #(a);", 2)
+ .must_include(["a"])
+ .must_exclude(["foo"]);
+
+ let world = TestWorld::new("#import \"foo.typ\": a.b; #(b);")
+ .with_source("foo.typ", "#import \"a.typ\"")
+ .with_source("a.typ", "#let b = 1;");
+ test(&world, 2).must_include_value(("b", Some(&Value::Int(1))));
}
}
diff --git a/crates/typst-ide/src/tests.rs b/crates/typst-ide/src/tests.rs
index f41808dac..6678ab841 100644
--- a/crates/typst-ide/src/tests.rs
+++ b/crates/typst-ide/src/tests.rs
@@ -55,7 +55,7 @@ impl TestWorld {
pub fn with_asset_at(mut self, path: &str, filename: &str) -> Self {
let id = FileId::new(None, VirtualPath::new(path));
let data = typst_dev_assets::get_by_name(filename).unwrap();
- let bytes = Bytes::from_static(data);
+ let bytes = Bytes::new(data);
Arc::make_mut(&mut self.files).assets.insert(id, bytes);
self
}
@@ -152,7 +152,7 @@ impl Default for TestBase {
fn default() -> Self {
let fonts: Vec<_> = typst_assets::fonts()
.chain(typst_dev_assets::fonts())
- .flat_map(|data| Font::iter(Bytes::from_static(data)))
+ .flat_map(|data| Font::iter(Bytes::new(data)))
.collect();
Self {
diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs
index 99ae0620b..cbfffe530 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};
@@ -201,12 +201,17 @@ 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,
};
// Find metadata about the function.
- if let Some(Value::Func(func)) = world.library().global.scope().get(&callee);
+ if let Some(Value::Func(func)) = world
+ .library()
+ .global
+ .scope()
+ .get(&callee)
+ .map(Binding::read);
then { (func, named) }
else { return None; }
};
@@ -352,6 +357,13 @@ mod tests {
.must_be_text("This closure captures `f` and `y`");
}
+ #[test]
+ fn test_tooltip_import() {
+ let world = TestWorld::new("#import \"other.typ\": a, b")
+ .with_source("other.typ", "#let (a, b, c) = (1, 2, 3)");
+ test(&world, -5, Side::After).must_be_code("1");
+ }
+
#[test]
fn test_tooltip_star_import() {
let world = TestWorld::new("#import \"other.typ\": *")
diff --git a/crates/typst-ide/src/utils.rs b/crates/typst-ide/src/utils.rs
index cd66ec8f0..d5d584e2b 100644
--- a/crates/typst-ide/src/utils.rs
+++ b/crates/typst-ide/src/utils.rs
@@ -171,7 +171,7 @@ where
self.find_iter(content.fields().iter().map(|(_, v)| v))?;
}
Value::Module(module) => {
- self.find_iter(module.scope().iter().map(|(_, v, _)| v))?;
+ self.find_iter(module.scope().iter().map(|(_, b)| b.read()))?;
}
_ => {}
}
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/fonts.rs b/crates/typst-kit/src/fonts.rs
index 83e13fd8f..c15d739ec 100644
--- a/crates/typst-kit/src/fonts.rs
+++ b/crates/typst-kit/src/fonts.rs
@@ -13,6 +13,7 @@ use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use fontdb::{Database, Source};
+use typst_library::foundations::Bytes;
use typst_library::text::{Font, FontBook, FontInfo};
use typst_timing::TimingScope;
@@ -52,9 +53,8 @@ impl FontSlot {
.as_ref()
.expect("`path` is not `None` if `font` is uninitialized"),
)
- .ok()?
- .into();
- Font::new(data, self.index)
+ .ok()?;
+ Font::new(Bytes::new(data), self.index)
})
.clone()
}
@@ -196,7 +196,7 @@ impl FontSearcher {
#[cfg(feature = "embed-fonts")]
fn add_embedded(&mut self) {
for data in typst_assets::fonts() {
- let buffer = typst_library::foundations::Bytes::from_static(data);
+ let buffer = Bytes::new(data);
for (i, font) in Font::iter(buffer).enumerate() {
self.book.push(font.info().clone());
self.fonts.push(FontSlot {
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())
+ )
+ }
+}
diff --git a/crates/typst-layout/src/flow/collect.rs b/crates/typst-layout/src/flow/collect.rs
index 12cfa152e..2c14f7a37 100644
--- a/crates/typst-layout/src/flow/collect.rs
+++ b/crates/typst-layout/src/flow/collect.rs
@@ -20,12 +20,16 @@ use typst_library::model::ParElem;
use typst_library::routines::{Pair, Routines};
use typst_library::text::TextElem;
use typst_library::World;
+use typst_utils::SliceExt;
-use super::{layout_multi_block, layout_single_block};
+use super::{layout_multi_block, layout_single_block, FlowMode};
+use crate::inline::ParSituation;
+use crate::modifiers::layout_and_modify;
/// Collects all elements of the flow into prepared children. These are much
/// simpler to handle than the raw elements.
#[typst_macros::time]
+#[allow(clippy::too_many_arguments)]
pub fn collect<'a>(
engine: &mut Engine,
bump: &'a Bump,
@@ -33,6 +37,7 @@ pub fn collect<'a>(
locator: Locator<'a>,
base: Size,
expand: bool,
+ mode: FlowMode,
) -> SourceResult>> {
Collector {
engine,
@@ -42,9 +47,9 @@ pub fn collect<'a>(
base,
expand,
output: Vec::with_capacity(children.len()),
- last_was_par: false,
+ par_situation: ParSituation::First,
}
- .run()
+ .run(mode)
}
/// State for collection.
@@ -56,12 +61,20 @@ struct Collector<'a, 'x, 'y> {
expand: bool,
locator: SplitLocator<'a>,
output: Vec>,
- last_was_par: bool,
+ par_situation: ParSituation,
}
impl<'a> Collector<'a, '_, '_> {
/// Perform the collection.
- fn run(mut self) -> SourceResult>> {
+ fn run(self, mode: FlowMode) -> SourceResult>> {
+ match mode {
+ FlowMode::Root | FlowMode::Block => self.run_block(),
+ FlowMode::Inline => self.run_inline(),
+ }
+ }
+
+ /// Perform collection for block-level children.
+ fn run_block(mut self) -> SourceResult>> {
for &(child, styles) in self.children {
if let Some(elem) = child.to_packed::() {
self.output.push(Child::Tag(&elem.tag));
@@ -94,6 +107,42 @@ impl<'a> Collector<'a, '_, '_> {
Ok(self.output)
}
+ /// Perform collection for inline-level children.
+ fn run_inline(mut self) -> SourceResult>> {
+ // Extract leading and trailing tags.
+ let (start, end) = self.children.split_prefix_suffix(|(c, _)| c.is::());
+ let inner = &self.children[start..end];
+
+ // Compute the shared styles, ignoring tags.
+ let styles = StyleChain::trunk(inner.iter().map(|&(_, s)| s)).unwrap_or_default();
+
+ // Layout the lines.
+ let lines = crate::inline::layout_inline(
+ self.engine,
+ inner,
+ &mut self.locator,
+ styles,
+ self.base,
+ self.expand,
+ )?
+ .into_frames();
+
+ for (c, _) in &self.children[..start] {
+ let elem = c.to_packed::().unwrap();
+ self.output.push(Child::Tag(&elem.tag));
+ }
+
+ let leading = ParElem::leading_in(styles);
+ self.lines(lines, leading, styles);
+
+ for (c, _) in &self.children[end..] {
+ let elem = c.to_packed::().unwrap();
+ self.output.push(Child::Tag(&elem.tag));
+ }
+
+ Ok(self.output)
+ }
+
/// Collect vertical spacing into a relative or fractional child.
fn v(&mut self, elem: &'a Packed, styles: StyleChain<'a>) {
self.output.push(match elem.amount {
@@ -109,24 +158,35 @@ impl<'a> Collector<'a, '_, '_> {
elem: &'a Packed,
styles: StyleChain<'a>,
) -> SourceResult<()> {
- let align = AlignElem::alignment_in(styles).resolve(styles);
- let leading = ParElem::leading_in(styles);
- let spacing = ParElem::spacing_in(styles);
- let costs = TextElem::costs_in(styles);
-
- let lines = crate::layout_inline(
+ let lines = crate::inline::layout_par(
+ elem,
self.engine,
- &elem.children,
self.locator.next(&elem.span()),
styles,
- self.last_was_par,
self.base,
self.expand,
+ self.par_situation,
)?
.into_frames();
+ let spacing = elem.spacing(styles);
+ let leading = elem.leading(styles);
+
self.output.push(Child::Rel(spacing.into(), 4));
+ self.lines(lines, leading, styles);
+
+ self.output.push(Child::Rel(spacing.into(), 4));
+ self.par_situation = ParSituation::Consecutive;
+
+ Ok(())
+ }
+
+ /// Collect laid-out lines.
+ fn lines(&mut self, lines: Vec , leading: Abs, styles: StyleChain<'a>) {
+ let align = AlignElem::alignment_in(styles).resolve(styles);
+ let costs = TextElem::costs_in(styles);
+
// Determine whether to prevent widow and orphans.
let len = lines.len();
let prevent_orphans =
@@ -165,11 +225,6 @@ impl<'a> Collector<'a, '_, '_> {
self.output
.push(Child::Line(self.boxed(LineChild { frame, align, need })));
}
-
- self.output.push(Child::Rel(spacing.into(), 4));
- self.last_was_par = true;
-
- Ok(())
}
/// Collect a block into a [`SingleChild`] or [`MultiChild`] depending on
@@ -218,7 +273,7 @@ impl<'a> Collector<'a, '_, '_> {
};
self.output.push(spacing(elem.below(styles)));
- self.last_was_par = false;
+ self.par_situation = ParSituation::Other;
}
/// Collects a placed element into a [`PlacedChild`].
@@ -377,8 +432,9 @@ fn layout_single_impl(
route: Route::extend(route),
};
- layout_single_block(elem, &mut engine, locator, styles, region)
- .map(|frame| frame.post_processed(styles))
+ layout_and_modify(styles, |styles| {
+ layout_single_block(elem, &mut engine, locator, styles, region)
+ })
}
/// A child that encapsulates a prepared breakable block.
@@ -473,11 +529,8 @@ fn layout_multi_impl(
route: Route::extend(route),
};
- layout_multi_block(elem, &mut engine, locator, styles, regions).map(|mut fragment| {
- for frame in &mut fragment {
- frame.post_process(styles);
- }
- fragment
+ layout_and_modify(styles, |styles| {
+ layout_multi_block(elem, &mut engine, locator, styles, regions)
})
}
@@ -579,20 +632,23 @@ impl PlacedChild<'_> {
self.cell.get_or_init(base, |base| {
let align = self.alignment.unwrap_or_else(|| Alignment::CENTER);
let aligned = AlignElem::set_alignment(align).wrap();
+ let styles = self.styles.chain(&aligned);
- let mut frame = crate::layout_frame(
- engine,
- &self.elem.body,
- self.locator.relayout(),
- self.styles.chain(&aligned),
- Region::new(base, Axes::splat(false)),
- )?;
+ let mut frame = layout_and_modify(styles, |styles| {
+ crate::layout_frame(
+ engine,
+ &self.elem.body,
+ self.locator.relayout(),
+ styles,
+ Region::new(base, Axes::splat(false)),
+ )
+ })?;
if self.float {
frame.set_parent(self.elem.location().unwrap());
}
- Ok(frame.post_processed(self.styles))
+ Ok(frame)
})
}
diff --git a/crates/typst-layout/src/flow/compose.rs b/crates/typst-layout/src/flow/compose.rs
index 3cf66f9e3..76af8f650 100644
--- a/crates/typst-layout/src/flow/compose.rs
+++ b/crates/typst-layout/src/flow/compose.rs
@@ -17,7 +17,9 @@ use typst_library::model::{
use typst_syntax::Span;
use typst_utils::{NonZeroExt, Numeric};
-use super::{distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Stop, Work};
+use super::{
+ distribute, Config, FlowMode, FlowResult, LineNumberConfig, PlacedChild, Stop, Work,
+};
/// Composes the contents of a single page/region. A region can have multiple
/// columns/subregions.
@@ -356,7 +358,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
migratable: bool,
) -> FlowResult<()> {
// Footnotes are only supported at the root level.
- if !self.config.root {
+ if self.config.mode != FlowMode::Root {
return Ok(());
}
diff --git a/crates/typst-layout/src/flow/mod.rs b/crates/typst-layout/src/flow/mod.rs
index 2f0ec39a9..cba228bcd 100644
--- a/crates/typst-layout/src/flow/mod.rs
+++ b/crates/typst-layout/src/flow/mod.rs
@@ -25,7 +25,7 @@ use typst_library::layout::{
Regions, Rel, Size,
};
use typst_library::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine};
-use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
+use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines};
use typst_library::text::TextElem;
use typst_library::World;
use typst_utils::{NonZeroExt, Numeric};
@@ -140,9 +140,10 @@ fn layout_fragment_impl(
engine.route.check_layout_depth().at(content.span())?;
+ let mut kind = FragmentKind::Block;
let arenas = Arenas::default();
let children = (engine.routines.realize)(
- RealizationKind::LayoutFragment,
+ RealizationKind::LayoutFragment(&mut kind),
&mut engine,
&mut locator,
&arenas,
@@ -158,62 +159,45 @@ fn layout_fragment_impl(
regions,
columns,
column_gutter,
- false,
+ kind.into(),
)
}
+/// The mode a flow can be laid out in.
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub enum FlowMode {
+ /// A root flow with block-level elements. Like `FlowMode::Block`, but can
+ /// additionally host footnotes and line numbers.
+ Root,
+ /// A flow whose children are block-level elements.
+ Block,
+ /// A flow whose children are inline-level elements.
+ Inline,
+}
+
+impl From for FlowMode {
+ fn from(value: FragmentKind) -> Self {
+ match value {
+ FragmentKind::Inline => Self::Inline,
+ FragmentKind::Block => Self::Block,
+ }
+ }
+}
+
/// Lays out realized content into regions, potentially with columns.
#[allow(clippy::too_many_arguments)]
-pub(crate) fn layout_flow(
+pub fn layout_flow<'a>(
engine: &mut Engine,
- children: &[Pair],
- locator: &mut SplitLocator,
- shared: StyleChain,
+ children: &[Pair<'a>],
+ locator: &mut SplitLocator<'a>,
+ shared: StyleChain<'a>,
mut regions: Regions,
columns: NonZeroUsize,
column_gutter: Rel,
- root: bool,
+ mode: FlowMode,
) -> SourceResult {
// Prepare configuration that is shared across the whole flow.
- let config = Config {
- root,
- shared,
- columns: {
- let mut count = columns.get();
- if !regions.size.x.is_finite() {
- count = 1;
- }
-
- let gutter = column_gutter.relative_to(regions.base().x);
- let width = (regions.size.x - gutter * (count - 1) as f64) / count as f64;
- let dir = TextElem::dir_in(shared);
- ColumnConfig { count, width, gutter, dir }
- },
- footnote: FootnoteConfig {
- separator: FootnoteEntry::separator_in(shared),
- clearance: FootnoteEntry::clearance_in(shared),
- gap: FootnoteEntry::gap_in(shared),
- expand: regions.expand.x,
- },
- line_numbers: root.then(|| LineNumberConfig {
- scope: ParLine::numbering_scope_in(shared),
- default_clearance: {
- let width = if PageElem::flipped_in(shared) {
- PageElem::height_in(shared)
- } else {
- PageElem::width_in(shared)
- };
-
- // Clamp below is safe (min <= max): if the font size is
- // negative, we set min = max = 0; otherwise,
- // `0.75 * size <= 2.5 * size` for zero and positive sizes.
- (0.026 * width.unwrap_or_default()).clamp(
- Em::new(0.75).resolve(shared).max(Abs::zero()),
- Em::new(2.5).resolve(shared).max(Abs::zero()),
- )
- },
- }),
- };
+ 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.
@@ -225,6 +209,7 @@ pub(crate) fn layout_flow(
locator.next(&()),
Size::new(config.columns.width, regions.full),
regions.expand.x,
+ mode,
)?;
let mut work = Work::new(&children);
@@ -247,6 +232,55 @@ pub(crate) fn layout_flow(
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: {
+ let mut count = columns.get();
+ if !regions.size.x.is_finite() {
+ count = 1;
+ }
+
+ let gutter = column_gutter.relative_to(regions.base().x);
+ let width = (regions.size.x - gutter * (count - 1) as f64) / count as f64;
+ let dir = TextElem::dir_in(shared);
+ ColumnConfig { count, width, gutter, dir }
+ },
+ footnote: FootnoteConfig {
+ separator: FootnoteEntry::separator_in(shared),
+ clearance: FootnoteEntry::clearance_in(shared),
+ gap: FootnoteEntry::gap_in(shared),
+ expand: regions.expand.x,
+ },
+ line_numbers: (mode == FlowMode::Root).then(|| LineNumberConfig {
+ scope: ParLine::numbering_scope_in(shared),
+ default_clearance: {
+ let width = if PageElem::flipped_in(shared) {
+ PageElem::height_in(shared)
+ } else {
+ PageElem::width_in(shared)
+ };
+
+ // Clamp below is safe (min <= max): if the font size is
+ // negative, we set min = max = 0; otherwise,
+ // `0.75 * size <= 2.5 * size` for zero and positive sizes.
+ (0.026 * width.unwrap_or_default()).clamp(
+ Em::new(0.75).resolve(shared).max(Abs::zero()),
+ Em::new(2.5).resolve(shared).max(Abs::zero()),
+ )
+ },
+ }),
+ }
+}
+
/// The work that is left to do by flow layout.
///
/// The lifetimes 'a and 'b are used across flow layout:
@@ -318,7 +352,7 @@ impl<'a, 'b> Work<'a, 'b> {
struct Config<'x> {
/// Whether this is the root flow, which can host footnotes and line
/// numbers.
- root: bool,
+ mode: FlowMode,
/// The styles shared by the whole flow. This is used for footnotes and line
/// numbers.
shared: StyleChain<'x>,
diff --git a/crates/typst-layout/src/grid/layouter.rs b/crates/typst-layout/src/grid/layouter.rs
index 7c94617dc..af47ff72f 100644
--- a/crates/typst-layout/src/grid/layouter.rs
+++ b/crates/typst-layout/src/grid/layouter.rs
@@ -3,6 +3,7 @@ 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::{
Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel,
Size, Sizing,
@@ -13,8 +14,8 @@ use typst_syntax::Span;
use typst_utils::{MaybeReverseIter, Numeric};
use super::{
- generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Cell, CellGrid,
- LinePosition, LineSegment, Repeatable, Rowspan, UnbreakableRowGroup,
+ generate_line_segments, hline_stroke_at_column, layout_cell, vline_stroke_at_row,
+ LineSegment, Rowspan, UnbreakableRowGroup,
};
/// Performs grid layout.
@@ -843,7 +844,8 @@ impl<'a> GridLayouter<'a> {
let size = Size::new(available, height);
let pod = Region::new(size, Axes::splat(false));
- let frame = cell.layout(engine, 0, self.styles, pod.into())?.into_frame();
+ let frame =
+ layout_cell(cell, engine, 0, self.styles, pod.into())?.into_frame();
resolved.set_max(frame.width() - already_covered_width);
}
@@ -1086,7 +1088,7 @@ impl<'a> GridLayouter<'a> {
};
let frames =
- cell.layout(engine, disambiguator, self.styles, pod)?.into_frames();
+ layout_cell(cell, engine, disambiguator, self.styles, pod)?.into_frames();
// Skip the first region if one cell in it is empty. Then,
// remeasure.
@@ -1252,9 +1254,9 @@ impl<'a> GridLayouter<'a> {
// rows.
pod.full = self.regions.full;
}
- let frame = cell
- .layout(engine, disambiguator, self.styles, pod)?
- .into_frame();
+ let frame =
+ layout_cell(cell, engine, disambiguator, self.styles, pod)?
+ .into_frame();
let mut pos = pos;
if self.is_rtl {
// In the grid, cell colspans expand to the right,
@@ -1310,7 +1312,7 @@ impl<'a> GridLayouter<'a> {
// Push the layouted frames into the individual output frames.
let fragment =
- cell.layout(engine, disambiguator, self.styles, pod)?;
+ layout_cell(cell, engine, disambiguator, self.styles, pod)?;
for (output, frame) in outputs.iter_mut().zip(fragment) {
let mut pos = pos;
if self.is_rtl {
@@ -1375,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,
@@ -1444,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
@@ -1492,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
@@ -1578,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 3e89612a1..7549673f1 100644
--- a/crates/typst-layout/src/grid/lines.rs
+++ b/crates/typst-layout/src/grid/lines.rs
@@ -1,41 +1,11 @@
-use std::num::NonZeroUsize;
use std::sync::Arc;
use typst_library::foundations::{AlternativeFold, Fold};
+use typst_library::layout::grid::resolve::{CellGrid, Line, Repeatable};
use typst_library::layout::Abs;
use typst_library::visualize::Stroke;
-use super::{CellGrid, LinePosition, Repeatable, RowPiece};
-
-/// Represents an explicit grid line (horizontal or vertical) specified by the
-/// user.
-pub struct Line {
- /// The index of the track after this line. This will be the index of the
- /// row a horizontal line is above of, or of the column right after a
- /// vertical line.
- ///
- /// Must be within `0..=tracks.len()` (where `tracks` is either `grid.cols`
- /// or `grid.rows`, ignoring gutter tracks, as appropriate).
- pub index: usize,
- /// The index of the track at which this line starts being drawn.
- /// This is the first column a horizontal line appears in, or the first row
- /// a vertical line appears in.
- ///
- /// Must be within `0..tracks.len()` minus gutter tracks.
- pub start: usize,
- /// The index after the last track through which the line is drawn.
- /// Thus, the line is drawn through tracks `start..end` (note that `end` is
- /// exclusive).
- ///
- /// Must be within `1..=tracks.len()` minus gutter tracks.
- /// `None` indicates the line should go all the way to the end.
- pub end: Option,
- /// The line's stroke. This is `None` when the line is explicitly used to
- /// override a previously specified line.
- pub stroke: Option>>,
- /// The line's position in relation to the track with its index.
- pub position: LinePosition,
-}
+use super::RowPiece;
/// Indicates which priority a particular grid line segment should have, based
/// on the highest priority configuration that defined the segment's stroke.
@@ -493,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 };
@@ -588,13 +558,13 @@ pub fn hline_stroke_at_column(
#[cfg(test)]
mod test {
+ use std::num::NonZeroUsize;
use typst_library::foundations::Content;
use typst_library::introspection::Locator;
+ use typst_library::layout::grid::resolve::{Cell, Entry, LinePosition};
use typst_library::layout::{Axes, Sides, Sizing};
use typst_utils::NonZeroExt;
- use super::super::cells::Entry;
- use super::super::Cell;
use super::*;
fn sample_cell() -> Cell<'static> {
diff --git a/crates/typst-layout/src/grid/mod.rs b/crates/typst-layout/src/grid/mod.rs
index 769bef8c5..1b4380f0a 100644
--- a/crates/typst-layout/src/grid/mod.rs
+++ b/crates/typst-layout/src/grid/mod.rs
@@ -1,40 +1,44 @@
-mod cells;
mod layouter;
mod lines;
mod repeated;
mod rowspans;
-pub use self::cells::{Cell, CellGrid};
pub use self::layouter::GridLayouter;
-use std::num::NonZeroUsize;
-use std::sync::Arc;
-
-use ecow::eco_format;
-use typst_library::diag::{SourceResult, Trace, Tracepoint};
+use typst_library::diag::SourceResult;
use typst_library::engine::Engine;
-use typst_library::foundations::{Fold, Packed, Smart, StyleChain};
+use typst_library::foundations::{Packed, StyleChain};
use typst_library::introspection::Locator;
-use typst_library::layout::{
- Abs, Alignment, Axes, Dir, Fragment, GridCell, GridChild, GridElem, GridItem, Length,
- OuterHAlignment, OuterVAlignment, Regions, Rel, Sides,
-};
-use typst_library::model::{TableCell, TableChild, TableElem, TableItem};
-use typst_library::text::TextElem;
-use typst_library::visualize::{Paint, Stroke};
-use typst_syntax::Span;
+use typst_library::layout::grid::resolve::{grid_to_cellgrid, table_to_cellgrid, Cell};
+use typst_library::layout::{Fragment, GridElem, Regions};
+use typst_library::model::TableElem;
-use self::cells::{
- LinePosition, ResolvableCell, ResolvableGridChild, ResolvableGridItem,
-};
use self::layouter::RowPiece;
use self::lines::{
- generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Line,
- LineSegment,
+ generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, LineSegment,
};
-use self::repeated::{Footer, Header, Repeatable};
use self::rowspans::{Rowspan, UnbreakableRowGroup};
+/// Layout the cell into the given regions.
+///
+/// The `disambiguator` indicates which instance of this cell this should be
+/// layouted as. For normal cells, it is always `0`, but for headers and
+/// footers, it indicates the index of the header/footer among all. See the
+/// [`Locator`] docs for more details on the concepts behind this.
+pub fn layout_cell(
+ cell: &Cell,
+ engine: &mut Engine,
+ disambiguator: usize,
+ styles: StyleChain,
+ regions: Regions,
+) -> SourceResult {
+ let mut locator = cell.locator.relayout();
+ if disambiguator > 0 {
+ locator = locator.split().next_inner(disambiguator as u128);
+ }
+ crate::layout_fragment(engine, &cell.body, locator, styles, regions)
+}
+
/// Layout the grid.
#[typst_macros::time(span = elem.span())]
pub fn layout_grid(
@@ -44,54 +48,8 @@ pub fn layout_grid(
styles: StyleChain,
regions: Regions,
) -> SourceResult {
- let inset = elem.inset(styles);
- let align = elem.align(styles);
- let columns = elem.columns(styles);
- let rows = elem.rows(styles);
- let column_gutter = elem.column_gutter(styles);
- let row_gutter = elem.row_gutter(styles);
- let fill = elem.fill(styles);
- let stroke = elem.stroke(styles);
-
- let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
- let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
- // Use trace to link back to the grid when a specific cell errors
- let tracepoint = || Tracepoint::Call(Some(eco_format!("grid")));
- let resolve_item = |item: &GridItem| grid_item_to_resolvable(item, styles);
- let children = elem.children().iter().map(|child| match child {
- GridChild::Header(header) => ResolvableGridChild::Header {
- repeat: header.repeat(styles),
- span: header.span(),
- items: header.children().iter().map(resolve_item),
- },
- GridChild::Footer(footer) => ResolvableGridChild::Footer {
- repeat: footer.repeat(styles),
- span: footer.span(),
- items: footer.children().iter().map(resolve_item),
- },
- GridChild::Item(item) => {
- ResolvableGridChild::Item(grid_item_to_resolvable(item, styles))
- }
- });
- let grid = CellGrid::resolve(
- tracks,
- gutter,
- locator,
- children,
- fill,
- align,
- &inset,
- &stroke,
- engine,
- styles,
- elem.span(),
- )
- .trace(engine.world, tracepoint, elem.span())?;
-
- let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
-
- // Measure the columns and layout the grid row-by-row.
- layouter.layout(engine)
+ let grid = grid_to_cellgrid(elem, engine, locator, styles)?;
+ GridLayouter::new(&grid, regions, styles, elem.span()).layout(engine)
}
/// Layout the table.
@@ -103,314 +61,6 @@ pub fn layout_table(
styles: StyleChain,
regions: Regions,
) -> SourceResult {
- let inset = elem.inset(styles);
- let align = elem.align(styles);
- let columns = elem.columns(styles);
- let rows = elem.rows(styles);
- let column_gutter = elem.column_gutter(styles);
- let row_gutter = elem.row_gutter(styles);
- let fill = elem.fill(styles);
- let stroke = elem.stroke(styles);
-
- let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
- let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
- // Use trace to link back to the table when a specific cell errors
- let tracepoint = || Tracepoint::Call(Some(eco_format!("table")));
- let resolve_item = |item: &TableItem| table_item_to_resolvable(item, styles);
- let children = elem.children().iter().map(|child| match child {
- TableChild::Header(header) => ResolvableGridChild::Header {
- repeat: header.repeat(styles),
- span: header.span(),
- items: header.children().iter().map(resolve_item),
- },
- TableChild::Footer(footer) => ResolvableGridChild::Footer {
- repeat: footer.repeat(styles),
- span: footer.span(),
- items: footer.children().iter().map(resolve_item),
- },
- TableChild::Item(item) => {
- ResolvableGridChild::Item(table_item_to_resolvable(item, styles))
- }
- });
- let grid = CellGrid::resolve(
- tracks,
- gutter,
- locator,
- children,
- fill,
- align,
- &inset,
- &stroke,
- engine,
- styles,
- elem.span(),
- )
- .trace(engine.world, tracepoint, elem.span())?;
-
- let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
- layouter.layout(engine)
-}
-
-fn grid_item_to_resolvable(
- item: &GridItem,
- styles: StyleChain,
-) -> ResolvableGridItem> {
- match item {
- GridItem::HLine(hline) => ResolvableGridItem::HLine {
- y: hline.y(styles),
- start: hline.start(styles),
- end: hline.end(styles),
- stroke: hline.stroke(styles),
- span: hline.span(),
- position: match hline.position(styles) {
- OuterVAlignment::Top => LinePosition::Before,
- OuterVAlignment::Bottom => LinePosition::After,
- },
- },
- GridItem::VLine(vline) => ResolvableGridItem::VLine {
- x: vline.x(styles),
- start: vline.start(styles),
- end: vline.end(styles),
- stroke: vline.stroke(styles),
- span: vline.span(),
- position: match vline.position(styles) {
- OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => {
- LinePosition::After
- }
- OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => {
- LinePosition::Before
- }
- OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before,
- OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After,
- },
- },
- GridItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()),
- }
-}
-
-fn table_item_to_resolvable(
- item: &TableItem,
- styles: StyleChain,
-) -> ResolvableGridItem> {
- match item {
- TableItem::HLine(hline) => ResolvableGridItem::HLine {
- y: hline.y(styles),
- start: hline.start(styles),
- end: hline.end(styles),
- stroke: hline.stroke(styles),
- span: hline.span(),
- position: match hline.position(styles) {
- OuterVAlignment::Top => LinePosition::Before,
- OuterVAlignment::Bottom => LinePosition::After,
- },
- },
- TableItem::VLine(vline) => ResolvableGridItem::VLine {
- x: vline.x(styles),
- start: vline.start(styles),
- end: vline.end(styles),
- stroke: vline.stroke(styles),
- span: vline.span(),
- position: match vline.position(styles) {
- OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => {
- LinePosition::After
- }
- OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => {
- LinePosition::Before
- }
- OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before,
- OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After,
- },
- },
- TableItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()),
- }
-}
-
-impl ResolvableCell for Packed {
- fn resolve_cell<'a>(
- mut self,
- x: usize,
- y: usize,
- fill: &Option,
- align: Smart,
- inset: Sides>>,
- stroke: Sides >>>>,
- breakable: bool,
- locator: Locator<'a>,
- styles: StyleChain,
- ) -> Cell<'a> {
- let cell = &mut *self;
- let colspan = cell.colspan(styles);
- let rowspan = cell.rowspan(styles);
- let breakable = cell.breakable(styles).unwrap_or(breakable);
- let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
-
- let cell_stroke = cell.stroke(styles);
- let stroke_overridden =
- cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_))));
-
- // Using a typical 'Sides' fold, an unspecified side loses to a
- // specified side. Additionally, when both are specified, an inner
- // None wins over the outer Some, and vice-versa. When both are
- // specified and Some, fold occurs, which, remarkably, leads to an Arc
- // clone.
- //
- // In the end, we flatten because, for layout purposes, an unspecified
- // cell stroke is the same as specifying 'none', so we equate the two
- // concepts.
- let stroke = cell_stroke.fold(stroke).map(Option::flatten);
- cell.push_x(Smart::Custom(x));
- cell.push_y(Smart::Custom(y));
- cell.push_fill(Smart::Custom(fill.clone()));
- cell.push_align(match align {
- Smart::Custom(align) => {
- Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align)))
- }
- // Don't fold if the table is using outer alignment. Use the
- // cell's alignment instead (which, in the end, will fold with
- // the outer alignment when it is effectively displayed).
- Smart::Auto => cell.align(styles),
- });
- cell.push_inset(Smart::Custom(
- cell.inset(styles).map_or(inset, |inner| inner.fold(inset)),
- ));
- cell.push_stroke(
- // Here we convert the resolved stroke to a regular stroke, however
- // with resolved units (that is, 'em' converted to absolute units).
- // We also convert any stroke unspecified by both the cell and the
- // outer stroke ('None' in the folded stroke) to 'none', that is,
- // all sides are present in the resulting Sides object accessible
- // by show rules on table cells.
- stroke.as_ref().map(|side| {
- Some(side.as_ref().map(|cell_stroke| {
- Arc::new((**cell_stroke).clone().map(Length::from))
- }))
- }),
- );
- cell.push_breakable(Smart::Custom(breakable));
- Cell {
- body: self.pack(),
- locator,
- fill,
- colspan,
- rowspan,
- stroke,
- stroke_overridden,
- breakable,
- }
- }
-
- fn x(&self, styles: StyleChain) -> Smart {
- (**self).x(styles)
- }
-
- fn y(&self, styles: StyleChain) -> Smart {
- (**self).y(styles)
- }
-
- fn colspan(&self, styles: StyleChain) -> NonZeroUsize {
- (**self).colspan(styles)
- }
-
- fn rowspan(&self, styles: StyleChain) -> NonZeroUsize {
- (**self).rowspan(styles)
- }
-
- fn span(&self) -> Span {
- Packed::span(self)
- }
-}
-
-impl ResolvableCell for Packed {
- fn resolve_cell<'a>(
- mut self,
- x: usize,
- y: usize,
- fill: &Option,
- align: Smart,
- inset: Sides>>,
- stroke: Sides >>>>,
- breakable: bool,
- locator: Locator<'a>,
- styles: StyleChain,
- ) -> Cell<'a> {
- let cell = &mut *self;
- let colspan = cell.colspan(styles);
- let rowspan = cell.rowspan(styles);
- let breakable = cell.breakable(styles).unwrap_or(breakable);
- let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
-
- let cell_stroke = cell.stroke(styles);
- let stroke_overridden =
- cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_))));
-
- // Using a typical 'Sides' fold, an unspecified side loses to a
- // specified side. Additionally, when both are specified, an inner
- // None wins over the outer Some, and vice-versa. When both are
- // specified and Some, fold occurs, which, remarkably, leads to an Arc
- // clone.
- //
- // In the end, we flatten because, for layout purposes, an unspecified
- // cell stroke is the same as specifying 'none', so we equate the two
- // concepts.
- let stroke = cell_stroke.fold(stroke).map(Option::flatten);
- cell.push_x(Smart::Custom(x));
- cell.push_y(Smart::Custom(y));
- cell.push_fill(Smart::Custom(fill.clone()));
- cell.push_align(match align {
- Smart::Custom(align) => {
- Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align)))
- }
- // Don't fold if the grid is using outer alignment. Use the
- // cell's alignment instead (which, in the end, will fold with
- // the outer alignment when it is effectively displayed).
- Smart::Auto => cell.align(styles),
- });
- cell.push_inset(Smart::Custom(
- cell.inset(styles).map_or(inset, |inner| inner.fold(inset)),
- ));
- cell.push_stroke(
- // Here we convert the resolved stroke to a regular stroke, however
- // with resolved units (that is, 'em' converted to absolute units).
- // We also convert any stroke unspecified by both the cell and the
- // outer stroke ('None' in the folded stroke) to 'none', that is,
- // all sides are present in the resulting Sides object accessible
- // by show rules on grid cells.
- stroke.as_ref().map(|side| {
- Some(side.as_ref().map(|cell_stroke| {
- Arc::new((**cell_stroke).clone().map(Length::from))
- }))
- }),
- );
- cell.push_breakable(Smart::Custom(breakable));
- Cell {
- body: self.pack(),
- locator,
- fill,
- colspan,
- rowspan,
- stroke,
- stroke_overridden,
- breakable,
- }
- }
-
- fn x(&self, styles: StyleChain) -> Smart {
- (**self).x(styles)
- }
-
- fn y(&self, styles: StyleChain) -> Smart {
- (**self).y(styles)
- }
-
- fn colspan(&self, styles: StyleChain) -> NonZeroUsize {
- (**self).colspan(styles)
- }
-
- fn rowspan(&self, styles: StyleChain) -> NonZeroUsize {
- (**self).rowspan(styles)
- }
-
- fn span(&self) -> Span {
- Packed::span(self)
- }
+ let grid = table_to_cellgrid(elem, engine, locator, styles)?;
+ GridLayouter::new(&grid, regions, styles, elem.span()).layout(engine)
}
diff --git a/crates/typst-layout/src/grid/repeated.rs b/crates/typst-layout/src/grid/repeated.rs
index 8d08d56db..22d2a09ef 100644
--- a/crates/typst-layout/src/grid/repeated.rs
+++ b/crates/typst-layout/src/grid/repeated.rs
@@ -1,50 +1,11 @@
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::rowspans::UnbreakableRowGroup;
-/// A repeatable grid header. Starts at the first row.
-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.
-pub struct Footer {
- /// The first row included in this footer.
- pub start: usize,
-}
-
-/// A possibly repeatable grid object.
-/// It still exists even when not repeatable, but must not have additional
-/// considerations by grid layout, other than for consistency (such as making
-/// a certain group of rows unbreakable).
-pub enum Repeatable {
- Repeated(T),
- NotRepeated(T),
-}
-
-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.
- pub fn as_repeated(&self) -> Option<&T> {
- match self {
- Self::Repeated(repeated) => Some(repeated),
- Self::NotRepeated(_) => None,
- }
- }
-}
-
impl GridLayouter<'_> {
/// Layouts the header's rows.
/// Skips regions as necessary.
diff --git a/crates/typst-layout/src/grid/rowspans.rs b/crates/typst-layout/src/grid/rowspans.rs
index 93d4c960d..21992ed02 100644
--- a/crates/typst-layout/src/grid/rowspans.rs
+++ b/crates/typst-layout/src/grid/rowspans.rs
@@ -1,12 +1,12 @@
use typst_library::diag::SourceResult;
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::repeated::Repeatable;
-use super::{Cell, GridLayouter};
+use super::{layout_cell, Cell, GridLayouter};
/// All information needed to layout a single rowspan.
pub struct Rowspan {
@@ -141,7 +141,7 @@ impl GridLayouter<'_> {
}
// Push the layouted frames directly into the finished frames.
- let fragment = cell.layout(engine, disambiguator, self.styles, pod)?;
+ 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
.finished
@@ -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/image.rs b/crates/typst-layout/src/image.rs
index 77e1d0838..3e5b7d8bd 100644
--- a/crates/typst-layout/src/image.rs
+++ b/crates/typst-layout/src/image.rs
@@ -1,16 +1,17 @@
use std::ffi::OsStr;
-use typst_library::diag::{bail, warning, At, SourceResult, StrResult};
+use typst_library::diag::{warning, At, SourceResult, StrResult};
use typst_library::engine::Engine;
-use typst_library::foundations::{Packed, Smart, StyleChain};
+use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain};
use typst_library::introspection::Locator;
use typst_library::layout::{
Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size,
};
-use typst_library::loading::Readable;
+use typst_library::loading::DataSource;
use typst_library::text::families;
use typst_library::visualize::{
- Curve, Image, ImageElem, ImageFit, ImageFormat, RasterFormat, VectorFormat,
+ Curve, ExchangeFormat, Image, ImageElem, ImageFit, ImageFormat, ImageKind,
+ RasterImage, SvgImage, VectorFormat,
};
/// Layout the image.
@@ -26,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 data = elem.data();
+ let Derived { source, derived: data } = &elem.source;
let format = match elem.format(styles) {
Smart::Custom(v) => v,
- Smart::Auto => determine_format(elem.path().as_str(), data).at(span)?,
+ Smart::Auto => determine_format(source, data).at(span)?,
};
// Warn the user if the image contains a foreign object. Not perfect
// because the svg could also be encoded, but that's an edge case.
if format == ImageFormat::Vector(VectorFormat::Svg) {
let has_foreign_object =
- data.as_str().is_some_and(|s| s.contains(">(),
- elem.flatten_text(styles),
- )
- .at(span)?;
+ let kind = match format {
+ ImageFormat::Raster(format) => ImageKind::Raster(
+ RasterImage::new(
+ data.clone(),
+ format,
+ elem.icc(styles).as_ref().map(|icc| icc.derived.clone()),
+ )
+ .at(span)?,
+ ),
+ ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg(
+ SvgImage::with_fonts(
+ data.clone(),
+ engine.world,
+ &families(styles).map(|f| f.as_str()).collect::>(),
+ )
+ .at(span)?,
+ ),
+ };
+
+ let image = Image::new(kind, elem.alt(styles), elem.scaling(styles));
// Determine the image's pixel aspect ratio.
let pxw = image.width();
@@ -83,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(
@@ -119,25 +133,23 @@ pub fn layout_image(
Ok(frame)
}
-/// Determine the image format based on path and data.
-fn determine_format(path: &str, data: &Readable) -> StrResult {
- let ext = std::path::Path::new(path)
- .extension()
- .and_then(OsStr::to_str)
- .unwrap_or_default()
- .to_lowercase();
+/// Try to determine the image format based on the data.
+fn determine_format(source: &DataSource, data: &Bytes) -> StrResult {
+ if let DataSource::Path(path) = source {
+ let ext = std::path::Path::new(path.as_str())
+ .extension()
+ .and_then(OsStr::to_str)
+ .unwrap_or_default()
+ .to_lowercase();
- Ok(match ext.as_str() {
- "png" => ImageFormat::Raster(RasterFormat::Png),
- "jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg),
- "gif" => ImageFormat::Raster(RasterFormat::Gif),
- "svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
- _ => match &data {
- Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg),
- Readable::Bytes(bytes) => match RasterFormat::detect(bytes) {
- Some(f) => ImageFormat::Raster(f),
- None => bail!("unknown image format"),
- },
- },
- })
+ match ext.as_str() {
+ "png" => return Ok(ExchangeFormat::Png.into()),
+ "jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()),
+ "gif" => return Ok(ExchangeFormat::Gif.into()),
+ "svg" | "svgz" => return Ok(VectorFormat::Svg.into()),
+ _ => {}
+ }
+ }
+
+ Ok(ImageFormat::detect(data).ok_or("unknown image format")?)
}
diff --git a/crates/typst-layout/src/inline/box.rs b/crates/typst-layout/src/inline/box.rs
index 6dfbc9696..e21928d3c 100644
--- a/crates/typst-layout/src/inline/box.rs
+++ b/crates/typst-layout/src/inline/box.rs
@@ -11,7 +11,7 @@ use typst_utils::Numeric;
use crate::flow::unbreakable_pod;
use crate::shapes::{clip_rect, fill_and_stroke};
-/// Lay out a box as part of a paragraph.
+/// Lay out a box as part of inline layout.
#[typst_macros::time(name = "box", span = elem.span())]
pub fn layout_box(
elem: &Packed,
diff --git a/crates/typst-layout/src/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs
index 23e82c417..5a1b7b4fc 100644
--- a/crates/typst-layout/src/inline/collect.rs
+++ b/crates/typst-layout/src/inline/collect.rs
@@ -1,10 +1,10 @@
-use typst_library::diag::bail;
+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::routines::Pair;
use typst_library::text::{
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
SpaceElem, TextElem,
@@ -13,9 +13,10 @@ use typst_syntax::Span;
use typst_utils::Numeric;
use super::*;
+use crate::modifiers::{layout_and_modify, FrameModifiers, FrameModify};
// The characters by which spacing, inline content and pins are replaced in the
-// paragraph's full text.
+// full text.
const SPACING_REPLACE: &str = " "; // Space
const OBJ_REPLACE: &str = "\u{FFFC}"; // Object Replacement Character
@@ -26,7 +27,7 @@ const POP_EMBEDDING: &str = "\u{202C}";
const LTR_ISOLATE: &str = "\u{2066}";
const POP_ISOLATE: &str = "\u{2069}";
-/// A prepared item in a paragraph layout.
+/// A prepared item in a inline layout.
#[derive(Debug)]
pub enum Item<'a> {
/// A shaped text run with consistent style and direction.
@@ -36,7 +37,7 @@ pub enum Item<'a> {
/// Fractional spacing between other items.
Fractional(Fr, Option<(&'a Packed, Locator<'a>, StyleChain<'a>)>),
/// Layouted inline-level content.
- Frame(Frame, StyleChain<'a>),
+ Frame(Frame),
/// A tag.
Tag(&'a Tag),
/// An item that is invisible and needs to be skipped, e.g. a Unicode
@@ -67,7 +68,7 @@ impl<'a> Item<'a> {
match self {
Self::Text(shaped) => shaped.text,
Self::Absolute(_, _) | Self::Fractional(_, _) => SPACING_REPLACE,
- Self::Frame(_, _) => OBJ_REPLACE,
+ Self::Frame(_) => OBJ_REPLACE,
Self::Tag(_) => "",
Self::Skip(s) => s,
}
@@ -83,7 +84,7 @@ impl<'a> Item<'a> {
match self {
Self::Text(shaped) => shaped.width,
Self::Absolute(v, _) => *v,
- Self::Frame(frame, _) => frame.width(),
+ Self::Frame(frame) => frame.width(),
Self::Fractional(_, _) | Self::Tag(_) => Abs::zero(),
Self::Skip(_) => Abs::zero(),
}
@@ -112,38 +113,31 @@ impl Segment<'_> {
}
}
-/// Collects all text of the paragraph into one string and a collection of
-/// segments that correspond to pieces of that string. This also performs
-/// string-level preprocessing like case transformations.
+/// Collects all text into one string and a collection of segments that
+/// correspond to pieces of that string. This also performs string-level
+/// preprocessing like case transformations.
#[typst_macros::time]
pub fn collect<'a>(
- children: &'a StyleVec,
+ children: &[Pair<'a>],
engine: &mut Engine<'_>,
locator: &mut SplitLocator<'a>,
- styles: &'a StyleChain<'a>,
+ config: &Config,
region: Size,
- consecutive: bool,
) -> SourceResult<(String, Vec>, SpanMapper)> {
let mut collector = Collector::new(2 + children.len());
let mut quoter = SmartQuoter::new();
- let outer_dir = TextElem::dir_in(*styles);
- let first_line_indent = ParElem::first_line_indent_in(*styles);
- if !first_line_indent.is_zero()
- && consecutive
- && AlignElem::alignment_in(*styles).resolve(*styles).x == outer_dir.start().into()
- {
- collector.push_item(Item::Absolute(first_line_indent.resolve(*styles), false));
+ if !config.first_line_indent.is_zero() {
+ collector.push_item(Item::Absolute(config.first_line_indent, 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));
+ 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.iter(styles) {
+ for &(child, styles) in children {
let prev_len = collector.full.len();
if child.is::() {
@@ -151,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),
@@ -161,24 +155,23 @@ pub fn collect<'a>(
}
if let Some(case) = TextElem::case_in(styles) {
- full.push_str(&case.apply(elem.text()));
+ full.push_str(&case.apply(&elem.text));
} else {
- full.push_str(elem.text());
+ full.push_str(&elem.text);
}
- if dir != outer_dir {
+ if dir != config.dir {
// Insert "Pop Directional Formatting".
full.push_str(POP_EMBEDDING);
}
});
} else if let Some(elem) = child.to_packed::() {
- let amount = elem.amount();
- if amount.is_zero() {
+ if elem.amount.is_zero() {
continue;
}
- collector.push_item(match amount {
- Spacing::Fr(fr) => Item::Fractional(*fr, None),
+ collector.push_item(match elem.amount {
+ Spacing::Fr(fr) => Item::Fractional(fr, None),
Spacing::Rel(rel) => Item::Absolute(
rel.resolve(styles).relative_to(region.x),
elem.weak(styles),
@@ -211,8 +204,10 @@ pub fn collect<'a>(
InlineItem::Space(space, weak) => {
collector.push_item(Item::Absolute(space, weak));
}
- InlineItem::Frame(frame) => {
- collector.push_item(Item::Frame(frame, styles));
+ InlineItem::Frame(mut frame) => {
+ frame.modify(&FrameModifiers::get_in(styles));
+ apply_baseline_shift(&mut frame, styles);
+ collector.push_item(Item::Frame(frame));
}
}
}
@@ -223,13 +218,22 @@ pub fn collect<'a>(
if let Sizing::Fr(v) = elem.width(styles) {
collector.push_item(Item::Fractional(v, Some((elem, loc, styles))));
} else {
- let frame = layout_box(elem, engine, loc, styles, region)?;
- collector.push_item(Item::Frame(frame, styles));
+ let mut frame = layout_and_modify(styles, |styles| {
+ layout_box(elem, engine, loc, styles, region)
+ })?;
+ apply_baseline_shift(&mut frame, styles);
+ collector.push_item(Item::Frame(frame));
}
} else if let Some(elem) = child.to_packed::() {
collector.push_item(Item::Tag(&elem.tag));
} else {
- bail!(child.span(), "unexpected paragraph child");
+ // Non-paragraph inline layout should never trigger this since it
+ // only won't be triggered if we see any non-inline content.
+ engine.sink.warn(warning!(
+ child.span(),
+ "{} may not occur inside of a paragraph and was ignored",
+ child.func().name()
+ ));
};
let len = collector.full.len() - prev_len;
diff --git a/crates/typst-layout/src/inline/finalize.rs b/crates/typst-layout/src/inline/finalize.rs
index 57044f0ec..c9de0085e 100644
--- a/crates/typst-layout/src/inline/finalize.rs
+++ b/crates/typst-layout/src/inline/finalize.rs
@@ -9,19 +9,19 @@ pub fn finalize(
engine: &mut Engine,
p: &Preparation,
lines: &[Line],
- styles: StyleChain,
region: Size,
expand: bool,
locator: &mut SplitLocator<'_>,
) -> SourceResult {
- // Determine the paragraph's width: Full width of the region if we should
+ // Determine the resulting width: Full width of the region if we should
// expand or there's fractional spacing, fit-to-width otherwise.
let width = if !region.x.is_finite()
|| (!expand && lines.iter().all(|line| line.fr().is_zero()))
{
- 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 ef7e26c3c..659d33f4a 100644
--- a/crates/typst-layout/src/inline/line.rs
+++ b/crates/typst-layout/src/inline/line.rs
@@ -2,14 +2,14 @@ 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;
use super::*;
+use crate::modifiers::layout_and_modify;
const SHY: char = '\u{ad}';
const HYPHEN: char = '-';
@@ -17,12 +17,12 @@ const EN_DASH: char = '–';
const EM_DASH: char = '—';
const LINE_SEPARATOR: char = '\u{2028}'; // We use LS to distinguish justified breaks.
-/// A layouted line, consisting of a sequence of layouted paragraph items that
-/// are mostly borrowed from the preparation phase. This type enables you to
-/// measure the size of a line in a range before committing to building the
-/// line's frame.
+/// A layouted line, consisting of a sequence of layouted inline items that are
+/// mostly borrowed from the preparation phase. This type enables you to measure
+/// the size of a line in a range before committing to building the line's
+/// frame.
///
-/// At most two paragraph items must be created individually for this line: The
+/// At most two inline items must be created individually for this line: The
/// first and last one since they may be broken apart by the start or end of the
/// line, respectively. But even those can partially reuse previous results when
/// the break index is safe-to-break per rustybuzz.
@@ -93,7 +93,7 @@ impl Line<'_> {
pub fn has_negative_width_items(&self) -> bool {
self.items.iter().any(|item| match item {
Item::Absolute(amount, _) => *amount < Abs::zero(),
- Item::Frame(frame, _) => frame.width() < Abs::zero(),
+ Item::Frame(frame) => frame.width() < Abs::zero(),
_ => false,
})
}
@@ -134,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) {
@@ -154,16 +154,16 @@ 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.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);
}
}
@@ -233,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;
}
@@ -307,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);
}
@@ -331,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();
@@ -358,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()
{
@@ -403,12 +406,17 @@ 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,
}
}
+/// Apply the current baseline shift to a frame.
+pub fn apply_baseline_shift(frame: &mut Frame, styles: StyleChain) {
+ frame.translate(Point::with_y(TextElem::baseline_in(styles)));
+}
+
/// Commit to a line and build its frame.
#[allow(clippy::too_many_arguments)]
pub fn commit(
@@ -418,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. When the paragraph is RTL, the
+ // 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.
@@ -509,10 +516,11 @@ pub fn commit(
let amount = v.share(fr, remaining);
if let Some((elem, loc, styles)) = elem {
let region = Size::new(amount, full);
- let mut frame =
- layout_box(elem, engine, loc.relayout(), *styles, region)?;
- frame.translate(Point::with_y(TextElem::baseline_in(*styles)));
- push(&mut offset, frame.post_processed(*styles));
+ let mut frame = layout_and_modify(*styles, |styles| {
+ layout_box(elem, engine, loc.relayout(), styles, region)
+ })?;
+ apply_baseline_shift(&mut frame, *styles);
+ push(&mut offset, frame);
} else {
offset += amount;
}
@@ -524,12 +532,10 @@ pub fn commit(
justification_ratio,
extra_justification,
);
- push(&mut offset, frame.post_processed(shaped.styles));
+ push(&mut offset, frame);
}
- Item::Frame(frame, styles) => {
- let mut frame = frame.clone();
- frame.translate(Point::with_y(TextElem::baseline_in(*styles)));
- push(&mut offset, frame.post_processed(*styles));
+ Item::Frame(frame) => {
+ push(&mut offset, frame.clone());
}
Item::Tag(tag) => {
let mut frame = Frame::soft(Size::zero());
@@ -549,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);
}
@@ -570,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);
@@ -601,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)));
}
@@ -626,7 +626,7 @@ fn overhang(c: char) -> f64 {
}
}
-/// A collection of owned or borrowed paragraph items.
+/// A collection of owned or borrowed inline items.
pub struct Items<'a>(Vec>);
impl<'a> Items<'a> {
diff --git a/crates/typst-layout/src/inline/linebreak.rs b/crates/typst-layout/src/inline/linebreak.rs
index 7b66fcdb4..31512604f 100644
--- a/crates/typst-layout/src/inline/linebreak.rs
+++ b/crates/typst-layout/src/inline/linebreak.rs
@@ -17,7 +17,7 @@ use unicode_segmentation::UnicodeSegmentation;
use super::*;
-/// The cost of a line or paragraph layout.
+/// The cost of a line or inline layout.
type Cost = f64;
// Cost parameters.
@@ -104,21 +104,13 @@ impl Breakpoint {
}
}
-/// Breaks the paragraph into lines.
+/// Breaks the text into lines.
pub fn linebreak<'a>(
engine: &Engine,
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),
}
@@ -181,13 +173,12 @@ fn linebreak_simple<'a>(
/// lines with hyphens even more.
///
/// To find the layout with the minimal total cost the algorithm uses dynamic
-/// programming: For each possible breakpoint it determines the optimal
-/// paragraph layout _up to that point_. It walks over all possible start points
-/// for a line ending at that point and finds the one for which the cost of the
-/// line plus the cost of the optimal paragraph up to the start point (already
-/// computed and stored in dynamic programming table) is minimal. The final
-/// result is simply the layout determined for the last breakpoint at the end of
-/// text.
+/// programming: For each possible breakpoint, it determines the optimal layout
+/// _up to that point_. It walks over all possible start points for a line
+/// ending at that point and finds the one for which the cost of the line plus
+/// the cost of the optimal layout up to the start point (already computed and
+/// stored in dynamic programming table) is minimal. The final result is simply
+/// the layout determined for the last breakpoint at the end of text.
#[typst_macros::time]
fn linebreak_optimized<'a>(
engine: &Engine,
@@ -215,7 +206,7 @@ fn linebreak_optimized_bounded<'a>(
metrics: &CostMetrics,
upper_bound: Cost,
) -> Vec> {
- /// An entry in the dynamic programming table for paragraph optimization.
+ /// An entry in the dynamic programming table for inline layout optimization.
struct Entry<'a> {
pred: usize,
total: Cost,
@@ -299,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 });
}
}
@@ -321,7 +312,7 @@ fn linebreak_optimized_bounded<'a>(
// This should only happen if our bound was faulty. Which shouldn't happen!
if table[idx].end != p.text.len() {
#[cfg(debug_assertions)]
- panic!("bounded paragraph layout is incomplete");
+ panic!("bounded inline layout is incomplete");
#[cfg(not(debug_assertions))]
return linebreak_optimized_bounded(engine, p, width, metrics, Cost::INFINITY);
@@ -342,7 +333,7 @@ fn linebreak_optimized_bounded<'a>(
/// (which is costly) to determine costs, it determines approximate costs using
/// cumulative arrays.
///
-/// This results in a likely good paragraph layouts, for which we then compute
+/// This results in a likely good inline layouts, for which we then compute
/// the exact cost. This cost is an upper bound for proper optimized
/// linebreaking. We can use it to heavily prune the search space.
#[typst_macros::time]
@@ -355,7 +346,7 @@ fn linebreak_optimized_approximate(
// Determine the cumulative estimation metrics.
let estimates = Estimates::compute(p);
- /// An entry in the dynamic programming table for paragraph optimization.
+ /// An entry in the dynamic programming table for inline layout optimization.
struct Entry {
pred: usize,
total: Cost,
@@ -385,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
@@ -432,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,
@@ -574,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
@@ -664,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,
};
@@ -831,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))
@@ -862,17 +853,17 @@ struct CostMetrics {
}
impl CostMetrics {
- /// Compute shared metrics for paragraph optimization.
+ /// Compute shared metrics for inline layout optimization.
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 658e30846..5ef820d07 100644
--- a/crates/typst-layout/src/inline/mod.rs
+++ b/crates/typst-layout/src/inline/mod.rs
@@ -13,17 +13,22 @@ pub use self::box_::layout_box;
use comemo::{Track, Tracked, TrackedMut};
use typst_library::diag::SourceResult;
use typst_library::engine::{Engine, Route, Sink, Traced};
-use typst_library::foundations::{StyleChain, StyleVec};
-use typst_library::introspection::{Introspector, Locator, LocatorLink};
-use typst_library::layout::{Fragment, Size};
-use typst_library::model::ParElem;
-use typst_library::routines::Routines;
+use typst_library::foundations::{Packed, Resolve, Smart, StyleChain};
+use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator};
+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;
use self::finalize::finalize;
-use self::line::{commit, line, Line};
+use self::line::{apply_baseline_shift, commit, line, Line};
use self::linebreak::{linebreak, Breakpoint};
use self::prepare::{prepare, Preparation};
use self::shaping::{
@@ -34,18 +39,18 @@ use self::shaping::{
/// Range of a substring of text.
type Range = std::ops::Range;
-/// Layouts content inline.
-pub fn layout_inline(
+/// Layouts the paragraph.
+pub fn layout_par(
+ elem: &Packed,
engine: &mut Engine,
- children: &StyleVec,
locator: Locator,
styles: StyleChain,
- consecutive: bool,
region: Size,
expand: bool,
+ situation: ParSituation,
) -> SourceResult {
- layout_inline_impl(
- children,
+ layout_par_impl(
+ elem,
engine.routines,
engine.world,
engine.introspector,
@@ -54,17 +59,17 @@ pub fn layout_inline(
engine.route.track(),
locator.track(),
styles,
- consecutive,
region,
expand,
+ situation,
)
}
-/// The internal, memoized implementation of `layout_inline`.
+/// The internal, memoized implementation of `layout_par`.
#[comemo::memoize]
#[allow(clippy::too_many_arguments)]
-fn layout_inline_impl(
- children: &StyleVec,
+fn layout_par_impl(
+ elem: &Packed,
routines: &Routines,
world: Tracked,
introspector: Tracked,
@@ -73,12 +78,12 @@ fn layout_inline_impl(
route: Tracked,
locator: Tracked,
styles: StyleChain,
- consecutive: bool,
region: Size,
expand: bool,
+ situation: ParSituation,
) -> SourceResult {
let link = LocatorLink::new(locator);
- let locator = Locator::link(&link);
+ let mut locator = Locator::link(&link).split();
let mut engine = Engine {
routines,
world,
@@ -88,18 +93,227 @@ fn layout_inline_impl(
route: Route::extend(route),
};
- let mut locator = locator.split();
+ let arenas = Arenas::default();
+ let children = (engine.routines.realize)(
+ RealizationKind::LayoutPar,
+ &mut engine,
+ &mut locator,
+ &arenas,
+ &elem.body,
+ styles,
+ )?;
+
+ layout_inline_impl(
+ &mut engine,
+ &children,
+ &mut locator,
+ styles,
+ 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.
+pub fn layout_inline<'a>(
+ engine: &mut Engine,
+ children: &[Pair<'a>],
+ locator: &mut SplitLocator<'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, &mut engine, &mut locator, &styles, region, consecutive)?;
+ let (text, segments, spans) = collect(children, engine, locator, &config, region)?;
- // Perform BiDi analysis and then prepares paragraph layout.
- let p = prepare(&mut engine, children, &text, segments, spans, styles)?;
+ // Perform BiDi analysis and performs some preparation steps before we
+ // proceed to line breaking.
+ let p = prepare(engine, &config, &text, segments, spans)?;
- // Break the paragraph into lines.
- let lines = linebreak(&engine, &p, region.x - p.hang);
+ // Break the text into lines.
+ let lines = linebreak(engine, &p, region.x - config.hanging_indent);
// Turn the selected lines into frames.
- finalize(&mut engine, &p, &lines, styles, region, expand, &mut 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.
+///
+/// In the form `Option`, `None` implies that we are creating an
+/// inline layout that isn't a semantic paragraph.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum ParSituation {
+ /// The paragraph is the first thing in the flow.
+ First,
+ /// The paragraph follows another paragraph.
+ Consecutive,
+ /// 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 2dd79aecf..5d7fcd7cb 100644
--- a/crates/typst-layout/src/inline/prepare.rs
+++ b/crates/typst-layout/src/inline/prepare.rs
@@ -1,23 +1,23 @@
-use typst_library::foundations::{Resolve, Smart};
-use typst_library::layout::{Abs, AlignElem, Dir, Em, FixedAlignment};
-use typst_library::model::Linebreaks;
-use typst_library::text::{Costs, Lang, TextElem};
+use typst_library::layout::{Dir, Em};
use unicode_bidi::{BidiInfo, Level as BidiLevel};
use super::*;
-/// A paragraph representation in which children are already layouted and text
-/// is already preshaped.
+/// A representation in which children are already layouted and text is already
+/// preshaped.
///
/// In many cases, we can directly reuse these results when constructing a line.
/// Only when a line break falls onto a text index that is not safe-to-break per
/// rustybuzz, we have to reshape that portion.
pub struct Preparation<'a> {
- /// The paragraph's full text.
+ /// The full text.
pub text: &'a str,
- /// Bidirectional text embedding levels for the paragraph.
+ /// Configuration for inline layout.
+ pub config: &'a Config,
+ /// Bidirectional text embedding levels.
///
- /// This is `None` if the paragraph is BiDi-uniform (all the base direction).
+ /// This is `None` if all text directions are uniform (all the base
+ /// direction).
pub bidi: Option>,
/// Text runs, spacing and layouted elements.
pub items: Vec<(Range, Item<'a>)>,
@@ -25,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 paragraph's resolved horizontal alignment.
- pub align: FixedAlignment,
- /// Whether to justify the paragraph.
- pub justify: bool,
- /// The paragraph's hanging indent.
- pub hang: Abs,
- /// Whether to add spacing between CJK and Latin characters.
- pub cjk_latin_spacing: bool,
- /// Whether font fallback is enabled for this paragraph.
- pub fallback: bool,
- /// How to determine line breaks.
- pub linebreaks: Smart,
- /// The text size.
- pub size: Abs,
}
impl<'a> Preparation<'a> {
@@ -71,20 +49,18 @@ impl<'a> Preparation<'a> {
}
}
-/// Performs BiDi analysis and then prepares paragraph layout by building a
+/// Performs BiDi analysis and then prepares further layout by building a
/// representation on which we can do line breaking without layouting each and
/// every line from scratch.
#[typst_macros::time]
pub fn prepare<'a>(
engine: &mut Engine,
- children: &'a StyleVec,
+ config: &'a Config,
text: &'a str,
segments: Vec>,
spans: SpanMapper,
- styles: StyleChain<'a>,
) -> SourceResult> {
- let dir = TextElem::dir_in(styles);
- let default_level = match dir {
+ let default_level = match config.dir {
Dir::RTL => BidiLevel::rtl(),
_ => BidiLevel::ltr(),
};
@@ -120,28 +96,17 @@ 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);
}
Ok(Preparation {
+ config,
text,
bidi: is_bidi.then_some(bidi),
items,
indices,
spans,
- hyphenate: children.shared_get(styles, TextElem::hyphenate_in),
- costs: TextElem::costs_in(styles),
- dir,
- lang: children.shared_get(styles, TextElem::lang_in),
- align: AlignElem::alignment_in(styles).resolve(styles).x,
- justify: ParElem::justify_in(styles),
- hang: ParElem::hanging_indent_in(styles),
- cjk_latin_spacing,
- fallback: TextElem::fallback_in(styles),
- linebreaks: ParElem::linebreaks_in(styles),
- size: TextElem::size_in(styles),
})
}
diff --git a/crates/typst-layout/src/inline/shaping.rs b/crates/typst-layout/src/inline/shaping.rs
index d6b7632b6..159619eb3 100644
--- a/crates/typst-layout/src/inline/shaping.rs
+++ b/crates/typst-layout/src/inline/shaping.rs
@@ -20,6 +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};
/// The result of shaping text.
///
@@ -28,7 +29,7 @@ use super::{decorate, Item, Range, SpanMapper};
/// frame.
#[derive(Clone)]
pub struct ShapedText<'a> {
- /// The start of the text in the full paragraph.
+ /// The start of the text in the full text.
pub base: usize,
/// The text that was shaped.
pub text: &'a str,
@@ -65,9 +66,9 @@ pub struct ShapedGlyph {
pub y_offset: Em,
/// The adjustability of the glyph.
pub adjustability: Adjustability,
- /// The byte range of this glyph's cluster in the full paragraph. A cluster
- /// is a sequence of one or multiple glyphs that cannot be separated and
- /// must always be treated as a union.
+ /// The byte range of this glyph's cluster in the full inline layout. A
+ /// cluster is a sequence of one or multiple glyphs that cannot be separated
+ /// and must always be treated as a union.
///
/// The range values of the glyphs in a [`ShapedText`] should not overlap
/// with each other, and they should be monotonically increasing (for
@@ -326,6 +327,7 @@ impl<'a> ShapedText<'a> {
offset += width;
}
+ frame.modify(&FrameModifiers::get_in(self.styles));
frame
}
@@ -403,7 +405,7 @@ impl<'a> ShapedText<'a> {
/// Reshape a range of the shaped text, reusing information from this
/// shaping process if possible.
///
- /// The text `range` is relative to the whole paragraph.
+ /// The text `range` is relative to the whole inline layout.
pub fn reshape(&'a self, engine: &Engine, text_range: Range) -> ShapedText<'a> {
let text = &self.text[text_range.start - self.base..text_range.end - self.base];
if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
@@ -463,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();
@@ -568,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;
@@ -810,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-layout/src/lib.rs b/crates/typst-layout/src/lib.rs
index 2e8c1129b..443e90d61 100644
--- a/crates/typst-layout/src/lib.rs
+++ b/crates/typst-layout/src/lib.rs
@@ -6,6 +6,7 @@ mod image;
mod inline;
mod lists;
mod math;
+mod modifiers;
mod pad;
mod pages;
mod repeat;
@@ -16,7 +17,6 @@ mod transforms;
pub use self::flow::{layout_columns, layout_fragment, layout_frame};
pub use self::grid::{layout_grid, layout_table};
pub use self::image::layout_image;
-pub use self::inline::{layout_box, layout_inline};
pub use self::lists::{layout_enum, layout_list};
pub use self::math::{layout_equation_block, layout_equation_inline};
pub use self::pad::layout_pad;
diff --git a/crates/typst-layout/src/lists.rs b/crates/typst-layout/src/lists.rs
index 0d51a1e4e..f8d910abf 100644
--- a/crates/typst-layout/src/lists.rs
+++ b/crates/typst-layout/src/lists.rs
@@ -4,11 +4,12 @@ use typst_library::diag::SourceResult;
use typst_library::engine::Engine;
use typst_library::foundations::{Content, Context, Depth, Packed, StyleChain};
use typst_library::introspection::Locator;
+use typst_library::layout::grid::resolve::{Cell, CellGrid};
use typst_library::layout::{Axes, Fragment, HAlignment, Regions, Sizing, VAlignment};
-use typst_library::model::{EnumElem, ListElem, Numbering, ParElem};
+use typst_library::model::{EnumElem, ListElem, Numbering, ParElem, ParbreakElem};
use typst_library::text::TextElem;
-use crate::grid::{Cell, CellGrid, GridLayouter};
+use crate::grid::GridLayouter;
/// Layout the list.
#[typst_macros::time(span = elem.span())]
@@ -21,8 +22,9 @@ pub fn layout_list(
) -> SourceResult {
let indent = elem.indent(styles);
let body_indent = elem.body_indent(styles);
+ let tight = elem.tight(styles);
let gutter = elem.spacing(styles).unwrap_or_else(|| {
- if elem.tight(styles) {
+ if tight {
ParElem::leading_in(styles).into()
} else {
ParElem::spacing_in(styles).into()
@@ -39,12 +41,18 @@ pub fn layout_list(
let mut cells = vec![];
let mut locator = locator.split();
- for item in elem.children() {
+ for item in &elem.children {
+ // Text in wide lists shall always turn into paragraphs.
+ let mut body = item.body.clone();
+ if !tight {
+ body += ParbreakElem::shared();
+ }
+
cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new(marker.clone(), locator.next(&marker.span())));
cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new(
- item.body.clone().styled(ListElem::set_depth(Depth(1))),
+ body.styled(ListElem::set_depth(Depth(1))),
locator.next(&item.body.span()),
));
}
@@ -77,8 +85,9 @@ pub fn layout_enum(
let reversed = elem.reversed(styles);
let indent = elem.indent(styles);
let body_indent = elem.body_indent(styles);
+ let tight = elem.tight(styles);
let gutter = elem.spacing(styles).unwrap_or_else(|| {
- if elem.tight(styles) {
+ if tight {
ParElem::leading_in(styles).into()
} else {
ParElem::spacing_in(styles).into()
@@ -100,7 +109,7 @@ pub fn layout_enum(
// relation to the item it refers to.
let number_align = elem.number_align(styles);
- for item in elem.children() {
+ for item in &elem.children {
number = item.number(styles).unwrap_or(number);
let context = Context::new(None, Some(styles));
@@ -123,11 +132,17 @@ pub fn layout_enum(
let resolved =
resolved.aligned(number_align).styled(TextElem::set_overhang(false));
+ // Text in wide enums shall always turn into paragraphs.
+ let mut body = item.body.clone();
+ if !tight {
+ body += ParbreakElem::shared();
+ }
+
cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new(resolved, locator.next(&())));
cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new(
- item.body.clone().styled(EnumElem::set_parents(smallvec![number])),
+ body.styled(EnumElem::set_parents(smallvec![number])),
locator.next(&item.body.span()),
));
number =
diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs
index 0ebe785f1..f2dfa2c45 100644
--- a/crates/typst-layout/src/math/accent.rs
+++ b/crates/typst-layout/src/math/accent.rs
@@ -16,7 +16,7 @@ pub fn layout_accent(
styles: StyleChain,
) -> SourceResult<()> {
let cramped = style_cramped();
- let mut base = ctx.layout_into_fragment(elem.base(), styles.chain(&cramped))?;
+ 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 {
@@ -29,12 +29,12 @@ pub fn layout_accent(
let width = elem.size(styles).relative_to(base.width());
- let Accent(c) = elem.accent();
- let mut glyph = GlyphFragment::new(ctx, styles, *c, 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.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/crates/typst-layout/src/math/attach.rs b/crates/typst-layout/src/math/attach.rs
index 263fc5c6d..e1d7d7c9d 100644
--- a/crates/typst-layout/src/math/attach.rs
+++ b/crates/typst-layout/src/math/attach.rs
@@ -1,10 +1,9 @@
use typst_library::diag::SourceResult;
-use typst_library::foundations::{Packed, StyleChain};
+use typst_library::foundations::{Packed, StyleChain, SymbolElem};
use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Rel, Size};
use typst_library::math::{
AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem,
};
-use typst_library::text::TextElem;
use typst_utils::OptionExt;
use super::{
@@ -29,7 +28,7 @@ pub fn layout_attach(
let elem = merged.as_ref().unwrap_or(elem);
let stretch = stretch_size(styles, elem);
- let mut base = ctx.layout_into_fragment(elem.base(), styles)?;
+ let mut base = ctx.layout_into_fragment(&elem.base, styles)?;
let sup_style = style_for_superscript(styles);
let sup_style_chain = styles.chain(&sup_style);
let tl = elem.tl(sup_style_chain);
@@ -95,7 +94,7 @@ pub fn layout_primes(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
- match *elem.count() {
+ match elem.count {
count @ 1..=4 => {
let c = match count {
1 => '′',
@@ -104,13 +103,14 @@ pub fn layout_primes(
4 => '⁗',
_ => unreachable!(),
};
- let f = ctx.layout_into_fragment(&TextElem::packed(c), styles)?;
+ let f = ctx.layout_into_fragment(&SymbolElem::packed(c), styles)?;
ctx.push(f);
}
count => {
// Custom amount of primes
- let prime =
- ctx.layout_into_fragment(&TextElem::packed('′'), styles)?.into_frame();
+ let prime = ctx
+ .layout_into_fragment(&SymbolElem::packed('′'), styles)?
+ .into_frame();
let width = prime.width() * (count + 1) as f64 / 2.0;
let mut frame = Frame::soft(Size::new(width, prime.height()));
frame.set_baseline(prime.ascent());
@@ -134,7 +134,7 @@ pub fn layout_scripts(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
- let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?;
+ let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
fragment.set_limits(Limits::Never);
ctx.push(fragment);
Ok(())
@@ -148,7 +148,7 @@ pub fn layout_limits(
styles: StyleChain,
) -> SourceResult<()> {
let limits = if elem.inline(styles) { Limits::Always } else { Limits::Display };
- let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?;
+ let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
fragment.set_limits(limits);
ctx.push(fragment);
Ok(())
@@ -157,9 +157,9 @@ pub fn layout_limits(
/// Get the size to stretch the base to.
fn stretch_size(styles: StyleChain, elem: &Packed) -> Option> {
// Extract from an EquationElem.
- let mut base = elem.base();
+ let mut base = &elem.base;
while let Some(equation) = base.to_packed::() {
- base = equation.body();
+ base = &equation.body;
}
base.to_packed::().map(|stretch| stretch.size(styles))
diff --git a/crates/typst-layout/src/math/cancel.rs b/crates/typst-layout/src/math/cancel.rs
index 716832fbf..9826397fa 100644
--- a/crates/typst-layout/src/math/cancel.rs
+++ b/crates/typst-layout/src/math/cancel.rs
@@ -16,7 +16,7 @@ pub fn layout_cancel(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
- let body = ctx.layout_into_fragment(elem.body(), styles)?;
+ let body = ctx.layout_into_fragment(&elem.body, styles)?;
// Preserve properties of body.
let body_class = body.class();
diff --git a/crates/typst-layout/src/math/frac.rs b/crates/typst-layout/src/math/frac.rs
index fdc3be172..6d3caac45 100644
--- a/crates/typst-layout/src/math/frac.rs
+++ b/crates/typst-layout/src/math/frac.rs
@@ -1,5 +1,5 @@
use typst_library::diag::SourceResult;
-use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
+use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem};
use typst_library::layout::{Em, Frame, FrameItem, Point, Size};
use typst_library::math::{BinomElem, FracElem};
use typst_library::text::TextElem;
@@ -23,8 +23,8 @@ pub fn layout_frac(
layout_frac_like(
ctx,
styles,
- elem.num(),
- std::slice::from_ref(elem.denom()),
+ &elem.num,
+ std::slice::from_ref(&elem.denom),
false,
elem.span(),
)
@@ -37,7 +37,7 @@ pub fn layout_binom(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
- layout_frac_like(ctx, styles, elem.upper(), elem.lower(), true, elem.span())
+ layout_frac_like(ctx, styles, &elem.upper, &elem.lower, true, elem.span())
}
/// Layout a fraction or binomial.
@@ -80,7 +80,10 @@ fn layout_frac_like(
let denom = ctx.layout_into_frame(
&Content::sequence(
// Add a comma between each element.
- denom.iter().flat_map(|a| [TextElem::packed(','), a.clone()]).skip(1),
+ denom
+ .iter()
+ .flat_map(|a| [SymbolElem::packed(','), a.clone()])
+ .skip(1),
),
styles.chain(&denom_style),
)?;
diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs
index a0453c14f..1b508a349 100644
--- a/crates/typst-layout/src/math/fragment.rs
+++ b/crates/typst-layout/src/math/fragment.rs
@@ -1,23 +1,23 @@
use std::fmt::{self, Debug, Formatter};
use rustybuzz::Feature;
-use smallvec::SmallVec;
use ttf_parser::gsub::{AlternateSubstitution, SingleSubstitution, SubstitutionSubtable};
use ttf_parser::opentype_layout::LayoutTable;
use ttf_parser::{GlyphId, Rect};
use typst_library::foundations::StyleChain;
use typst_library::introspection::Tag;
use typst_library::layout::{
- Abs, Axis, Corner, Em, Frame, FrameItem, HideElem, Point, Size, VAlignment,
+ Abs, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment,
};
use typst_library::math::{EquationElem, MathSize};
-use typst_library::model::{Destination, LinkElem};
use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem};
use typst_library::visualize::Paint;
use typst_syntax::Span;
+use typst_utils::default_math_class;
use unicode_math_class::MathClass;
use super::{stretch_glyph, MathContext, Scaled};
+use crate::modifiers::{FrameModifiers, FrameModify};
#[derive(Debug, Clone)]
pub enum MathFragment {
@@ -245,8 +245,7 @@ pub struct GlyphFragment {
pub class: MathClass,
pub math_size: MathSize,
pub span: Span,
- pub dests: SmallVec<[Destination; 1]>,
- pub hidden: bool,
+ pub modifiers: FrameModifiers,
pub limits: Limits,
pub extended_shape: bool,
}
@@ -277,11 +276,7 @@ impl GlyphFragment {
span: Span,
) -> Self {
let class = EquationElem::class_in(styles)
- .or_else(|| match c {
- ':' => Some(MathClass::Relation),
- '.' | '/' | '⋯' | '⋱' | '⋰' | '⋮' => Some(MathClass::Normal),
- _ => unicode_math_class::class(c),
- })
+ .or_else(|| default_math_class(c))
.unwrap_or(MathClass::Normal);
let mut fragment = Self {
@@ -302,8 +297,7 @@ impl GlyphFragment {
accent_attach: Abs::zero(),
class,
span,
- dests: LinkElem::dests_in(styles),
- hidden: HideElem::hidden_in(styles),
+ modifiers: FrameModifiers::get_in(styles),
extended_shape: false,
};
fragment.set_id(ctx, id);
@@ -390,7 +384,7 @@ impl GlyphFragment {
let mut frame = Frame::soft(size);
frame.set_baseline(self.ascent);
frame.push(Point::with_y(self.ascent + self.shift), FrameItem::Text(item));
- frame.post_process_raw(self.dests, self.hidden);
+ frame.modify(&self.modifiers);
frame
}
@@ -516,7 +510,7 @@ impl FrameFragment {
let base_ascent = frame.ascent();
let accent_attach = frame.width() / 2.0;
Self {
- frame: frame.post_processed(styles),
+ frame: frame.modified(&FrameModifiers::get_in(styles)),
font_size: TextElem::size_in(styles),
class: EquationElem::class_in(styles).unwrap_or(MathClass::Normal),
math_size: EquationElem::size_in(styles),
@@ -632,7 +626,7 @@ pub enum Limits {
impl Limits {
/// The default limit configuration if the given character is the base.
pub fn for_char(c: char) -> Self {
- match unicode_math_class::class(c) {
+ match default_math_class(c) {
Some(MathClass::Large) => {
if is_integral_char(c) {
Limits::Never
diff --git a/crates/typst-layout/src/math/lr.rs b/crates/typst-layout/src/math/lr.rs
index 2f4556fe5..bf8235411 100644
--- a/crates/typst-layout/src/math/lr.rs
+++ b/crates/typst-layout/src/math/lr.rs
@@ -2,6 +2,7 @@ use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain};
use typst_library::layout::{Abs, Axis, Rel};
use typst_library::math::{EquationElem, LrElem, MidElem};
+use typst_utils::SliceExt;
use unicode_math_class::MathClass;
use super::{stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL};
@@ -13,32 +14,23 @@ pub fn layout_lr(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
- let mut body = elem.body();
-
// Extract from an EquationElem.
+ let mut body = &elem.body;
if let Some(equation) = body.to_packed::() {
- body = equation.body();
+ body = &equation.body;
}
// Extract implicit LrElem.
if let Some(lr) = body.to_packed::() {
if lr.size(styles).is_one() {
- body = lr.body();
+ body = &lr.body;
}
}
let mut fragments = ctx.layout_into_fragments(body, styles)?;
// Ignore leading and trailing ignorant fragments.
- let start_idx = fragments
- .iter()
- .position(|f| !f.is_ignorant())
- .unwrap_or(fragments.len());
- let end_idx = fragments
- .iter()
- .skip(start_idx)
- .rposition(|f| !f.is_ignorant())
- .map_or(start_idx, |i| start_idx + i + 1);
+ let (start_idx, end_idx) = fragments.split_prefix_suffix(|f| f.is_ignorant());
let inner_fragments = &mut fragments[start_idx..end_idx];
let axis = scaled!(ctx, styles, axis_height);
@@ -100,7 +92,7 @@ pub fn layout_mid(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
- let mut fragments = ctx.layout_into_fragments(elem.body(), styles)?;
+ let mut fragments = ctx.layout_into_fragments(&elem.body, styles)?;
for fragment in &mut fragments {
match fragment {
diff --git a/crates/typst-layout/src/math/mat.rs b/crates/typst-layout/src/math/mat.rs
index d28bb037d..bf4929026 100644
--- a/crates/typst-layout/src/math/mat.rs
+++ b/crates/typst-layout/src/math/mat.rs
@@ -27,7 +27,7 @@ pub fn layout_vec(
let frame = layout_vec_body(
ctx,
styles,
- elem.children(),
+ &elem.children,
elem.align(styles),
elem.gap(styles),
LeftRightAlternator::Right,
@@ -44,7 +44,7 @@ pub fn layout_mat(
styles: StyleChain,
) -> SourceResult<()> {
let augment = elem.augment(styles);
- let rows = elem.rows();
+ let rows = &elem.rows;
if let Some(aug) = &augment {
for &offset in &aug.hline.0 {
@@ -58,7 +58,7 @@ pub fn layout_mat(
}
}
- let ncols = elem.rows().first().map_or(0, |row| row.len());
+ let ncols = rows.first().map_or(0, |row| row.len());
for &offset in &aug.vline.0 {
if offset == 0 || offset.unsigned_abs() >= ncols {
@@ -97,7 +97,7 @@ pub fn layout_cases(
let frame = layout_vec_body(
ctx,
styles,
- elem.children(),
+ &elem.children,
FixedAlignment::Start,
elem.gap(styles),
LeftRightAlternator::None,
diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs
index 62ecd1725..708a4443d 100644
--- a/crates/typst-layout/src/math/mod.rs
+++ b/crates/typst-layout/src/math/mod.rs
@@ -17,7 +17,9 @@ use rustybuzz::Feature;
use ttf_parser::Tag;
use typst_library::diag::{bail, SourceResult};
use typst_library::engine::Engine;
-use typst_library::foundations::{Content, NativeElement, Packed, Resolve, StyleChain};
+use typst_library::foundations::{
+ Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem,
+};
use typst_library::introspection::{Counter, Locator, SplitLocator, TagElem};
use typst_library::layout::{
Abs, AlignElem, Axes, BlockElem, BoxElem, Em, FixedAlignment, Fragment, Frame, HElem,
@@ -200,8 +202,7 @@ pub fn layout_equation_block(
let counter = Counter::of(EquationElem::elem())
.display_at_loc(engine, elem.location().unwrap(), styles, numbering)?
.spanned(span);
- let number =
- (engine.routines.layout_frame)(engine, &counter, locator.next(&()), styles, pod)?;
+ let number = crate::layout_frame(engine, &counter, locator.next(&()), styles, pod)?;
static NUMBER_GUTTER: Em = Em::new(0.5);
let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles);
@@ -535,6 +536,8 @@ fn layout_realized(
layout_h(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::() {
self::text::layout_text(elem, ctx, styles)?;
+ } else if let Some(elem) = elem.to_packed::() {
+ self::text::layout_symbol(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::() {
layout_box(elem, ctx, styles)?;
} else if elem.is::() {
@@ -615,7 +618,7 @@ fn layout_box(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
- let frame = (ctx.engine.routines.layout_box)(
+ let frame = crate::inline::layout_box(
elem,
ctx.engine,
ctx.locator.next(&elem.span()),
@@ -632,7 +635,7 @@ fn layout_h(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
- if let Spacing::Rel(rel) = elem.amount() {
+ if let Spacing::Rel(rel) = elem.amount {
if rel.rel.is_zero() {
ctx.push(MathFragment::Spacing(rel.abs.resolve(styles), elem.weak(styles)));
}
@@ -641,17 +644,16 @@ 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,
styles: StyleChain,
) -> SourceResult<()> {
- let class = *elem.class();
- let style = EquationElem::set_class(Some(class)).wrap();
- let mut fragment = ctx.layout_into_fragment(elem.body(), styles.chain(&style))?;
- fragment.set_class(class);
- fragment.set_limits(Limits::for_class(class));
+ let style = EquationElem::set_class(Some(elem.class)).wrap();
+ let mut fragment = ctx.layout_into_fragment(&elem.body, styles.chain(&style))?;
+ fragment.set_class(elem.class);
+ fragment.set_limits(Limits::for_class(elem.class));
ctx.push(fragment);
Ok(())
}
@@ -663,7 +665,7 @@ fn layout_op(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
- let fragment = ctx.layout_into_fragment(elem.text(), styles)?;
+ let fragment = ctx.layout_into_fragment(&elem.text, styles)?;
let italics = fragment.italics_correction();
let accent_attach = fragment.accent_attach();
let text_like = fragment.is_text_like();
@@ -689,7 +691,7 @@ fn layout_external(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult {
- (ctx.engine.routines.layout_frame)(
+ crate::layout_frame(
ctx.engine,
content,
ctx.locator.next(&content.span()),
diff --git a/crates/typst-layout/src/math/root.rs b/crates/typst-layout/src/math/root.rs
index 4e5d844f2..a6b5c03d0 100644
--- a/crates/typst-layout/src/math/root.rs
+++ b/crates/typst-layout/src/math/root.rs
@@ -18,7 +18,6 @@ pub fn layout_root(
styles: StyleChain,
) -> SourceResult<()> {
let index = elem.index(styles);
- let radicand = elem.radicand();
let span = elem.span();
let gap = scaled!(
@@ -36,7 +35,7 @@ pub fn layout_root(
let radicand = {
let cramped = style_cramped();
let styles = styles.chain(&cramped);
- let run = ctx.layout_into_run(radicand, styles)?;
+ let run = ctx.layout_into_run(&elem.radicand, styles)?;
let multiline = run.is_multiline();
let mut radicand = run.into_fragment(styles).into_frame();
if multiline {
diff --git a/crates/typst-layout/src/math/stretch.rs b/crates/typst-layout/src/math/stretch.rs
index 4bc5a9262..dafa8cbe8 100644
--- a/crates/typst-layout/src/math/stretch.rs
+++ b/crates/typst-layout/src/math/stretch.rs
@@ -10,6 +10,7 @@ use super::{
delimiter_alignment, GlyphFragment, MathContext, MathFragment, Scaled,
VariantFragment,
};
+use crate::modifiers::FrameModify;
/// Maximum number of times extenders can be repeated.
const MAX_REPEATS: usize = 1024;
@@ -21,7 +22,7 @@ pub fn layout_stretch(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
- let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?;
+ let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
stretch_fragment(
ctx,
styles,
@@ -265,7 +266,7 @@ fn assemble(
let mut frame = Frame::soft(size);
let mut offset = Abs::zero();
frame.set_baseline(baseline);
- frame.post_process_raw(base.dests, base.hidden);
+ frame.modify(&base.modifiers);
for (fragment, advance) in selected {
let pos = match axis {
diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs
index eb30373dd..59ac5b089 100644
--- a/crates/typst-layout/src/math/text.rs
+++ b/crates/typst-layout/src/math/text.rs
@@ -1,8 +1,8 @@
use std::f64::consts::SQRT_2;
-use ecow::{eco_vec, EcoString};
+use ecow::EcoString;
use typst_library::diag::SourceResult;
-use typst_library::foundations::{Packed, StyleChain, StyleVec};
+use typst_library::foundations::{Packed, StyleChain, SymbolElem};
use typst_library::layout::{Abs, Size};
use typst_library::math::{EquationElem, MathSize, MathVariant};
use typst_library::text::{
@@ -20,56 +20,68 @@ pub fn layout_text(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
- let text = elem.text();
+ let text = &elem.text;
let span = elem.span();
- let mut chars = text.chars();
- let math_size = EquationElem::size_in(styles);
- let mut dtls = ctx.dtls_table.is_some();
- let fragment: MathFragment = if let Some(mut glyph) = chars
- .next()
- .filter(|_| chars.next().is_none())
- .map(|c| dtls_char(c, &mut dtls))
- .map(|c| styled_char(styles, c, true))
- .and_then(|c| GlyphFragment::try_new(ctx, styles, c, span))
- {
- // A single letter that is available in the math font.
- if dtls {
- glyph.make_dotless_form(ctx);
- }
+ let fragment = if text.contains(is_newline) {
+ layout_text_lines(text.split(is_newline), span, ctx, styles)?
+ } else {
+ layout_inline_text(text, span, ctx, styles)?
+ };
+ ctx.push(fragment);
+ Ok(())
+}
- match math_size {
- MathSize::Script => {
- glyph.make_script_size(ctx);
- }
- MathSize::ScriptScript => {
- glyph.make_script_script_size(ctx);
- }
- _ => (),
+/// Layout multiple lines of text.
+fn layout_text_lines<'a>(
+ lines: impl Iterator- ,
+ span: Span,
+ ctx: &mut MathContext,
+ styles: StyleChain,
+) -> SourceResult
{
+ let mut fragments = vec![];
+ for (i, line) in lines.enumerate() {
+ if i != 0 {
+ fragments.push(MathFragment::Linebreak);
}
+ if !line.is_empty() {
+ fragments.push(layout_inline_text(line, span, ctx, styles)?.into());
+ }
+ }
+ let mut frame = MathRun::new(fragments).into_frame(styles);
+ let axis = scaled!(ctx, styles, axis_height);
+ frame.set_baseline(frame.height() / 2.0 + axis);
+ Ok(FrameFragment::new(styles, frame))
+}
- if glyph.class == MathClass::Large {
- let mut variant = if math_size == MathSize::Display {
- let height = scaled!(ctx, styles, display_operator_min_height)
- .max(SQRT_2 * glyph.height());
- glyph.stretch_vertical(ctx, height, Abs::zero())
- } else {
- glyph.into_variant()
- };
- // TeXbook p 155. Large operators are always vertically centered on the axis.
- variant.center_on_axis(ctx);
- variant.into()
- } else {
- glyph.into()
- }
- } else if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
- // Numbers aren't that difficult.
+/// Layout the given text string into a [`FrameFragment`] after styling all
+/// characters for the math font (without auto-italics).
+fn layout_inline_text(
+ text: &str,
+ span: Span,
+ ctx: &mut MathContext,
+ styles: StyleChain,
+) -> SourceResult {
+ if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
+ // 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![];
- for c in text.chars() {
- let c = styled_char(styles, c, false);
- fragments.push(GlyphFragment::new(ctx, styles, c, span).into());
+ 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),
+ _ => {}
+ }
+ }
+ fragments.push(glyph.into());
}
let frame = MathRun::new(fragments).into_frame(styles);
- FrameFragment::new(styles, frame).with_text_like(true).into()
+ Ok(FrameFragment::new(styles, frame).with_text_like(true))
} else {
let local = [
TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)),
@@ -77,64 +89,96 @@ pub fn layout_text(
]
.map(|p| p.wrap());
- // Anything else is handled by Typst's standard text layout.
let styles = styles.chain(&local);
- let text: EcoString =
+ let styled_text: EcoString =
text.chars().map(|c| styled_char(styles, c, false)).collect();
- if text.contains(is_newline) {
- let mut fragments = vec![];
- for (i, piece) in text.split(is_newline).enumerate() {
- if i != 0 {
- fragments.push(MathFragment::Linebreak);
- }
- if !piece.is_empty() {
- fragments.push(layout_complex_text(piece, ctx, span, styles)?.into());
- }
- }
- let mut frame = MathRun::new(fragments).into_frame(styles);
- let axis = scaled!(ctx, styles, axis_height);
- frame.set_baseline(frame.height() / 2.0 + axis);
- FrameFragment::new(styles, frame).into()
- } else {
- layout_complex_text(&text, ctx, span, styles)?.into()
+
+ let spaced = styled_text.graphemes(true).nth(1).is_some();
+ let elem = TextElem::packed(styled_text).spanned(span);
+
+ // There isn't a natural width for a paragraph in a math environment;
+ // because it will be placed somewhere probably not at the left margin
+ // it will overflow. So emulate an `hbox` instead and allow the
+ // paragraph to extend as far as needed.
+ let frame = crate::inline::layout_inline(
+ ctx.engine,
+ &[(&elem, styles)],
+ &mut ctx.locator.next(&span).split(),
+ styles,
+ Size::splat(Abs::inf()),
+ false,
+ )?
+ .into_frame();
+
+ Ok(FrameFragment::new(styles, frame)
+ .with_class(MathClass::Alphabetic)
+ .with_text_like(true)
+ .with_spaced(spaced))
+ }
+}
+
+/// Layout a single character in the math font with the correct styling applied
+/// (includes auto-italics).
+pub fn layout_symbol(
+ elem: &Packed,
+ ctx: &mut MathContext,
+ styles: StyleChain,
+) -> SourceResult<()> {
+ // Switch dotless char to normal when we have the dtls OpenType feature.
+ // This should happen before the main styling pass.
+ let (unstyled_c, dtls) = match try_dotless(elem.text) {
+ Some(c) if ctx.dtls_table.is_some() => (c, true),
+ _ => (elem.text, false),
+ };
+ let c = styled_char(styles, unstyled_c, true);
+ let fragment = match GlyphFragment::try_new(ctx, styles, c, elem.span()) {
+ Some(glyph) => layout_glyph(glyph, dtls, ctx, styles),
+ None => {
+ // Not in the math font, fallback to normal inline text layout.
+ layout_inline_text(c.encode_utf8(&mut [0; 4]), elem.span(), ctx, styles)?
+ .into()
}
};
-
ctx.push(fragment);
Ok(())
}
-/// Layout the given text string into a [`FrameFragment`].
-fn layout_complex_text(
- text: &str,
+/// Layout a [`GlyphFragment`].
+fn layout_glyph(
+ mut glyph: GlyphFragment,
+ dtls: bool,
ctx: &mut MathContext,
- span: Span,
styles: StyleChain,
-) -> SourceResult {
- // There isn't a natural width for a paragraph in a math environment;
- // because it will be placed somewhere probably not at the left margin
- // it will overflow. So emulate an `hbox` instead and allow the paragraph
- // to extend as far as needed.
- let spaced = text.graphemes(true).nth(1).is_some();
- let elem = TextElem::packed(text).spanned(span);
- let frame = (ctx.engine.routines.layout_inline)(
- ctx.engine,
- &StyleVec::wrap(eco_vec![elem]),
- ctx.locator.next(&span),
- styles,
- false,
- Size::splat(Abs::inf()),
- false,
- )?
- .into_frame();
+) -> MathFragment {
+ if dtls {
+ glyph.make_dotless_form(ctx);
+ }
+ let math_size = EquationElem::size_in(styles);
+ match math_size {
+ MathSize::Script => glyph.make_script_size(ctx),
+ MathSize::ScriptScript => glyph.make_script_script_size(ctx),
+ _ => {}
+ }
- Ok(FrameFragment::new(styles, frame)
- .with_class(MathClass::Alphabetic)
- .with_text_like(true)
- .with_spaced(spaced))
+ if glyph.class == MathClass::Large {
+ let mut variant = if math_size == MathSize::Display {
+ let height = scaled!(ctx, styles, display_operator_min_height)
+ .max(SQRT_2 * glyph.height());
+ glyph.stretch_vertical(ctx, height, Abs::zero())
+ } else {
+ glyph.into_variant()
+ };
+ // TeXbook p 155. Large operators are always vertically centered on the
+ // axis.
+ variant.center_on_axis(ctx);
+ variant.into()
+ } else {
+ glyph.into()
+ }
}
-/// Select the correct styled math letter.
+/// Style the character by selecting the unicode codepoint for italic, bold,
+/// caligraphic, etc.
///
///
///
@@ -353,15 +397,12 @@ fn greek_exception(
})
}
-/// Switch dotless character to non dotless character for use of the dtls
-/// OpenType feature.
-pub fn dtls_char(c: char, dtls: &mut bool) -> char {
- match (c, *dtls) {
- ('ı', true) => 'i',
- ('ȷ', true) => 'j',
- _ => {
- *dtls = false;
- c
- }
+/// The non-dotless version of a dotless character that can be used with the
+/// `dtls` OpenType feature.
+pub fn try_dotless(c: char) -> Option {
+ match c {
+ 'ı' => Some('i'),
+ 'ȷ' => Some('j'),
+ _ => None,
}
}
diff --git a/crates/typst-layout/src/math/underover.rs b/crates/typst-layout/src/math/underover.rs
index e55996389..7b3617c3e 100644
--- a/crates/typst-layout/src/math/underover.rs
+++ b/crates/typst-layout/src/math/underover.rs
@@ -32,7 +32,7 @@ pub fn layout_underline(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
- layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Under)
+ layout_underoverline(ctx, styles, &elem.body, elem.span(), Position::Under)
}
/// Lays out an [`OverlineElem`].
@@ -42,7 +42,7 @@ pub fn layout_overline(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
- layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Over)
+ layout_underoverline(ctx, styles, &elem.body, elem.span(), Position::Over)
}
/// Lays out an [`UnderbraceElem`].
@@ -55,7 +55,7 @@ pub fn layout_underbrace(
layout_underoverspreader(
ctx,
styles,
- elem.body(),
+ &elem.body,
&elem.annotation(styles),
'⏟',
BRACE_GAP,
@@ -74,7 +74,7 @@ pub fn layout_overbrace(
layout_underoverspreader(
ctx,
styles,
- elem.body(),
+ &elem.body,
&elem.annotation(styles),
'⏞',
BRACE_GAP,
@@ -93,7 +93,7 @@ pub fn layout_underbracket(
layout_underoverspreader(
ctx,
styles,
- elem.body(),
+ &elem.body,
&elem.annotation(styles),
'⎵',
BRACKET_GAP,
@@ -112,7 +112,7 @@ pub fn layout_overbracket(
layout_underoverspreader(
ctx,
styles,
- elem.body(),
+ &elem.body,
&elem.annotation(styles),
'⎴',
BRACKET_GAP,
@@ -131,7 +131,7 @@ pub fn layout_underparen(
layout_underoverspreader(
ctx,
styles,
- elem.body(),
+ &elem.body,
&elem.annotation(styles),
'⏝',
PAREN_GAP,
@@ -150,7 +150,7 @@ pub fn layout_overparen(
layout_underoverspreader(
ctx,
styles,
- elem.body(),
+ &elem.body,
&elem.annotation(styles),
'⏜',
PAREN_GAP,
@@ -169,7 +169,7 @@ pub fn layout_undershell(
layout_underoverspreader(
ctx,
styles,
- elem.body(),
+ &elem.body,
&elem.annotation(styles),
'⏡',
SHELL_GAP,
@@ -188,7 +188,7 @@ pub fn layout_overshell(
layout_underoverspreader(
ctx,
styles,
- elem.body(),
+ &elem.body,
&elem.annotation(styles),
'⏠',
SHELL_GAP,
diff --git a/crates/typst-layout/src/modifiers.rs b/crates/typst-layout/src/modifiers.rs
new file mode 100644
index 000000000..ac5f40b04
--- /dev/null
+++ b/crates/typst-layout/src/modifiers.rs
@@ -0,0 +1,110 @@
+use typst_library::foundations::StyleChain;
+use typst_library::layout::{Fragment, Frame, FrameItem, HideElem, Point};
+use typst_library::model::{Destination, LinkElem};
+
+/// Frame-level modifications resulting from styles that do not impose any
+/// layout structure.
+///
+/// These are always applied at the highest level of style uniformity.
+/// Consequently, they must be applied by all layouters that manually manage
+/// styles of their children (because they can produce children with varying
+/// styles). This currently includes flow, inline, and math layout.
+///
+/// Other layouters don't manually need to handle it because their parents that
+/// result from realization will take care of it and the styles can only apply
+/// to them as a whole, not part of it (since they don't manage styles).
+///
+/// Currently existing frame modifiers are:
+/// - `HideElem::hidden`
+/// - `LinkElem::dests`
+#[derive(Debug, Clone)]
+pub struct FrameModifiers {
+ /// A destination to link to.
+ dest: Option,
+ /// Whether the contents of the frame should be hidden.
+ hidden: bool,
+}
+
+impl FrameModifiers {
+ /// Retrieve all modifications that should be applied per-frame.
+ pub fn get_in(styles: StyleChain) -> Self {
+ Self {
+ dest: LinkElem::current_in(styles),
+ hidden: HideElem::hidden_in(styles),
+ }
+ }
+}
+
+/// Applies [`FrameModifiers`].
+pub trait FrameModify {
+ /// Apply the modifiers in-place.
+ fn modify(&mut self, modifiers: &FrameModifiers);
+
+ /// Apply the modifiers, and return the modified result.
+ fn modified(mut self, modifiers: &FrameModifiers) -> Self
+ where
+ Self: Sized,
+ {
+ self.modify(modifiers);
+ self
+ }
+}
+
+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();
+ }
+ }
+}
+
+impl FrameModify for Fragment {
+ fn modify(&mut self, modifiers: &FrameModifiers) {
+ for frame in self.iter_mut() {
+ frame.modify(modifiers);
+ }
+ }
+}
+
+impl FrameModify for Result
+where
+ T: FrameModify,
+{
+ fn modify(&mut self, props: &FrameModifiers) {
+ if let Ok(inner) = self {
+ inner.modify(props);
+ }
+ }
+}
+
+/// Performs layout and modification in one step.
+///
+/// This just runs `layout(styles).modified(&FrameModifiers::get_in(styles))`,
+/// but with the additional step that redundant modifiers (which are already
+/// applied here) are removed from the `styles` passed to `layout`. This is used
+/// for the layout of containers like `block`.
+pub fn layout_and_modify(styles: StyleChain, layout: F) -> R
+where
+ F: FnOnce(StyleChain) -> R,
+ R: FrameModify,
+{
+ let modifiers = FrameModifiers::get_in(styles);
+
+ // Disable the current link internally since it's already applied at this
+ // level of layout. This means we don't generate redundant nested links,
+ // which may bloat the output considerably.
+ let reset;
+ let outer = styles;
+ let mut styles = styles;
+ if modifiers.dest.is_some() {
+ reset = LinkElem::set_current(None).wrap();
+ styles = outer.chain(&reset);
+ }
+
+ layout(styles).modified(&modifiers)
+}
diff --git a/crates/typst-layout/src/pages/collect.rs b/crates/typst-layout/src/pages/collect.rs
index 0bbae9f4c..8eab18a62 100644
--- a/crates/typst-layout/src/pages/collect.rs
+++ b/crates/typst-layout/src/pages/collect.rs
@@ -23,7 +23,7 @@ pub enum Item<'a> {
/// things like tags and weak pagebreaks.
pub fn collect<'a>(
mut children: &'a mut [Pair<'a>],
- mut locator: SplitLocator<'a>,
+ locator: &mut SplitLocator<'a>,
mut initial: StyleChain<'a>,
) -> Vec- > {
// The collected page-level items.
diff --git a/crates/typst-layout/src/pages/mod.rs b/crates/typst-layout/src/pages/mod.rs
index 27002a6c9..14dc0f3fb 100644
--- a/crates/typst-layout/src/pages/mod.rs
+++ b/crates/typst-layout/src/pages/mod.rs
@@ -83,7 +83,7 @@ fn layout_document_impl(
styles,
)?;
- let pages = layout_pages(&mut engine, &mut children, locator, styles)?;
+ let pages = layout_pages(&mut engine, &mut children, &mut locator, styles)?;
let introspector = Introspector::paged(&pages);
Ok(PagedDocument { pages, info, introspector })
@@ -93,7 +93,7 @@ fn layout_document_impl(
fn layout_pages<'a>(
engine: &mut Engine,
children: &'a mut [Pair<'a>],
- locator: SplitLocator<'a>,
+ locator: &mut SplitLocator<'a>,
styles: StyleChain<'a>,
) -> SourceResult
> {
// Slice up the children into logical parts.
diff --git a/crates/typst-layout/src/pages/run.rs b/crates/typst-layout/src/pages/run.rs
index 79ff5ab05..6d2d29da5 100644
--- a/crates/typst-layout/src/pages/run.rs
+++ b/crates/typst-layout/src/pages/run.rs
@@ -19,7 +19,7 @@ use typst_library::visualize::Paint;
use typst_library::World;
use typst_utils::Numeric;
-use crate::flow::layout_flow;
+use crate::flow::{layout_flow, FlowMode};
/// A mostly finished layout for one page. Needs only knowledge of its exact
/// page number to be finalized into a `Page`. (Because the margins can depend
@@ -181,7 +181,7 @@ fn layout_page_run_impl(
Regions::repeat(area, area.map(Abs::is_finite)),
PageElem::columns_in(styles),
ColumnsElem::gutter_in(styles),
- true,
+ FlowMode::Root,
)?;
// Layouts a single marginal.
diff --git a/crates/typst-layout/src/shapes.rs b/crates/typst-layout/src/shapes.rs
index 7c56bf763..7ab41e9d4 100644
--- a/crates/typst-layout/src/shapes.rs
+++ b/crates/typst-layout/src/shapes.rs
@@ -62,7 +62,7 @@ pub fn layout_path(
axes.resolve(styles).zip_map(region.size, Rel::relative_to).to_point()
};
- let vertices = elem.vertices();
+ let vertices = &elem.vertices;
let points: Vec = vertices.iter().map(|c| resolve(c.vertex())).collect();
let mut size = Size::zero();
@@ -150,7 +150,7 @@ pub fn layout_curve(
) -> SourceResult {
let mut builder = CurveBuilder::new(region, styles);
- for item in elem.components() {
+ for item in &elem.components {
match item {
CurveComponent::Move(element) => {
let relative = element.relative(styles);
@@ -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.
@@ -399,7 +400,7 @@ pub fn layout_polygon(
region: Region,
) -> SourceResult {
let points: Vec = elem
- .vertices()
+ .vertices
.iter()
.map(|c| c.resolve(styles).zip_map(region.size, Rel::relative_to).to_point())
.collect();
@@ -1281,7 +1282,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 +1306,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-layout/src/stack.rs b/crates/typst-layout/src/stack.rs
index a3ebc9f36..c468945eb 100644
--- a/crates/typst-layout/src/stack.rs
+++ b/crates/typst-layout/src/stack.rs
@@ -27,7 +27,7 @@ pub fn layout_stack(
let spacing = elem.spacing(styles);
let mut deferred = None;
- for child in elem.children() {
+ for child in &elem.children {
match child {
StackChild::Spacing(kind) => {
layouter.layout_spacing(*kind);
@@ -36,14 +36,14 @@ pub fn layout_stack(
StackChild::Block(block) => {
// Transparently handle `h`.
if let (Axis::X, Some(h)) = (axis, block.to_packed::()) {
- layouter.layout_spacing(*h.amount());
+ layouter.layout_spacing(h.amount);
deferred = None;
continue;
}
// Transparently handle `v`.
if let (Axis::Y, Some(v)) = (axis, block.to_packed::()) {
- layouter.layout_spacing(*v.amount());
+ layouter.layout_spacing(v.amount);
deferred = None;
continue;
}
diff --git a/crates/typst-layout/src/transforms.rs b/crates/typst-layout/src/transforms.rs
index e0f29c4c2..f4526dd09 100644
--- a/crates/typst-layout/src/transforms.rs
+++ b/crates/typst-layout/src/transforms.rs
@@ -52,7 +52,7 @@ pub fn layout_rotate(
region,
size,
styles,
- elem.body(),
+ &elem.body,
Transform::rotate(angle),
align,
elem.reflow(styles),
@@ -81,7 +81,7 @@ pub fn layout_scale(
region,
size,
styles,
- elem.body(),
+ &elem.body,
Transform::scale(scale.x, scale.y),
elem.origin(styles).resolve(styles),
elem.reflow(styles),
@@ -169,7 +169,7 @@ pub fn layout_skew(
region,
size,
styles,
- elem.body(),
+ &elem.body,
Transform::skew(ax, ay),
align,
elem.reflow(styles),
diff --git a/crates/typst-library/Cargo.toml b/crates/typst-library/Cargo.toml
index c6331bced..b210637a8 100644
--- a/crates/typst-library/Cargo.toml
+++ b/crates/typst-library/Cargo.toml
@@ -39,6 +39,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 }
@@ -61,6 +62,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/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/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/crates/typst-library/src/foundations/args.rs b/crates/typst-library/src/foundations/args.rs
index a60e6d7f2..430c4e9ad 100644
--- a/crates/typst-library/src/foundations/args.rs
+++ b/crates/typst-library/src/foundations/args.rs
@@ -1,4 +1,5 @@
use std::fmt::{self, Debug, Formatter};
+use std::ops::Add;
use ecow::{eco_format, eco_vec, EcoString, EcoVec};
use typst_syntax::{Span, Spanned};
@@ -304,8 +305,6 @@ impl Args {
/// ```
#[func(constructor)]
pub fn construct(
- /// The real arguments (the other argument is just for the docs).
- /// The docs argument cannot be called `args`.
args: &mut Args,
/// The arguments to construct.
#[external]
@@ -366,7 +365,7 @@ impl Debug for Args {
impl Repr for Args {
fn repr(&self) -> EcoString {
let pieces = self.items.iter().map(Arg::repr).collect::>();
- repr::pretty_array_like(&pieces, false).into()
+ eco_format!("arguments{}", repr::pretty_array_like(&pieces, false))
}
}
@@ -376,6 +375,21 @@ impl PartialEq for Args {
}
}
+impl Add for Args {
+ type Output = Self;
+
+ fn add(mut self, rhs: Self) -> Self::Output {
+ self.items.retain(|item| {
+ !item.name.as_ref().is_some_and(|name| {
+ rhs.items.iter().any(|a| a.name.as_ref() == Some(name))
+ })
+ });
+ self.items.extend(rhs.items);
+ self.span = Span::detached();
+ self
+ }
+}
+
/// An argument to a function call: `12` or `draw: false`.
#[derive(Clone, Hash)]
#[allow(clippy::derived_hash_with_manual_eq)]
diff --git a/crates/typst-library/src/foundations/array.rs b/crates/typst-library/src/foundations/array.rs
index 30481cd7f..18020e4cb 100644
--- a/crates/typst-library/src/foundations/array.rs
+++ b/crates/typst-library/src/foundations/array.rs
@@ -301,9 +301,7 @@ impl Array {
#[func]
pub fn find(
&self,
- /// The engine.
engine: &mut Engine,
- /// The callsite context.
context: Tracked,
/// The function to apply to each item. Must return a boolean.
searcher: Func,
@@ -325,9 +323,7 @@ impl Array {
#[func]
pub fn position(
&self,
- /// The engine.
engine: &mut Engine,
- /// The callsite context.
context: Tracked,
/// The function to apply to each item. Must return a boolean.
searcher: Func,
@@ -363,8 +359,6 @@ impl Array {
/// ```
#[func]
pub fn range(
- /// The real arguments (the other arguments are just for the docs, this
- /// function is a bit involved, so we parse the arguments manually).
args: &mut Args,
/// The start of the range (inclusive).
#[external]
@@ -402,9 +396,7 @@ impl Array {
#[func]
pub fn filter(
&self,
- /// The engine.
engine: &mut Engine,
- /// The callsite context.
context: Tracked,
/// The function to apply to each item. Must return a boolean.
test: Func,
@@ -427,9 +419,7 @@ impl Array {
#[func]
pub fn map(
self,
- /// The engine.
engine: &mut Engine,
- /// The callsite context.
context: Tracked,
/// The function to apply to each item.
mapper: Func,
@@ -481,8 +471,6 @@ impl Array {
#[func]
pub fn zip(
self,
- /// The real arguments (the `others` arguments are just for the docs, this
- /// function is a bit involved, so we parse the positional arguments manually).
args: &mut Args,
/// Whether all arrays have to have the same length.
/// For example, `{(1, 2).zip((1, 2, 3), exact: true)}` produces an
@@ -569,9 +557,7 @@ impl Array {
#[func]
pub fn fold(
self,
- /// The engine.
engine: &mut Engine,
- /// The callsite context.
context: Tracked,
/// The initial value to start with.
init: Value,
@@ -631,9 +617,7 @@ impl Array {
#[func]
pub fn any(
self,
- /// The engine.
engine: &mut Engine,
- /// The callsite context.
context: Tracked,
/// The function to apply to each item. Must return a boolean.
test: Func,
@@ -651,9 +635,7 @@ impl Array {
#[func]
pub fn all(
self,
- /// The engine.
engine: &mut Engine,
- /// The callsite context.
context: Tracked,
/// The function to apply to each item. Must return a boolean.
test: Func,
@@ -831,11 +813,8 @@ impl Array {
#[func]
pub fn sorted(
self,
- /// The engine.
engine: &mut Engine,
- /// The callsite context.
context: Tracked,
- /// The callsite span.
span: Span,
/// If given, applies this function to the elements in the array to
/// determine the keys to sort by.
@@ -911,9 +890,7 @@ impl Array {
#[func(title = "Deduplicate")]
pub fn dedup(
self,
- /// The engine.
engine: &mut Engine,
- /// The callsite context.
context: Tracked,
/// If given, applies this function to the elements in the array to
/// determine the keys to deduplicate by.
@@ -997,9 +974,7 @@ impl Array {
#[func]
pub fn reduce(
self,
- /// The engine.
engine: &mut Engine,
- /// The callsite context.
context: Tracked,
/// The reducing function. Must have two parameters: One for the
/// accumulated value and one for an item.
@@ -1154,6 +1129,53 @@ impl FromValue for SmallVec<[T; N]> {
}
}
+/// One element, or multiple provided as an array.
+#[derive(Debug, Clone, PartialEq, Hash)]
+pub struct OneOrMultiple(pub Vec);
+
+impl Reflect for OneOrMultiple {
+ fn input() -> CastInfo {
+ T::input() + Array::input()
+ }
+
+ fn output() -> CastInfo {
+ T::output() + Array::output()
+ }
+
+ fn castable(value: &Value) -> bool {
+ Array::castable(value) || T::castable(value)
+ }
+}
+
+impl IntoValue for OneOrMultiple {
+ fn into_value(self) -> Value {
+ self.0.into_value()
+ }
+}
+
+impl FromValue for OneOrMultiple {
+ fn from_value(value: Value) -> HintedStrResult {
+ if T::castable(&value) {
+ return Ok(Self(vec![T::from_value(value)?]));
+ }
+ if Array::castable(&value) {
+ return Ok(Self(
+ Array::from_value(value)?
+ .into_iter()
+ .map(|value| T::from_value(value))
+ .collect::>()?,
+ ));
+ }
+ Err(Self::error(&value))
+ }
+}
+
+impl Default for OneOrMultiple {
+ fn default() -> Self {
+ Self(vec![])
+ }
+}
+
/// The error message when the array is empty.
#[cold]
fn array_is_empty() -> EcoString {
diff --git a/crates/typst-library/src/foundations/bytes.rs b/crates/typst-library/src/foundations/bytes.rs
index 05fe4763a..d633c99ad 100644
--- a/crates/typst-library/src/foundations/bytes.rs
+++ b/crates/typst-library/src/foundations/bytes.rs
@@ -1,6 +1,8 @@
-use std::borrow::Cow;
+use std::any::Any;
use std::fmt::{self, Debug, Formatter};
+use std::hash::{Hash, Hasher};
use std::ops::{Add, AddAssign, Deref};
+use std::str::Utf8Error;
use std::sync::Arc;
use ecow::{eco_format, EcoString};
@@ -39,28 +41,75 @@ use crate::foundations::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value
/// #str(data.slice(1, 4))
/// ```
#[ty(scope, cast)]
-#[derive(Clone, Hash, Eq, PartialEq)]
-pub struct Bytes(Arc>>);
+#[derive(Clone, Hash)]
+#[allow(clippy::derived_hash_with_manual_eq)]
+pub struct Bytes(Arc>);
impl Bytes {
- /// Create a buffer from a static byte slice.
- pub fn from_static(slice: &'static [u8]) -> Self {
- Self(Arc::new(LazyHash::new(Cow::Borrowed(slice))))
+ /// Create `Bytes` from anything byte-like.
+ ///
+ /// The `data` type will directly back this bytes object. This means you can
+ /// e.g. pass `&'static [u8]` or `[u8; 8]` and no extra vector will be
+ /// allocated.
+ ///
+ /// If the type is `Vec` and the `Bytes` are unique (i.e. not cloned),
+ /// the vector will be reused when mutating to the `Bytes`.
+ ///
+ /// If your source type is a string, prefer [`Bytes::from_string`] to
+ /// directly use the UTF-8 encoded string data without any copying.
+ pub fn new(data: T) -> Self
+ where
+ T: AsRef<[u8]> + Send + Sync + 'static,
+ {
+ Self(Arc::new(LazyHash::new(data)))
+ }
+
+ /// Create `Bytes` from anything string-like, implicitly viewing the UTF-8
+ /// representation.
+ ///
+ /// The `data` type will directly back this bytes object. This means you can
+ /// e.g. pass `String` or `EcoString` without any copying.
+ pub fn from_string(data: T) -> Self
+ where
+ T: AsRef + Send + Sync + 'static,
+ {
+ Self(Arc::new(LazyHash::new(StrWrapper(data))))
}
/// Return `true` if the length is 0.
pub fn is_empty(&self) -> bool {
- self.0.is_empty()
+ self.as_slice().is_empty()
}
- /// Return a view into the buffer.
+ /// Return a view into the bytes.
pub fn as_slice(&self) -> &[u8] {
self
}
- /// Return a copy of the buffer as a vector.
+ /// Try to view the bytes as an UTF-8 string.
+ ///
+ /// If these bytes were created via `Bytes::from_string`, UTF-8 validation
+ /// is skipped.
+ pub fn as_str(&self) -> Result<&str, Utf8Error> {
+ self.inner().as_str()
+ }
+
+ /// Return a copy of the bytes as a vector.
pub fn to_vec(&self) -> Vec {
- self.0.to_vec()
+ self.as_slice().to_vec()
+ }
+
+ /// Try to turn the bytes into a `Str`.
+ ///
+ /// - If these bytes were created via `Bytes::from_string::`, the
+ /// string is cloned directly.
+ /// - If these bytes were created via `Bytes::from_string`, but from a
+ /// different type of string, UTF-8 validation is still skipped.
+ pub fn to_str(&self) -> Result {
+ match self.inner().as_any().downcast_ref::() {
+ Some(string) => Ok(string.clone()),
+ None => self.as_str().map(Into::into),
+ }
}
/// Resolve an index or throw an out of bounds error.
@@ -72,12 +121,15 @@ impl Bytes {
///
/// `index == len` is considered in bounds.
fn locate_opt(&self, index: i64) -> Option {
+ let len = self.as_slice().len();
let wrapped =
- if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) };
+ if index >= 0 { Some(index) } else { (len as i64).checked_add(index) };
+ wrapped.and_then(|v| usize::try_from(v).ok()).filter(|&v| v <= len)
+ }
- wrapped
- .and_then(|v| usize::try_from(v).ok())
- .filter(|&v| v <= self.0.len())
+ /// Access the inner `dyn Bytelike`.
+ fn inner(&self) -> &dyn Bytelike {
+ &**self.0
}
}
@@ -106,7 +158,7 @@ impl Bytes {
/// The length in bytes.
#[func(title = "Length")]
pub fn len(&self) -> usize {
- self.0.len()
+ self.as_slice().len()
}
/// Returns the byte at the specified index. Returns the default value if
@@ -122,13 +174,13 @@ impl Bytes {
default: Option,
) -> StrResult {
self.locate_opt(index)
- .and_then(|i| self.0.get(i).map(|&b| Value::Int(b.into())))
+ .and_then(|i| self.as_slice().get(i).map(|&b| Value::Int(b.into())))
.or(default)
.ok_or_else(|| out_of_bounds_no_default(index, self.len()))
}
- /// Extracts a subslice of the bytes. Fails with an error if the start or end
- /// index is out of bounds.
+ /// Extracts a subslice of the bytes. Fails with an error if the start or
+ /// end index is out of bounds.
#[func]
pub fn slice(
&self,
@@ -148,9 +200,17 @@ impl Bytes {
if end.is_none() {
end = count.map(|c: i64| start + c);
}
+
let start = self.locate(start)?;
let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start);
- Ok(self.0[start..end].into())
+ let slice = &self.as_slice()[start..end];
+
+ // We could hold a view into the original bytes here instead of
+ // making a copy, but it's unclear when that's worth it. Java
+ // originally did that for strings, but went back on it because a
+ // very small view into a very large buffer would be a sort of
+ // memory leak.
+ Ok(Bytes::new(slice.to_vec()))
}
}
@@ -170,7 +230,15 @@ impl Deref for Bytes {
type Target = [u8];
fn deref(&self) -> &Self::Target {
- &self.0
+ self.inner().as_bytes()
+ }
+}
+
+impl Eq for Bytes {}
+
+impl PartialEq for Bytes {
+ fn eq(&self, other: &Self) -> bool {
+ self.0.eq(&other.0)
}
}
@@ -180,18 +248,6 @@ impl AsRef<[u8]> for Bytes {
}
}
-impl From<&[u8]> for Bytes {
- fn from(slice: &[u8]) -> Self {
- Self(Arc::new(LazyHash::new(slice.to_vec().into())))
- }
-}
-
-impl From> for Bytes {
- fn from(vec: Vec) -> Self {
- Self(Arc::new(LazyHash::new(vec.into())))
- }
-}
-
impl Add for Bytes {
type Output = Self;
@@ -207,10 +263,12 @@ impl AddAssign for Bytes {
// Nothing to do
} else if self.is_empty() {
*self = rhs;
- } else if Arc::strong_count(&self.0) == 1 && matches!(**self.0, Cow::Owned(_)) {
- Arc::make_mut(&mut self.0).to_mut().extend_from_slice(&rhs);
+ } else if let Some(vec) = Arc::get_mut(&mut self.0)
+ .and_then(|unique| unique.as_any_mut().downcast_mut::>())
+ {
+ vec.extend_from_slice(&rhs);
} else {
- *self = Self::from([self.as_slice(), rhs.as_slice()].concat());
+ *self = Self::new([self.as_slice(), rhs.as_slice()].concat());
}
}
}
@@ -228,20 +286,79 @@ impl Serialize for Bytes {
}
}
+/// Any type that can back a byte buffer.
+trait Bytelike: Send + Sync {
+ fn as_bytes(&self) -> &[u8];
+ fn as_str(&self) -> Result<&str, Utf8Error>;
+ fn as_any(&self) -> &dyn Any;
+ fn as_any_mut(&mut self) -> &mut dyn Any;
+}
+
+impl Bytelike for T
+where
+ T: AsRef<[u8]> + Send + Sync + 'static,
+{
+ fn as_bytes(&self) -> &[u8] {
+ self.as_ref()
+ }
+
+ fn as_str(&self) -> Result<&str, Utf8Error> {
+ std::str::from_utf8(self.as_ref())
+ }
+
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn as_any_mut(&mut self) -> &mut dyn Any {
+ self
+ }
+}
+
+impl Hash for dyn Bytelike {
+ fn hash(&self, state: &mut H) {
+ self.as_bytes().hash(state);
+ }
+}
+
+/// Makes string-like objects usable with `Bytes`.
+struct StrWrapper(T);
+
+impl Bytelike for StrWrapper
+where
+ T: AsRef + Send + Sync + 'static,
+{
+ fn as_bytes(&self) -> &[u8] {
+ self.0.as_ref().as_bytes()
+ }
+
+ fn as_str(&self) -> Result<&str, Utf8Error> {
+ Ok(self.0.as_ref())
+ }
+
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn as_any_mut(&mut self) -> &mut dyn Any {
+ self
+ }
+}
+
/// A value that can be cast to bytes.
pub struct ToBytes(Bytes);
cast! {
ToBytes,
- v: Str => Self(v.as_bytes().into()),
+ v: Str => Self(Bytes::from_string(v)),
v: Array => Self(v.iter()
.map(|item| match item {
Value::Int(byte @ 0..=255) => Ok(*byte as u8),
Value::Int(_) => bail!("number must be between 0 and 255"),
value => Err(::error(value)),
})
- .collect::, _>>()?
- .into()
+ .collect::, _>>()
+ .map(Bytes::new)?
),
v: Bytes => Self(v),
}
diff --git a/crates/typst-library/src/foundations/calc.rs b/crates/typst-library/src/foundations/calc.rs
index fd4498e07..a8e0eaeb3 100644
--- a/crates/typst-library/src/foundations/calc.rs
+++ b/crates/typst-library/src/foundations/calc.rs
@@ -97,7 +97,6 @@ cast! {
/// ```
#[func(title = "Power")]
pub fn pow(
- /// The callsite span.
span: Span,
/// The base of the power.
///
@@ -159,7 +158,6 @@ pub fn pow(
/// ```
#[func(title = "Exponential")]
pub fn exp(
- /// The callsite span.
span: Span,
/// The exponent of the power.
exponent: Spanned,
@@ -412,7 +410,6 @@ pub fn tanh(
/// ```
#[func(title = "Logarithm")]
pub fn log(
- /// The callsite span.
span: Span,
/// The number whose logarithm to calculate. Must be strictly positive.
value: Spanned,
@@ -454,7 +451,6 @@ pub fn log(
/// ```
#[func(title = "Natural Logarithm")]
pub fn ln(
- /// The callsite span.
span: Span,
/// The number whose logarithm to calculate. Must be strictly positive.
value: Spanned,
@@ -782,7 +778,6 @@ pub fn round(
/// ```
#[func]
pub fn clamp(
- /// The callsite span.
span: Span,
/// The number to clamp.
value: DecNum,
@@ -815,7 +810,6 @@ pub fn clamp(
/// ```
#[func(title = "Minimum")]
pub fn min(
- /// The callsite span.
span: Span,
/// The sequence of values from which to extract the minimum.
/// Must not be empty.
@@ -833,7 +827,6 @@ pub fn min(
/// ```
#[func(title = "Maximum")]
pub fn max(
- /// The callsite span.
span: Span,
/// The sequence of values from which to extract the maximum.
/// Must not be empty.
@@ -911,7 +904,6 @@ pub fn odd(
/// ```
#[func(title = "Remainder")]
pub fn rem(
- /// The span of the function call.
span: Span,
/// The dividend of the remainder.
dividend: DecNum,
@@ -950,7 +942,6 @@ pub fn rem(
/// ```
#[func(title = "Euclidean Division")]
pub fn div_euclid(
- /// The callsite span.
span: Span,
/// The dividend of the division.
dividend: DecNum,
@@ -994,7 +985,6 @@ pub fn div_euclid(
/// ```
#[func(title = "Euclidean Remainder", keywords = ["modulo", "modulus"])]
pub fn rem_euclid(
- /// The callsite span.
span: Span,
/// The dividend of the remainder.
dividend: DecNum,
@@ -1031,7 +1021,6 @@ pub fn rem_euclid(
/// ```
#[func(title = "Quotient")]
pub fn quo(
- /// The span of the function call.
span: Span,
/// The dividend of the quotient.
dividend: DecNum,
diff --git a/crates/typst-library/src/foundations/cast.rs b/crates/typst-library/src/foundations/cast.rs
index 84f38f36e..38f409c67 100644
--- a/crates/typst-library/src/foundations/cast.rs
+++ b/crates/typst-library/src/foundations/cast.rs
@@ -13,7 +13,9 @@ use typst_syntax::{Span, Spanned};
use unicode_math_class::MathClass;
use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult};
-use crate::foundations::{array, repr, NativeElement, Packed, Repr, Str, Type, Value};
+use crate::foundations::{
+ array, repr, Fold, NativeElement, Packed, Repr, Str, Type, Value,
+};
/// Determine details of a type.
///
@@ -497,3 +499,58 @@ cast! {
/// An operator that can be both unary or binary like `+`.
"vary" => MathClass::Vary,
}
+
+/// A type that contains a user-visible source portion and something that is
+/// derived from it, but not user-visible.
+///
+/// An example usage would be `source` being a `DataSource` and `derived` a
+/// TextMate theme parsed from it. With `Derived`, we can store both parts in
+/// the `RawElem::theme` field and get automatic nice `Reflect` and `IntoValue`
+/// impls.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Derived {
+ /// The source portion.
+ pub source: S,
+ /// The derived portion.
+ pub derived: D,
+}
+
+impl Derived {
+ /// Create a new instance from the `source` and the `derived` data.
+ pub fn new(source: S, derived: D) -> Self {
+ Self { source, derived }
+ }
+}
+
+impl Reflect for Derived {
+ fn input() -> CastInfo {
+ S::input()
+ }
+
+ fn output() -> CastInfo {
+ S::output()
+ }
+
+ fn castable(value: &Value) -> bool {
+ S::castable(value)
+ }
+
+ fn error(found: &Value) -> HintedString {
+ S::error(found)
+ }
+}
+
+impl IntoValue for Derived {
+ fn into_value(self) -> Value {
+ self.source.into_value()
+ }
+}
+
+impl Fold for Derived {
+ fn fold(self, outer: Self) -> Self {
+ Self {
+ source: self.source.fold(outer.source),
+ derived: self.derived.fold(outer.derived),
+ }
+ }
+}
diff --git a/crates/typst-library/src/foundations/content.rs b/crates/typst-library/src/foundations/content.rs
index ab2f68ac2..76cd6a222 100644
--- a/crates/typst-library/src/foundations/content.rs
+++ b/crates/typst-library/src/foundations/content.rs
@@ -9,7 +9,6 @@ use std::sync::Arc;
use comemo::Tracked;
use ecow::{eco_format, EcoString};
use serde::{Serialize, Serializer};
-use smallvec::smallvec;
use typst_syntax::Span;
use typst_utils::{fat, singleton, LazyHash, SmallBitSet};
@@ -500,7 +499,7 @@ impl Content {
/// Link the content somewhere.
pub fn linked(self, dest: Destination) -> Self {
- self.styled(LinkElem::set_dests(smallvec![dest]))
+ self.styled(LinkElem::set_current(Some(dest)))
}
/// Set alignments for this content.
diff --git a/crates/typst-library/src/foundations/datetime.rs b/crates/typst-library/src/foundations/datetime.rs
index d15cd417a..2fc48a521 100644
--- a/crates/typst-library/src/foundations/datetime.rs
+++ b/crates/typst-library/src/foundations/datetime.rs
@@ -318,7 +318,6 @@ impl Datetime {
/// ```
#[func]
pub fn today(
- /// The engine.
engine: &mut Engine,
/// An offset to apply to the current UTC date. If set to `{auto}`, the
/// offset will be the local offset.
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/float.rs b/crates/typst-library/src/foundations/float.rs
index c3d4e0e73..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,23 +160,26 @@ 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,
) -> StrResult {
Ok(match size {
- 8 => match endian {
+ 8 => Bytes::new(match endian {
Endianness::Little => self.to_le_bytes(),
Endianness::Big => self.to_be_bytes(),
- }
- .as_slice()
- .into(),
- 4 => match endian {
+ }),
+ 4 => Bytes::new(match endian {
Endianness::Little => (self as f32).to_le_bytes(),
Endianness::Big => (self as f32).to_be_bytes(),
- }
- .as_slice()
- .into(),
+ }),
_ => bail!("size must be either 4 or 8"),
})
}
diff --git a/crates/typst-library/src/foundations/func.rs b/crates/typst-library/src/foundations/func.rs
index 40c826df9..66c6b70a5 100644
--- a/crates/typst-library/src/foundations/func.rs
+++ b/crates/typst-library/src/foundations/func.rs
@@ -9,11 +9,11 @@ use ecow::{eco_format, EcoString};
use typst_syntax::{ast, Span, SyntaxNode};
use typst_utils::{singleton, LazyHash, Static};
-use crate::diag::{bail, SourceResult, StrResult};
+use crate::diag::{bail, At, DeprecationSink, SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{
- cast, repr, scope, ty, Args, CastInfo, Content, Context, Element, IntoArgs, Scope,
- Selector, Type, Value,
+ cast, repr, scope, ty, Args, Bytes, CastInfo, Content, Context, Element, IntoArgs,
+ PluginFunc, Scope, Selector, Type, Value,
};
/// A mapping from argument values to a return value.
@@ -151,6 +151,8 @@ enum Repr {
Element(Element),
/// A user-defined closure.
Closure(Arc>),
+ /// A plugin WebAssembly function.
+ Plugin(Arc),
/// A nested function with pre-applied arguments.
With(Arc<(Func, Args)>),
}
@@ -164,6 +166,7 @@ impl Func {
Repr::Native(native) => Some(native.name),
Repr::Element(elem) => Some(elem.name()),
Repr::Closure(closure) => closure.name(),
+ Repr::Plugin(func) => Some(func.name()),
Repr::With(with) => with.0.name(),
}
}
@@ -176,6 +179,7 @@ impl Func {
Repr::Native(native) => Some(native.title),
Repr::Element(elem) => Some(elem.title()),
Repr::Closure(_) => None,
+ Repr::Plugin(_) => None,
Repr::With(with) => with.0.title(),
}
}
@@ -186,6 +190,7 @@ impl Func {
Repr::Native(native) => Some(native.docs),
Repr::Element(elem) => Some(elem.docs()),
Repr::Closure(_) => None,
+ Repr::Plugin(_) => None,
Repr::With(with) => with.0.docs(),
}
}
@@ -204,6 +209,7 @@ impl Func {
Repr::Native(native) => Some(&native.0.params),
Repr::Element(elem) => Some(elem.params()),
Repr::Closure(_) => None,
+ Repr::Plugin(_) => None,
Repr::With(with) => with.0.params(),
}
}
@@ -221,6 +227,7 @@ impl Func {
Some(singleton!(CastInfo, CastInfo::Type(Type::of::())))
}
Repr::Closure(_) => None,
+ Repr::Plugin(_) => None,
Repr::With(with) => with.0.returns(),
}
}
@@ -231,6 +238,7 @@ impl Func {
Repr::Native(native) => native.keywords,
Repr::Element(elem) => elem.keywords(),
Repr::Closure(_) => &[],
+ Repr::Plugin(_) => &[],
Repr::With(with) => with.0.keywords(),
}
}
@@ -241,16 +249,21 @@ impl Func {
Repr::Native(native) => Some(&native.0.scope),
Repr::Element(elem) => Some(elem.scope()),
Repr::Closure(_) => None,
+ Repr::Plugin(_) => None,
Repr::With(with) => with.0.scope(),
}
}
/// Get a field from this function's scope, if possible.
- pub fn field(&self, field: &str) -> StrResult<&'static Value> {
+ 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(field) => Ok(field),
+ 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}`"),
@@ -266,6 +279,14 @@ impl Func {
}
}
+ /// Extract the plugin function, if it is one.
+ pub fn to_plugin(&self) -> Option<&PluginFunc> {
+ match &self.repr {
+ Repr::Plugin(func) => Some(func),
+ _ => None,
+ }
+ }
+
/// Call the function with the given context and arguments.
pub fn call(
&self,
@@ -307,6 +328,12 @@ impl Func {
context,
args,
),
+ Repr::Plugin(func) => {
+ let inputs = args.all::()?;
+ let output = func.call(inputs).at(args.span)?;
+ args.finish()?;
+ Ok(Value::Bytes(output))
+ }
Repr::With(with) => {
args.items = with.1.items.iter().cloned().chain(args.items).collect();
with.0.call(engine, context, args)
@@ -334,8 +361,6 @@ impl Func {
#[func]
pub fn with(
self,
- /// The real arguments (the other argument is just for the docs).
- /// The docs argument cannot be called `args`.
args: &mut Args,
/// The arguments to apply to the function.
#[external]
@@ -361,8 +386,6 @@ impl Func {
#[func]
pub fn where_(
self,
- /// The real arguments (the other argument is just for the docs).
- /// The docs argument cannot be called `args`.
args: &mut Args,
/// The fields to filter for.
#[variadic]
@@ -414,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,
}
}
@@ -429,12 +452,30 @@ impl From for Func {
}
}
+impl From<&'static NativeFuncData> for Func {
+ fn from(data: &'static NativeFuncData) -> Self {
+ Repr::Native(Static(data)).into()
+ }
+}
+
impl From for Func {
fn from(func: Element) -> Self {
Repr::Element(func).into()
}
}
+impl From for Func {
+ fn from(closure: Closure) -> Self {
+ Repr::Closure(Arc::new(LazyHash::new(closure))).into()
+ }
+}
+
+impl From for Func {
+ fn from(func: PluginFunc) -> Self {
+ Repr::Plugin(Arc::new(func)).into()
+ }
+}
+
/// A Typst function that is defined by a native Rust type that shadows a
/// native Rust function.
pub trait NativeFunc {
@@ -470,12 +511,6 @@ pub struct NativeFuncData {
pub returns: LazyLock,
}
-impl From<&'static NativeFuncData> for Func {
- fn from(data: &'static NativeFuncData) -> Self {
- Repr::Native(Static(data)).into()
- }
-}
-
cast! {
&'static NativeFuncData,
self => Func::from(self).into_value(),
@@ -529,12 +564,6 @@ impl Closure {
}
}
-impl From for Func {
- fn from(closure: Closure) -> Self {
- Repr::Closure(Arc::new(LazyHash::new(closure))).into()
- }
-}
-
cast! {
Closure,
self => Value::Func(self.into()),
diff --git a/crates/typst-library/src/foundations/int.rs b/crates/typst-library/src/foundations/int.rs
index bddffada3..83a89bf8a 100644
--- a/crates/typst-library/src/foundations/int.rs
+++ b/crates/typst-library/src/foundations/int.rs
@@ -1,6 +1,7 @@
use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError};
use ecow::{eco_format, EcoString};
+use smallvec::SmallVec;
use crate::diag::{bail, StrResult};
use crate::foundations::{
@@ -322,7 +323,7 @@ impl i64 {
Endianness::Little => self.to_le_bytes(),
};
- let mut buf = vec![0u8; size];
+ let mut buf = SmallVec::<[u8; 8]>::from_elem(0, size);
match endian {
Endianness::Big => {
// Copy the bytes from the array to the buffer, starting from
@@ -339,7 +340,7 @@ impl i64 {
}
}
- Bytes::from(buf)
+ Bytes::new(buf)
}
}
diff --git a/crates/typst-library/src/foundations/mod.rs b/crates/typst-library/src/foundations/mod.rs
index d960a666c..8e3aa060d 100644
--- a/crates/typst-library/src/foundations/mod.rs
+++ b/crates/typst-library/src/foundations/mod.rs
@@ -25,7 +25,8 @@ mod int;
mod label;
mod module;
mod none;
-mod plugin;
+#[path = "plugin.rs"]
+mod plugin_;
mod scope;
mod selector;
mod str;
@@ -56,7 +57,7 @@ pub use self::int::*;
pub use self::label::*;
pub use self::module::*;
pub use self::none::*;
-pub use self::plugin::*;
+pub use self::plugin_::*;
pub use self::repr::Repr;
pub use self::scope::*;
pub use self::selector::*;
@@ -84,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.category(FOUNDATIONS);
+ global.start_category(crate::Category::Foundations);
global.define_type::();
global.define_type::();
global.define_type::();
@@ -114,16 +108,17 @@ pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) {
global.define_type::();
global.define_type::();
global.define_type::